Skip to content

Instantly share code, notes, and snippets.

@bluemaex
Created February 3, 2026 17:35
Show Gist options
  • Select an option

  • Save bluemaex/f22b206abb93627a639074b591440d3b to your computer and use it in GitHub Desktop.

Select an option

Save bluemaex/f22b206abb93627a639074b591440d3b to your computer and use it in GitHub Desktop.
DeepDeck ESPHome YAML with Focus/Meeting Mode. The background led of a key represent it's state (on or off) - If the device isn't used for a while the LEDs are dimmed
esphome:
name: deepdeck-ahuyama
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "deepdeck"
password: "SuperSecure"
api:
ota:
- platform: esphome
# Enable logging
logger:
substitutions:
# Timing configuration
dim_timeout_s: '30'
off_timeout_s: '120'
idle_check_interval: '5s'
api_boot_delay: '2s'
# Brightness levels (0.0 - 1.0)
dimmed_brightness: '0.4'
status_brightness: '0.5'
globals:
- id: last_activity_time
type: unsigned long
restore_value: no
initial_value: '0'
- id: focus_total_secs
type: int
restore_value: no
initial_value: '1500' # Default 25 minutes (if no HA connection)
- id: focus_finish_timestamp
type: int
restore_value: no
initial_value: '0'
- id: led_state
type: int
restore_value: no
initial_value: '0' # 0=full, 1=dimmed, 2=off
- id: matrix_led_colors
type: std::vector<std::vector<uint8_t>>
restore_value: no
initial_value: |-
{
{255, 0, 0, 80, 0, 0}, // Key 1: red
{ 0, 255, 200, 0, 80, 60}, // Key 2: cyan
{255, 165, 0, 80, 50, 0}, // Key 3: orange
{128, 0, 255, 40, 0, 80}, // Key 4: purple
{ 0, 255, 0, 0, 80, 0}, // Key 5: green
{255, 255, 255, 80, 80, 80}, // Key 6: white
{255, 0, 128, 80, 0, 40}, // Key 7: pink
{ 0, 100, 255, 0, 30, 80}, // Key 8: blue
{255, 255, 0, 80, 80, 0}, // Key 9: yellow
{180, 0, 255, 55, 0, 80}, // Key 10: violet
{ 0, 200, 180, 0, 60, 55}, // Key 11: teal
{255, 100, 80, 80, 30, 25}, // Key 12: coral
{255, 0, 255, 80, 0, 80}, // Key 13: magenta
{128, 255, 0, 40, 80, 0}, // Key 14: lime
{255, 200, 0, 80, 60, 0}, // Key 15: gold
{ 0, 180, 255, 0, 55, 80}, // Key 16: sky blue
}
font:
- file: 'gfonts://Roboto'
id: font_large
size: 16
- file: 'gfonts://Roboto'
id: font_small
size: 12
esphome:
on_boot:
# Run after everything is initialized
priority: -100
then:
- lambda: 'id(last_activity_time) = millis();'
# Turn on status_leds parent at full brightness
- light.turn_on:
id: status_leds
brightness: 100%
transition_length: 0s
# Set individual status_led to black (off)
- light.turn_on:
id: status_led_1
brightness: 0%
red: 0%
green: 0%
blue: 0%
transition_length: 0s
- light.turn_on:
id: status_led_2
brightness: 0%
red: 0%
green: 0%
blue: 0%
transition_length: 0s
# Turn on matrix_leds with black color so our direct LED writes work
- light.turn_on:
id: matrix_leds
brightness: 100%
red: 0%
green: 0%
blue: 0%
transition_length: 0s
api:
on_client_connected:
then:
- delay: ${api_boot_delay}
- script.execute: sync_all_leds
i2c:
sda: GPIO21
scl: GPIO22
scan: true
apds9960:
address: 0x39
update_interval: 10s
proximity_gain: 8x
ambient_light_gain: 16x
gesture_led_drive: 50ma
gesture_gain: 8x
gesture_wait_time: 2.8ms
sensor:
# Rotary Encoders
- platform: rotary_encoder
name: 'Right Knob'
id: right_knob
pin_a: GPIO32
pin_b: GPIO33
on_clockwise:
- script.execute: activity_wake_leds
- homeassistant.event:
event: esphome.deepdeck_right_knob_turned
data:
direction: 'clockwise'
on_anticlockwise:
- script.execute: activity_wake_leds
- homeassistant.event:
event: esphome.deepdeck_right_knob_turned
data:
direction: 'anticlockwise'
internal: true
- platform: rotary_encoder
name: 'Left Knob'
id: left_knob
pin_a: GPIO25
pin_b: GPIO26
on_clockwise:
- script.execute: activity_wake_leds
- homeassistant.event:
event: esphome.deepdeck_left_knob_turned
data:
direction: 'clockwise'
on_anticlockwise:
- script.execute: activity_wake_leds
- homeassistant.event:
event: esphome.deepdeck_left_knob_turned
data:
direction: 'anticlockwise'
internal: true
# Gesture sensor (Individal Color Parts not exposed)
- platform: apds9960
type: PROXIMITY
name: 'APDS9960 Proximity'
light:
# 4x4 Button Matrix
# - The backlight is marked internal to prevent accidental usage
# - The individual Button LEDs are controlled directly via `set_matrix_led`
- platform: esp32_rmt_led_strip
internal: true
rgb_order: GRB
pin: GPIO17
num_leds: 16
chipset: SK6812
name: 'Matrix Backlight'
id: matrix_leds
effects:
- addressable_lambda:
name: 'Focus Complete'
update_interval: 250ms
lambda: |-
static int frame = 0;
if (frame % 2 == 0) {
it.all() = Color(0, 255, 100); // Green
} else {
it.all() = Color(255, 165, 0); // Orange
}
frame++;
if (frame >= 12) {
frame = 0;
id(focus_complete_cleanup).execute();
}
# Status LEDs (parent is internal to prevent accidental usage)
- platform: esp32_rmt_led_strip
rgb_order: GRB
pin: GPIO23
num_leds: 2
chipset: SK6812
name: 'Status LEDs'
id: status_leds
internal: true
- platform: partition
name: 'Status LED 1'
id: status_led_1
segments:
- id: status_leds
from: 0
to: 0
- platform: partition
name: 'Status LED 2'
id: status_led_2
segments:
- id: status_leds
from: 1
to: 1
binary_sensor:
# Rotary Buttons
- platform: gpio
name: 'Right Knob Button'
pin:
number: GPIO27
mode: INPUT_PULLUP
on_press:
- script.execute: activity_wake_leds
- homeassistant.event:
event: esphome.deepdeck_right_knob_pressed
data:
device: 'deepdeck'
- platform: gpio
name: 'Left Knob Button'
pin:
number: GPIO34
mode:
input: true
on_press:
- script.execute: activity_wake_leds
- homeassistant.event:
event: esphome.deepdeck_left_knob_pressed
data:
device: 'deepdeck'
# Gesture sensor - Swipe directions
- platform: apds9960
direction: UP
id: gesture_up
internal: true
on_state:
- script.execute:
id: handle_gesture
direction: 'UP'
- platform: apds9960
direction: DOWN
id: gesture_down
internal: true
on_state:
- script.execute:
id: handle_gesture
direction: 'DOWN'
- platform: apds9960
direction: LEFT
id: gesture_left
internal: true
on_state:
- script.execute:
id: handle_gesture
direction: 'LEFT'
- platform: apds9960
direction: RIGHT
id: gesture_right
internal: true
on_state:
- script.execute:
id: handle_gesture
direction: 'RIGHT'
text_sensor:
- platform: homeassistant
name: 'Display Text'
id: display_text
entity_id: input_text.deepdeck_display_text
- platform: template
name: 'Last Key Pressed'
id: last_key_pressed
- platform: template
name: 'APDS9960 Direction'
id: direction_state
# focus mode
- platform: homeassistant
name: 'Focus Timer State'
id: focus_timer_state
entity_id: timer.deepdeck_focus
on_value:
- lambda: 'id(set_matrix_led)->execute(13, x == "active");'
- platform: homeassistant
id: focus_timer_duration
entity_id: timer.deepdeck_focus
attribute: duration
on_value:
- lambda: |-
int h, m, s;
if (sscanf(x.c_str(), "%d:%d:%d", &h, &m, &s) >= 3) {
id(focus_total_secs) = h * 3600 + m * 60 + s;
}
- platform: homeassistant
id: focus_timer_finishes_at
entity_id: timer.deepdeck_focus
attribute: finishes_at
on_value:
# parse the UTC ISO string to a timestamp and save it in focus_finish_timestamp
- lambda: |-
int year, month, day, hour, minute, second;
if (sscanf(x.c_str(), "%d-%d-%dT%d:%d:%d", &year, &month, &day, &hour, &minute, &second) >= 6) {
struct tm finish_tm = {};
finish_tm.tm_year = year - 1900;
finish_tm.tm_mon = month - 1;
finish_tm.tm_mday = day;
finish_tm.tm_hour = hour;
finish_tm.tm_min = minute;
finish_tm.tm_sec = second;
id(focus_finish_timestamp) = mktime(&finish_tm);
}
- platform: homeassistant
id: focus_complete
entity_id: input_boolean.deepdeck_focus_complete
on_value:
- lambda: |-
if (x == "on") {
auto call = id(matrix_leds).make_call();
call.set_effect("Focus Complete");
call.set_transition_length(0);
call.perform();
}
# Device state for each matrix key
- platform: homeassistant
id: matrix_key_0
entity_id: light.wohnzimmer
on_value:
- lambda: 'id(set_matrix_led)->execute(0, x == "on");'
- platform: homeassistant
id: matrix_key_1
entity_id: light.wled_monitor
on_value:
- lambda: 'id(set_matrix_led)->execute(1, x == "on");'
- platform: homeassistant
id: matrix_key_2
entity_id: light.wled_ambient_group
on_value:
- lambda: 'id(set_matrix_led)->execute(2, x == "on");'
- platform: homeassistant
id: matrix_key_3
entity_id: switch.usb_monitorlicht
on_value:
- lambda: 'id(set_matrix_led)->execute(3, x == "on");'
- platform: homeassistant
id: matrix_key_4
entity_id: switch.usb_banana
on_value:
- lambda: 'id(set_matrix_led)->execute(4, x == "on");'
- platform: template
id: matrix_key_5
lambda: 'return {"off"};'
- platform: template
id: matrix_key_6
lambda: 'return {"off"};'
- platform: template
id: matrix_key_7
lambda: 'return {"off"};'
- platform: template
id: matrix_key_8
lambda: 'return {"off"};'
- platform: template
id: matrix_key_9
lambda: 'return {"off"};'
- platform: template
id: matrix_key_10
lambda: 'return {"off"};'
- platform: template
id: matrix_key_11
lambda: 'return {"off"};'
- platform: homeassistant
id: matrix_key_12
entity_id: media_player.denon_avr_x1200w
on_value:
- lambda: 'id(set_matrix_led)->execute(12, x == "on");'
- platform: template
id: matrix_key_13
lambda: 'return {"off"};'
- platform: homeassistant
id: matrix_key_14
entity_id: input_boolean.deepdeck_meeting_mode
on_value:
- lambda: 'id(set_matrix_led)->execute(14, x == "on");'
- platform: template
id: matrix_key_15
lambda: 'return {"off"};'
display:
- platform: ssd1306_i2c
model: 'SSD1306 128x64'
address: 0x3C
id: oled_display
update_interval: 1s
pages:
- id: main_page
lambda: |-
if (id(time_sntp).now().is_valid()) {
it.strftime(124, 58, id(font_small), TextAlign::BOTTOM_RIGHT, "%H:%M", id(time_sntp).now());
}
if (id(focus_timer_state).state == "active") {
auto utc_now = id(time_sntp).utcnow();
struct tm now_tm = {};
now_tm.tm_year = utc_now.year - 1900;
now_tm.tm_mon = utc_now.month - 1;
now_tm.tm_mday = utc_now.day_of_month;
now_tm.tm_hour = utc_now.hour;
now_tm.tm_min = utc_now.minute;
now_tm.tm_sec = utc_now.second;
int remaining_secs = id(focus_finish_timestamp) - (int)mktime(&now_tm);
if (remaining_secs >= 0) {
it.printf(64, 6, id(font_large), TextAlign::TOP_CENTER, "FOCUS");
int bar_x = 19, bar_y = 26, bar_w = 90, bar_h = 10;
float progress = 1.0f - (float)remaining_secs / (float)id(focus_total_secs);
progress = progress < 0 ? 0 : (progress > 1 ? 1 : progress);
int filled = (int)(bar_w * progress);
it.rectangle(bar_x, bar_y, bar_w, bar_h);
if (filled > 2) {
it.filled_rectangle(bar_x + 1, bar_y + 1, filled - 2, bar_h - 2);
}
it.printf(64, 44, id(font_small), TextAlign::CENTER, "%02d:%02d", remaining_secs / 60, remaining_secs % 60);
return;
}
}
bool is_sleeping = (id(led_state) >= 1);
if (!is_sleeping) {
std::string text = id(display_text).state;
if (!text.empty()) {
it.printf(64, 28, id(font_large), TextAlign::CENTER, "%s", text.c_str());
}
}
matrix_keypad:
id: keypad
columns:
- pin: GPIO16
- pin: GPIO15
- pin: GPIO14
- pin: GPIO13
rows:
- pin: GPIO0
- pin: GPIO4
- pin: GPIO5
- pin: GPIO12
has_diodes: true
keys: '1234567890ABCDEF'
on_key:
- script.execute: activity_wake_leds
- homeassistant.event:
event: esphome.deepdeck_key_pressed
data:
key: !lambda 'return std::string(1, x);'
device: 'deepdeck'
- text_sensor.template.publish:
id: last_key_pressed
state: !lambda 'return std::string(1, x);'
interval:
- interval: ${idle_check_interval}
then:
- lambda: |-
unsigned long elapsed = millis() - id(last_activity_time);
if (elapsed > ${dim_timeout_s} * 1000 && id(led_state) == 0) {
id(led_state) = 1;
id(sync_all_leds).execute();
}
else if (elapsed > ${off_timeout_s} * 1000 && id(led_state) == 1) {
id(led_state) = 2;
id(sync_all_leds).execute();
}
script:
# Handle gesture and reset after a while
- id: handle_gesture
mode: restart
parameters:
direction: string
then:
- script.execute: activity_wake_leds
- homeassistant.event:
event: esphome.deepdeck_gesture
data:
direction: !lambda 'return direction;'
- text_sensor.template.publish:
id: direction_state
state: !lambda 'return direction;'
- delay: 1s
- text_sensor.template.publish:
id: direction_state
state: 'NONE'
# Cleanup after focus animation completes
- id: focus_complete_cleanup
then:
- lambda: |-
auto call = id(matrix_leds).make_call();
call.set_effect("none");
call.set_transition_length(0);
call.perform();
- delay: 150ms
- light.turn_on:
id: matrix_leds
red: 0%
green: 0%
blue: 0%
brightness: 100%
transition_length: 0s
- delay: 50ms
- script.execute: sync_all_leds
# Internal: Apply LED color without showing (for batching)
- id: apply_matrix_led_color
mode: parallel
parameters:
key_idx: int
active: bool
then:
- lambda: |-
auto& color = id(matrix_led_colors)[key_idx];
float r = color[active ? 0 : 3] / 255.0f;
float g = color[active ? 1 : 4] / 255.0f;
float b = color[active ? 2 : 5] / 255.0f;
float brightness[] = {1.0f, ${dimmed_brightness}f, 0.0f};
auto* light = static_cast<esphome::light::AddressableLight*>(id(matrix_leds).get_output());
(*light)[key_idx] = Color(
uint8_t(r * brightness[id(led_state)] * 255),
uint8_t(g * brightness[id(led_state)] * 255),
uint8_t(b * brightness[id(led_state)] * 255)
);
# Set a single key LED by index (0-15) and state
- id: set_matrix_led
parameters:
key_idx: int
active: bool
then:
- lambda: |-
id(apply_matrix_led_color)->execute(key_idx, active);
auto* light = static_cast<esphome::light::AddressableLight*>(id(matrix_leds).get_output());
light->schedule_show();
# Update all matrix LEDs
- id: sync_all_leds
then:
- lambda: |-
esphome::text_sensor::TextSensor* sensors[] = {
id(matrix_key_0), id(matrix_key_1), id(matrix_key_2), id(matrix_key_3),
id(matrix_key_4), id(matrix_key_5), id(matrix_key_6), id(matrix_key_7),
id(matrix_key_8), id(matrix_key_9), id(matrix_key_10), id(matrix_key_11),
id(matrix_key_12), id(matrix_key_13), id(matrix_key_14), id(matrix_key_15)
};
for (int i = 0; i < 16; i++) {
bool active = sensors[i]->state == "on";
id(apply_matrix_led_color)->execute(i, active);
}
auto* light = static_cast<esphome::light::AddressableLight*>(id(matrix_leds).get_output());
light->schedule_show();
# Activity & Wake LEDs from dimmed/off state
- id: activity_wake_leds
mode: restart
then:
- lambda: |-
id(last_activity_time) = millis();
if(id(led_state) != 0) {
id(led_state) = 0;
id(sync_all_leds).execute();
}
automation:
#
# Right Encoder
#
- id: deepdeck_right_knob_volume
alias: 'DeepDeck: Right Knob Volume'
description: 'Adjust Denon volume with right encoder'
mode: restart
trigger:
- platform: event
event_type: esphome.deepdeck_right_knob_turned
action:
- choose:
- conditions:
- condition: template
value_template: "{{ trigger.event.data.direction == 'clockwise' }}"
sequence:
- service: media_player.volume_up
target:
entity_id: media_player.denon_avr_x1200w
- conditions:
- condition: template
value_template: "{{ trigger.event.data.direction == 'anticlockwise' }}"
sequence:
- service: media_player.volume_down
target:
entity_id: media_player.denon_avr_x1200w
- delay:
milliseconds: 100
- service: script.deepdeck_show_message
data:
message: >
Volume: {{ (state_attr('media_player.denon_avr_x1200w', 'volume_level') * 100) | round(0) }}%
- id: deepdeck_right_knob_button_mute
alias: 'DeepDeck: Right Knob Button Mute'
description: 'Toggle Denon mute with right encoder button'
mode: restart
trigger:
- platform: event
event_type: esphome.deepdeck_right_knob_pressed
action:
- service: media_player.volume_mute
target:
entity_id: media_player.denon_avr_x1200w
data:
is_volume_muted: >
{{ not state_attr('media_player.denon_avr_x1200w', 'is_volume_muted') }}
- service: script.deepdeck_show_message
data:
message: >
{% if state_attr('media_player.denon_avr_x1200w', 'is_volume_muted') %}
Unmuted
{% else %}
Muted
{% endif %}
#
# Left Encoder
#
- id: deepdeck_left_knob_brightness
alias: 'DeepDeck: Left Knob Brightness'
description: 'Adjust Wohnzimmer brightness with left encoder'
mode: restart
trigger:
- platform: event
event_type: esphome.deepdeck_left_knob_turned
action:
- [...]
- id: deepdeck_left_knob_brightness
alias: 'DeepDeck: Left Knob Brightness'
description: 'Adjust Wohnzimmer brightness with left encoder'
mode: restart
trigger:
- platform: event
event_type: esphome.deepdeck_left_knob_pressed
action:
- [...]
#
# Matrix Keys
#
- id: deepdeck_key_handler
alias: 'DeepDeck: Key Handler'
description: 'Route key presses to appropriate actions'
mode: restart
trigger:
- platform: event
event_type: esphome.deepdeck_key_pressed
action:
- choose:
# Key 1
- conditions:
- condition: template
value_template: "{{ trigger.event.data.key == '1' }}"
sequence:
- service: light.toggle
target:
entity_id: light.wohnzimmer
- service: script.deepdeck_show_message
data:
message: >
Wohnzimmer {{ states('light.wohnzimmer') | upper }}
# .. repeat until key E
- conditions:
- condition: template
value_template: "{{ trigger.event.data.key == 'F' }}"
sequence:
- service: light.toggle
target:
entity_id: light.wled_monitor
- service: script.deepdeck_show_message
data:
message: >
Monitor {{ states('light.wled_monitor') | upper }}
timer:
deepdeck_focus:
name: DeepDeck Focus Timer
duration: '00:25:00'
icon: mdi:timer-outline
restore: true
input_boolean:
deepdeck_meeting_mode:
name: DeepDeck Meeting Mode
icon: mdi:account-tie-voice
deepdeck_lights_off:
name: DeepDeck Lights Off State
initial: false
deepdeck_focus_complete:
name: DeepDeck Focus Complete
icon: mdi:check-circle
initial: false
input_text:
deepdeck_display_text:
name: DeepDeck Display Text
initial: '~ maexhome ~'
max: 50
script:
deepdeck_reset_display:
alias: 'DeepDeck: Reset Display'
sequence:
- service: input_text.set_value
target:
entity_id: input_text.deepdeck_display_text
data:
value: '~ maexhome ~'
deepdeck_show_message:
alias: 'DeepDeck: Show Message'
description: 'Show a message on display, then reset after delay'
mode: restart
fields:
message:
description: 'Message to display'
example: 'Hello World'
sequence:
- service: input_text.set_value
target:
entity_id: input_text.deepdeck_display_text
data:
value: '{{ message }}'
- delay:
seconds: 3
- service: script.deepdeck_reset_display
automation:
- id: deepdeck_focus_timer_finished
alias: 'DeepDeck: Focus Timer Finished'
description: 'Trigger ESPHome Animation when focus timer completes'
trigger:
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.deepdeck_focus
action:
- service: input_boolean.turn_on
target:
entity_id: input_boolean.deepdeck_focus_complete
- service: script.deepdeck_show_message
data:
message: 'FOCUS DONE'
- service: input_boolean.turn_off
target:
entity_id: input_boolean.deepdeck_focus_complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment