Безумная подсветка-часы для зеркала. А еще датчик CO2, выключатель света по хлопкам, датчик присутствия в комнате, сенсорная кнопка...
Очередная история о том, как “просто подсветка” внезапно превращается в миниатюрный умный дом с датчиками, хлопками, CO₂ и попытками ESP8266 не умереть в процессе. Формально — проект про зеркало и освещение. По факту — повод запихнуть в корпус всё, что давно валялось в ящике и просилось в бой.
Если коротко: модульная подсветка, которая умеет включаться не только по кнопке, но и по присутствию, расстоянию, звуку и безумной логике из YAML.
После того, как подсобрал себе умную форточку, встал вопрос об ее автоматизации. Прикрутил было по термометру умной настольной лампы, да только смысл в этом только в холодные сезоны есть. А хотелось сделать по содержанию CO2.

Плюс давно пора было попробовать управление освещением по хлопкам добавить.

Плюс умная подсветка для зеркала была бы кстати, что внутри гексагональной полки (с аналоговыми часами).


Так и родилась идея безумной подсветки. Для ее реализации оставалось только определиться с корпусом и прошивкой, схема же представлялась достаточно типовой. Ну а для новичков подготовил небольшой FAQ по применяемым технологиям.
Что такое ESPHome и зачем он нужен?
ESPHome — это система для создания прошивок микроконтроллеров ESP8266 и ESP32 с использованием YAML-конфигураций. Она позволяет быстро разрабатывать устройства умного дома без необходимости писать код на C++.
Как ESPHome интегрируется с Home Assistant?
ESPHome использует нативный API, через который устройства автоматически обнаруживаются в Home Assistant. После прошивки и подключения к сети устройство появляется в интерфейсе и может быть добавлено в систему в несколько кликов.
Обязательно ли использовать Home Assistant?
Нет. ESPHome может работать автономно или через MQTT. Однако связка с Home Assistant считается наиболее удобной и функциональной.
Как происходит прошивка устройства?
Первая прошивка обычно выполняется через USB. После этого можно использовать OTA (обновление по воздуху), что позволяет обновлять устройство удалённо.
Можно ли использовать ESPHome без интернета?
Да. Все взаимодействие происходит локально, интернет не требуется для работы устройств и автоматизаций.
Насколько это надёжно?
При стабильной сети и корректной конфигурации система работает устойчиво. Основные проблемы обычно связаны с Wi-Fi, а не с самим ESPHome.
Какие устройства поддерживаются?
ESPHome поддерживает широкий спектр датчиков и компонентов: температура, влажность, освещение, реле, дисплеи, кнопки и многое другое.
Сложно ли начать?
Базовые проекты достаточно просты, но по мере усложнения конфигураций требуется понимание YAML и принципов работы сети.
В чём преимущества ESPHome по сравнению с готовыми решениями?
- отсутствие облачной зависимости
- гибкость настройки
- низкая стоимость устройств
- возможность полной кастомизации
Какие недостатки?
- необходимость ручной настройки (либо нейросетевой, с чат-ботом)
- зависимость от качества Wi-Fi
- возможные сложности с отладкой
Блоки подсветки
Подсветку решил делать на стандартной адресной ленте с управлением по одному проводу, тут ничего нового. Однако оставался вопрос как ее организовать так, чтобы было удобно крепить. Посмотрел я на свою гексагональную полку, а в ней — дырки отверстия в креплениях есть.

Это навело на мысль, что можно сделать освещение модульным, на основе блоков. А блоки прикреплять к полке на винты, вкручивающиеся в отверстия креплений.
Вот только как делать сами блоки?.. Здесь мне на помощь пришли аккумуляторные лампы по 100 р/штука, о которых даже как-то был обзор.

Из ламп я вытащил начинку, а затем распилил корпус ровно надвое, так чтобы из одной лампы вышло по 2 заготовки для подсветки.

Да, там с одной стороны вырез под PIR, но это некритично, позже поймете.
Внутри лампы есть очень удобные рельсы для платы светильника. Я же нарезал из тонкой жестяной пластины отрезки и наклеил на них ленту. В наличии была только с редкими диодами, потому вышло по 4 диода на блок.

Чтобы это все-таки были блоки, а не просто понапиханные ленты в распиленные корпуса, на 3д принтере распечатал заглушки с отверстиями под провода соединительных разъемов.

Спаял и собрал все поедино, а заглушки прикрутил на один саморез.
Повторил 5 раз — получил 5 блоков. Столько мне и нужно было. В целом, можно наращивать и дальше почти сколько угодно.
После этого побочные модули были готовы, а о главном стоит поговорить отдельно, так как в нем я запихал ультимативное количество электроники, да и по сборке он сильно отличается от остальных.
Главный модуль
Собирать задумал на ESP8266, коих у меня КРАЙНЕ много.

Но обычная подсветка по кнопке — уже не модно, потому решил сделать по приближению, причем с зависимостью яркости от расстояния. А если никого нет — зачем же ей вообще гореть? Для этого пришлось применить давнишние сенсоры LD2420, которых я давно подзакупил под различные самоделки.

Для определения углекислого газа взял сенсор SCD40 сразу с обвязкой для I2C. дорогой, зараза, да еще на модуле дороже. Зато нет заморочи с пайкой! А сам сенсор качественный, не то что аналоговые нагревательные.

Для определения хлопков взял уже давно валявшиеся китайские платки с капсульными микрофонами. После переделки они начинают реагировать только на хлопки (и другие резкие звуки), а на громкие разговоры и прочее перестают. О переделке можно почитать тут.

До кучи решил влепить сенсорную кнопку TTP223 — они копеечные, а иметь тактильное управление может быть полезно — хлопать в ладоши для включения света прикольно, лежа в джакузи/кровати и попивая пина-коладу. Если же проходишь мимо зеркала, полезно иметь возможность прямого управления. Да и в коде можно реализовать какой-нибудь хак со сложными постукиваниями — хоть морзянку набивать.

Давно подумывал о замке для скрытого сейфа с «бондовской» системой открытия. Всякие потайные рычажки не нужны, если любое устройство и так считывает и записывает любое взаимодействие с собой. Бессмысленно, но прикольно, черт возьми!
RGB-ленту я уже показывал. Светодиодов на ней не очень густо, ну да поменяю потом если что.

И, конечно, мне нужен был разъем питания, коим выступил обычный 5525.

Схема тут даже не требуется — все модули подключаются проводами так, как указано в конфигурации YAML (раздел Прошивка). Единственно, напомню как паять обвязку на ESP8266.
GPIO02 — адресная лента WS2812 (Hex Clock / NeoPixel)
GPIO04 — I2C SDA
GPIO05 — I2C SCL
GPIO12 — сенсор касания TTP223
GPIO13 — UART RX (LD2420 радар)
GPIO14 — микрофон (clap sensor)
GPIO15 — UART TX (LD2420 радар)
И еще спаять стандартную обвязку на ESP8266 и кастомную — для модуля датчика звука.
Прикинул по размерам — вроде влезает.

Для ESP8266 применена стандартная обвязка. Настоятельно рекомендую прежде чем запаивать, прошить «голую» конфигурацию без пинов.
Схему я собрал, но как ее запихнуть в корпус?

Что касается ленты, ее я прилепил так же, как и в побочных модулях. Далее сверху на двусторонний скотч приклеил LD2420. Тут как раз очень полезным оказался тот факт, что светодиоды на ленте довольно редкие — как раз хватило места для сенсора между ними. Теперь оставалось этот бедлам с кучей датчиков как-то засунуть в корпус.

Изначально я просто все эти кишки пропихивал рукой внутрь, предварительно прилепив сенсорную кнопку на торец корпуса. Ну и ожидаемо на четвертый раз, когда надо было что-то поправить (а использование дырчатой жести сказалось на просвечивании всех светодиодов на модулях), у меня там что-то закоротило и ESP отправилась в мусорку. Поскольку на ошибках я все же учусь (хотя бы на своих), я решил вырезать еще жестянку и прилепить на нее все модули и ESP. Чтобы у них не было соблазна что-нибудь ЕЩЕ РАЗ закоротить внутри.

И это сработало! Мало того, что устройство стало хоть немного разбираемо, сам процесс сборки-разборки сильно сократился по времени и нервам. Достаточно стало просто аккуратно провести верхнюю пластину со светодиодной лентой до упора, а нижняя пластина уже и сама нормально проходила.
Я также предусмотрел вентиляционные отверстия специально для датчика CO2, потому что в закрытом корпусе (хоть и частично дырявом), его показания были бы менее точные. Просто просверлил 3 отверстия насквозь.

Но пластмасса как-то непонятно просверлилась, да и провода сквозь отверстия видно… Так что мне пришлось еще сделать 3Д-печатные вентзаглушки, которые я прилепил на клею.

В этом модуле выводится только один провод для RGB-ленты, так как это главное устройство. Потому вторую заглушку я сделал под круглый разъем, который туда плотно вставляется.

Заглушка же с выводом для RGB-разъема стандартная.
Теперь, когда все собрано и даже не коротит, пора заставить чудо-девайс работать!
Прошивка
Задачи перед контроллером я поставил нетривиальные. И CO2 по I2C меряй, и данные с сенсора присутствия LD2420 по UART снимай, и ленту красиво зажигай, и часы на ней отображай! А еще 2 «кнопки» — одна сенсорная, другая «хлопковая». Кажется, это самая сложная конфигурация, которую я когда-либо настраивал. Чтобы все это на откровенно устаревшем ESP8266 вертелось, пришлось пойти на кое-какие ухищрения. Во-первых, разгон с 80 до 160 МГц.
substitutions:
name: smart-mirror-backlight
friendly_name: Smart Mirror Backlight
esphome:
name: ${name}
friendly_name: ${friendly_name}
platformio_options:
board_build.f_cpu: 160000000LВо-вторых, выключил стандартные логи работы. Они сильно нагружают аппаратный UART, а на нем висит LD2420.
logger:
level: NONE
baud_rate: 0 # This effectively shuts down the hardware UART0 loggingБез этих базовых вещей контроллер начинал сходить с ума, в логах постоянно фиксировались потери пакетов с датчика присутствия, а устройство подвисало (и подсветка тоже). Но это удалось побороть.
О прочих ухищрениях расскажу уже во время описания основных функций. Начнем как раз с эффекта часов и подсветки зеркала.
Эффекты отрисовываются параллельно на одном «холсте». Значения цветов смешиваются сложением. Потому подсветка зеркала может накладываться на цвета часов. При этом якость подсветки зависит от расстояния до человека, и начинает нарастать, начиная с 70см до 0см, дальше 70 выключается. Часы же работают лишь тогда, когда LD2420 обнаруживает присутствие. В ином случае подсветка вовсе отключается. Есть поправки на гамму, чтобы яркость снижалась более «человеко-читаемо». Все это осуществляется с помощью скрипта и эффекта. Эффект работает постоянно, и обновляется 24 кадра в секунду. Ограниченная частота обновления дает контроллеру немного «подумать» и просчитать скрипты, пока ему в UART долбится LD2420 со своими пакетами.
globals:
- id: target_clock_opacity
type: float
initial_value: '0.0'
- id: target_white_opacity
type: float
initial_value: '0.0'
- id: current_clock_opacity
type: float
initial_value: '0.0'
- id: current_white_opacity
type: float
initial_value: '0.0'
sensor:
- platform: ld2420
moving_distance:
name: Moving Distance
id: moving_dist
filters:
# Filter out UART garbage common on ESP8266 during LED activity
- lambda: if (x > 800.0f) return {}; else return x;
on_value:
then:
- lambda: |-
// Only process distance logic if someone is actually present
if (!id(presence).state) return;
float dist = x;
if (dist < 70.0f) {
float pct = dist / 70.0f;
id(target_clock_opacity) = pct;
id(target_white_opacity) = 1.0f - pct;
} else {
id(target_clock_opacity) = 1.0f;
id(target_white_opacity) = 0.0f;
}
binary_sensor:
- platform: ld2420
has_target:
name: Presence
id: presence
on_release:
# When user is gone, immediately set targets to zero
then:
- lambda: |-
id(target_clock_opacity) = 0.0f;
id(target_white_opacity) = 0.0f;
light:
- platform: neopixelbus
type: GRB
variant: WS2812
method: ESP8266_UART1 # Direct hardware control for GPIO02
pin: GPIO02
num_leds: 24
id: clock_leds
name: "Hex Clock"
gamma_correct: 1.0 # We apply manual gamma for high-quality blending
effects:
- addressable_lambda:
name: "Hex Clock Effect"
update_interval: 40ms
lambda: |-
auto time = id(sntp_time).now();
if (!time.is_valid()) return;
// 1. Interpolate opacity every frame for smooth fading
// (0.08f provides a professional 'soft' transition)
id(current_clock_opacity) += (id(target_clock_opacity) - id(current_clock_opacity)) * 0.08f;
id(current_white_opacity) += (id(target_white_opacity) - id(current_white_opacity)) * 0.08f;
float c_op = id(current_clock_opacity);
float w_op = id(current_white_opacity);
// Turn off if invisible
if (c_op < 0.005f && w_op < 0.005f) {
it.all() = Color::BLACK;
return;
}
// 2. Hand Positions (12 o'clock = LED index 15)
float min_p = fmod(((time.minute + (time.second / 60.0f)) * 0.4f) + 15.0f, 24.0f);
float hour_p = fmod(((time.hour % 12 + (time.minute / 60.0f)) * 2.0f) + 15.0f, 24.0f);
// 3. Gamma & Glow Math
auto apply_gamma = [&](float value) -> float {
return powf(value, 2.2f);
};
auto get_glow = [&](int led_idx, float target_pos) -> float {
float diff = fabsf((float)led_idx - target_pos);
if (diff > 12.0f) diff = 24.0f - diff;
if (diff >= 1.2f) return 0.0f; // Tighter 3-LED spread
return apply_gamma(1.0f - (diff / 1.2f));
};
for (int i = 0; i < it.size(); i++) {
// Background (Mirror Mode)
float white = apply_gamma(w_op);
float r = white, g = white, b = white;
// Clock Hands (Additive Blending)
if (c_op > 0.001f) {
// Minute = Red, Hour = Blue
r += get_glow(i, min_p) * c_op;
b += get_glow(i, hour_p) * c_op;
}
it[i] = Color(
(uint8_t)(fminf(r, 1.0f) * 255.0f),
(uint8_t)(fminf(g, 1.0f) * 255.0f),
(uint8_t)(fminf(b, 1.0f) * 255.0f)
);
}Часы, как видно, аналоговые, и используется интерполяция яркости, чтобы выжать из 24 светодиодов хоть какую-то информацию. По сути это является проекцией одного кольца вычетов на другое, само время-то непрерывно. Исходно есть кольца 12 и 60 (часы и минуты), а мы их проецируем на 24. Если с часами проекция еще нормально работает (24>12), то с минутами приходится исхитряться. Но в целом для понимания в пределах ~5 минут работает нормально.
Короче, получилось или нет — решать вам. Но мне нравится. Выглядит необычно. Может, сами цвета стрелок поменял бы. Сейчас красная — минуты, синяя — часы.
Также включаем эффект при старте контроллера скриптом.
esphome:
name: ${name}
friendly_name: ${friendly_name}
platformio_options:
board_build.f_cpu: 160000000L
on_boot:
priority: -10
then:
- light.turn_on:
id: clock_leds
effect: "Hex Clock Effect"Для обработки логики двойных и тройных хлопков предусмотрены несколько скриптов и переменных, а также слайдеры и кнопки в интерфейсе умного дома. Слайдеры отвечают за длительность промежутков между хлопками. Кнопки отвечают за визуальное подтверждение (лента мигает в такт, заданный слайдерами) и сохранение в энергонезависимую память. Для отображения мигания существует свой скрипт.
number:
- platform: template
name: "Double Clap Gap (RAM)"
id: double_gap_slider
min_value: 50
max_value: 1000
step: 10
initial_value: 350 # Static number for validation
optimistic: true
- platform: template
name: "Triple Clap Gap 1 (RAM)"
id: triple_gap_1_slider
min_value: 50
max_value: 1000
step: 10
initial_value: 250
optimistic: true
- platform: template
name: "Triple Clap Gap 2 (RAM)"
id: triple_gap_2_slider
min_value: 50
max_value: 1000
step: 10
initial_value: 250
optimistic: true
button:
- platform: template
name: "Save Rhythm to Flash"
icon: "mdi:content-save"
on_press:
then:
- lambda: |-
id(saved_double_gap) = (int)id(double_gap_slider).state;
id(saved_triple_gap_1) = (int)id(triple_gap_1_slider).state;
id(saved_triple_gap_2) = (int)id(triple_gap_2_slider).state;
- platform: template
name: "Preview Double Rhythm"
on_press:
then:
- script.execute:
id: rhythm_blink
num: 2
gap1: !lambda "return (int)id(double_gap_slider).state;"
gap2: 0
- platform: template
name: "Preview Triple Rhythm"
on_press:
then:
- script.execute:
id: rhythm_blink
num: 3
gap1: !lambda "return (int)id(triple_gap_1_slider).state;"
gap2: !lambda "return (int)id(triple_gap_2_slider).state;"
script:
# This script performs the actual rhythmic blinking
- id: rhythm_blink
mode: restart
parameters:
num: int
gap1: int
gap2: int
then:
# 1. STOP THE CLOCK EFFECT (Separate block to avoid YAML error)
- light.turn_off:
id: clock_leds
- light.turn_on:
id: clock_leds
effect: none
# --- ВСПЫШКА 1 ---
- light.control: {id: clock_leds, red: 0, green: 1, blue: 0, brightness: 1%, transition_length: 0s}
- delay: 100ms
- light.turn_off:
id: clock_leds
transition_length: 0s
# --- ПАУЗА 1 ---
- if:
condition: { lambda: 'return num >= 2;' }
then:
- delay: !lambda "return gap1;"
# ВСПЫШКА 2
- light.turn_on:
id: clock_leds
effect: none
- delay: 100ms
- light.turn_off:
id: clock_leds
transition_length: 0s
# --- ПАУЗА 2 ---
- if:
condition: { lambda: 'return num == 3;' }
then:
- delay: !lambda "return gap2;"
# ВСПЫШКА 3
- light.turn_on:
id: clock_leds
effect: none
- delay: 100ms
- light.turn_off:
id: clock_leds
transition_length: 0s
# 3. ВОЗВРАТ: Включаем часы обратно
- delay: 100ms
- light.turn_on:
id: clock_leds
effect: "Hex Clock Effect"
brightness: 100%
Слайдерам соответствуют переменные для энергонезависимой памяти.
globals:
# Persistent globals (saved to flash only when we tell them to)
- id: saved_double_gap
type: int
restore_value: true
initial_value: '350'
- id: saved_triple_gap_1
type: int
restore_value: true
initial_value: '250'
- id: saved_triple_gap_2
type: int
restore_value: true
initial_value: '250'При перезапуске значения восстанавливаются скриптом.
esphome:
name: ${name}
friendly_name: ${friendly_name}
platformio_options:
board_build.f_cpu: 160000000L
on_boot:
priority: -10
then:
# Sync saved rhythm into RAM sliders
- number.set:
id: double_gap_slider
value: !lambda "return (float)id(saved_double_gap);"
- number.set:
id: triple_gap_1_slider
value: !lambda "return (float)id(saved_triple_gap_1);"
- number.set:
id: triple_gap_2_slider
value: !lambda "return (float)id(saved_triple_gap_2);"Для детектирования хлопков существует скрипт, привязанный к сенсору хлопков. Еще желательно настроить программный антидребезг, чтобы не ловить лишние шумы. Если детектирование корректно, срабатывает скрипт отправки события хлопков на HomeAssistant. Там их можно ловить и использовать для автоматизаций (управление светом, вентиляцией и т.д.)
- platform: gpio
pin:
number: GPIO14
mode: INPUT_PULLUP
inverted: true
id: mic_pin
filters:
- delayed_off: 40ms # Debounce burst noise
on_press:
then:
- lambda: |-
uint32_t now = millis();
uint32_t diff = now - id(last_clap_timestamp);
id(last_clap_timestamp) = now;
if (id(clap_count) == 0) {
id(clap_count) = 1;
} else if (id(clap_count) == 1) {
id(gap_1) = diff;
id(clap_count) = 2;
} else if (id(clap_count) == 2) {
id(gap_2) = diff;
id(clap_count) = 3;
}
- script.execute: analyze_rhythm
- id: analyze_rhythm
mode: restart
then:
- delay: 800ms
- lambda: |-
int count = id(clap_count);
int g1 = id(gap_1);
int g2 = id(gap_2);
int tolerance = 60;
if (count == 1) {
id(fire_clap_event).execute("single_clap");
}
else if (count == 2) {
int target = (int)id(double_gap_slider).state;
if (abs(g1 - target) <= tolerance) {
id(fire_clap_event).execute("double_clap");
id(rhythm_blink).execute(2, g1, 0);
}
}
else if (count == 3) {
int t1 = (int)id(triple_gap_1_slider).state;
int t2 = (int)id(triple_gap_2_slider).state;
if (abs(g1 - t1) <= tolerance && abs(g2 - t2) <= tolerance) {
id(fire_clap_event).execute("triple_clap");
id(rhythm_blink).execute(3, g1, g2);
}
}
id(clap_count) = 0;
id(gap_1) = 0;
id(gap_2) = 0;
- id: fire_clap_event
mode: parallel
parameters:
event_type: string
then:
- homeassistant.event:
event: esphome.clap_event
data:
type: !lambda 'return event_type;'Планирую в будущем переделать запоминание хлопков — лучше сделать чтобы была кнопка «записи» хлопков. Нажал — хлопнул как тебе вздумается — нажал «сохранить». И не нужны никакие слайдеры, и интуитивно. Но закодить это будет посложнее...
Остальные модули управляются вполне стандартно, так что описывать это смысла нет. Полная конфигурация со всеми устройствами и скриптами же выглядит следующим образом.
substitutions:
name: smart-mirror-backlight
friendly_name: Smart Mirror Backlight
esphome:
name: ${name}
friendly_name: ${friendly_name}
platformio_options:
board_build.f_cpu: 160000000L
on_boot:
priority: -10
then:
- light.turn_on:
id: clock_leds
effect: "Hex Clock Effect"
# Sync saved rhythm into RAM sliders
- number.set:
id: double_gap_slider
value: !lambda "return (float)id(saved_double_gap);"
- number.set:
id: triple_gap_1_slider
value: !lambda "return (float)id(saved_triple_gap_1);"
- number.set:
id: triple_gap_2_slider
value: !lambda "return (float)id(saved_triple_gap_2);"
esp8266:
board: esp12e
logger:
level: NONE
baud_rate: 0 # This effectively shuts down the hardware UART0 logging
# Enable Home Assistant API
api:
ota:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
captive_portal:
globals:
- id: target_clock_opacity
type: float
initial_value: '0.0'
- id: target_white_opacity
type: float
initial_value: '0.0'
- id: current_clock_opacity
type: float
initial_value: '0.0'
- id: current_white_opacity
type: float
initial_value: '0.0'
# Persistent globals (saved to flash only when we tell them to)
- id: saved_double_gap
type: int
restore_value: true
initial_value: '350'
- id: saved_triple_gap_1
type: int
restore_value: true
initial_value: '250'
- id: saved_triple_gap_2
type: int
restore_value: true
initial_value: '250'
# --- Real-time Clap Calculation (Missing variables added here) ---
- id: clap_count
type: int
initial_value: '0'
- id: last_clap_timestamp
type: uint32_t
initial_value: '0'
- id: gap_1
type: int
initial_value: '0'
- id: gap_2
type: int
initial_value: '0'
script:
# This script performs the actual rhythmic blinking
- id: rhythm_blink
mode: restart
parameters:
num: int
gap1: int
gap2: int
then:
# 1. STOP THE CLOCK EFFECT (Separate block to avoid YAML error)
- light.turn_off:
id: clock_leds
- light.turn_on:
id: clock_leds
effect: none
# --- ВСПЫШКА 1 ---
- light.control: {id: clock_leds, red: 0, green: 1, blue: 0, brightness: 1%, transition_length: 0s}
- delay: 100ms
- light.turn_off:
id: clock_leds
transition_length: 0s
# --- ПАУЗА 1 ---
- if:
condition: { lambda: 'return num >= 2;' }
then:
- delay: !lambda "return gap1;"
# ВСПЫШКА 2
- light.turn_on:
id: clock_leds
effect: none
- delay: 100ms
- light.turn_off:
id: clock_leds
transition_length: 0s
# --- ПАУЗА 2 ---
- if:
condition: { lambda: 'return num == 3;' }
then:
- delay: !lambda "return gap2;"
# ВСПЫШКА 3
- light.turn_on:
id: clock_leds
effect: none
- delay: 100ms
- light.turn_off:
id: clock_leds
transition_length: 0s
# 3. ВОЗВРАТ: Включаем часы обратно
- delay: 100ms
- light.turn_on:
id: clock_leds
effect: "Hex Clock Effect"
brightness: 100%
- id: analyze_rhythm
mode: restart
then:
- delay: 800ms
- lambda: |-
int count = id(clap_count);
int g1 = id(gap_1);
int g2 = id(gap_2);
int tolerance = 60;
if (count == 1) {
id(fire_clap_event).execute("single_clap");
}
else if (count == 2) {
int target = (int)id(double_gap_slider).state;
if (abs(g1 - target) <= tolerance) {
id(fire_clap_event).execute("double_clap");
id(rhythm_blink).execute(2, g1, 0);
}
}
else if (count == 3) {
int t1 = (int)id(triple_gap_1_slider).state;
int t2 = (int)id(triple_gap_2_slider).state;
if (abs(g1 - t1) <= tolerance && abs(g2 - t2) <= tolerance) {
id(fire_clap_event).execute("triple_clap");
id(rhythm_blink).execute(3, g1, g2);
}
}
id(clap_count) = 0;
id(gap_1) = 0;
id(gap_2) = 0;
- id: fire_clap_event
mode: parallel
parameters:
event_type: string
then:
- homeassistant.event:
event: esphome.clap_event
data:
type: !lambda 'return event_type;'
i2c:
sda: GPIO04
scl: GPIO05
scan: true
id: bus_a
uart:
tx_pin: GPIO15
rx_pin: GPIO13
baud_rate: 115200
ld2420:
text_sensor:
- platform: ld2420
fw_version:
name: LD2420 Firmware
- platform: template
name: ${friendly_name} uptime
lambda: |-
int seconds = (id(uptime_sec).state);
int days = seconds / (24 * 3600);
seconds = seconds % (24 * 3600);
int hours = seconds / 3600;
seconds = seconds % 3600;
int minutes = seconds / 60;
seconds = seconds % 60;
return { (String(days) +" д. " + String(hours) +" ч. " + String(minutes) +" мин.").c_str() };
icon: mdi:clock
update_interval: 113s
sensor:
- platform: ld2420
moving_distance:
name: Moving Distance
id: moving_dist
filters:
# Filter out UART garbage common on ESP8266 during LED activity
- lambda: if (x > 800.0f) return {}; else return x;
on_value:
then:
- lambda: |-
// Only process distance logic if someone is actually present
if (!id(presence).state) return;
float dist = x;
if (dist < 70.0f) {
float pct = dist / 70.0f;
id(target_clock_opacity) = pct;
id(target_white_opacity) = 1.0f - pct;
} else {
id(target_clock_opacity) = 1.0f;
id(target_white_opacity) = 0.0f;
}
- platform: scd4x
co2:
name: "Workshop CO2"
temperature:
name: "Workshop Temperature"
humidity:
name: "Workshop Humidity"
- platform: uptime
name: ${name}-uptime
id: uptime_sec
internal: true
- platform: wifi_signal
name: ${name}-WiFi-Signal"
update_interval: 60s
binary_sensor:
- platform: ld2420
has_target:
name: Presence
id: presence
on_release:
# When user is gone, immediately set targets to zero
then:
- lambda: |-
id(target_clock_opacity) = 0.0f;
id(target_white_opacity) = 0.0f;
- platform: gpio
pin: GPIO12
name: Touch Sensor
id: ttp223_pin
device_class: motion
- platform: gpio
pin:
number: GPIO14
mode: INPUT_PULLUP
inverted: true
id: mic_pin
filters:
- delayed_off: 40ms # Debounce burst noise
on_press:
then:
- lambda: |-
uint32_t now = millis();
uint32_t diff = now - id(last_clap_timestamp);
id(last_clap_timestamp) = now;
if (id(clap_count) == 0) {
id(clap_count) = 1;
} else if (id(clap_count) == 1) {
id(gap_1) = diff;
id(clap_count) = 2;
} else if (id(clap_count) == 2) {
id(gap_2) = diff;
id(clap_count) = 3;
}
- script.execute: analyze_rhythm
select:
- platform: ld2420
operating_mode:
name: Operating Mode
number:
- platform: ld2420
presence_timeout:
name: Detection Presence Timeout
min_gate_distance:
name: Detection Gate Minimum
max_gate_distance:
name: Detection Gate Maximum
# See "Number" section below for detail
gate_select:
name: Select Gate to Set
still_threshold:
name: Set Still Threshold Value
move_threshold:
name: Set Move Threshold Value
- platform: template
name: "Double Clap Gap (RAM)"
id: double_gap_slider
min_value: 50
max_value: 1000
step: 10
initial_value: 350 # Static number for validation
optimistic: true
- platform: template
name: "Triple Clap Gap 1 (RAM)"
id: triple_gap_1_slider
min_value: 50
max_value: 1000
step: 10
initial_value: 250
optimistic: true
- platform: template
name: "Triple Clap Gap 2 (RAM)"
id: triple_gap_2_slider
min_value: 50
max_value: 1000
step: 10
initial_value: 250
optimistic: true
button:
- platform: ld2420
apply_config:
name: Apply Config
factory_reset:
name: Factory Reset
restart_module:
name: Restart Module
revert_config:
name: Undo Edits
- platform: template
name: "Save Rhythm to Flash"
icon: "mdi:content-save"
on_press:
then:
- lambda: |-
id(saved_double_gap) = (int)id(double_gap_slider).state;
id(saved_triple_gap_1) = (int)id(triple_gap_1_slider).state;
id(saved_triple_gap_2) = (int)id(triple_gap_2_slider).state;
- platform: template
name: "Preview Double Rhythm"
on_press:
then:
- script.execute:
id: rhythm_blink
num: 2
gap1: !lambda "return (int)id(double_gap_slider).state;"
gap2: 0
- platform: template
name: "Preview Triple Rhythm"
on_press:
then:
- script.execute:
id: rhythm_blink
num: 3
gap1: !lambda "return (int)id(triple_gap_1_slider).state;"
gap2: !lambda "return (int)id(triple_gap_2_slider).state;"
time:
- platform: sntp
id: sntp_time
light:
- platform: neopixelbus
type: GRB
variant: WS2812
method: ESP8266_UART1 # Direct hardware control for GPIO02
pin: GPIO02
num_leds: 24
id: clock_leds
name: "Hex Clock"
gamma_correct: 1.0 # We apply manual gamma for high-quality blending
effects:
- addressable_lambda:
name: "Hex Clock Effect"
update_interval: 40ms
lambda: |-
auto time = id(sntp_time).now();
if (!time.is_valid()) return;
// 1. Interpolate opacity every frame for smooth fading
// (0.08f provides a professional 'soft' transition)
id(current_clock_opacity) += (id(target_clock_opacity) - id(current_clock_opacity)) * 0.08f;
id(current_white_opacity) += (id(target_white_opacity) - id(current_white_opacity)) * 0.08f;
float c_op = id(current_clock_opacity);
float w_op = id(current_white_opacity);
// Turn off if invisible
if (c_op < 0.005f && w_op < 0.005f) {
it.all() = Color::BLACK;
return;
}
// 2. Hand Positions (12 o'clock = LED index 15)
float min_p = fmod(((time.minute + (time.second / 60.0f)) * 0.4f) + 15.0f, 24.0f);
float hour_p = fmod(((time.hour % 12 + (time.minute / 60.0f)) * 2.0f) + 15.0f, 24.0f);
// 3. Gamma & Glow Math
auto apply_gamma = [&](float value) -> float {
return powf(value, 2.2f);
};
auto get_glow = [&](int led_idx, float target_pos) -> float {
float diff = fabsf((float)led_idx - target_pos);
if (diff > 12.0f) diff = 24.0f - diff;
if (diff >= 1.2f) return 0.0f; // Tighter 3-LED spread
return apply_gamma(1.0f - (diff / 1.2f));
};
for (int i = 0; i < it.size(); i++) {
// Background (Mirror Mode)
float white = apply_gamma(w_op);
float r = white, g = white, b = white;
// Clock Hands (Additive Blending)
if (c_op > 0.001f) {
// Minute = Red, Hour = Blue
r += get_glow(i, min_p) * c_op;
b += get_glow(i, hour_p) * c_op;
}
it[i] = Color(
(uint8_t)(fminf(r, 1.0f) * 255.0f),
(uint8_t)(fminf(g, 1.0f) * 255.0f),
(uint8_t)(fminf(b, 1.0f) * 255.0f)
);
}В общем и целом, ESP8266 для такого проекта уже слабовата. Если бы собирал еще раз, брал бы ESP32. Там нет проблем с параллельным чтением UART и выполнением скриптов, и заморачиваться с оптимизацией не нужно.
Интеграция и автоматизации
Вывел на панель своей комнаты данные по углекислому газу.

По автоматизациям начнем с того, что и было толчком к проекту — автоматизация окна. Для этого я прямо в интерфейсе умного дома настроил открытие окна при >800ppm (комфортная граница для помещений), либо если слишком жарко >27.

Кстати, если подойти к зеркалу вплотную, ppm закономерно подскакивает (ведь начинаешь дышать прямо на вентиляцию датчика CO2), что говорит сенсору «Душно, проветри». Интересно получается...
А закрытие происходит либо когда стало <=850ppm CO2, либо слишком холодно (<21, для холодной погоды).

Далее я событие двойного хлопка привязал к переключению верхнего света. Теперь можно включать-выключать освещение, не поднимая задницы. Целлюлит скажет вам спасибо!

Подумываю использовать одинарный хлопок как сигнализацию. Если дома никого нет, а что-то «само» хлопает, возможно, этому чему-то кто-то помог. Но здесь нужно продумать интеграцию камер по движению, плюс появился геркон на окне с приводом, плюс разные датчики присутствия по всему дому. Это потянет еще на одну статью, минимум. Сейчас же лень разбираться.
Нажатие сенсорной кнопки тоже привязал к верхнему свету. Зеркало расположено довольно удобно — прошел, нажал. Это если ладони устали хлопать.

Подумываю еще тройной хлопок на что-то приспособить. И, возможно, для сенсорной кнопки еще функций накидать.
Еще, когда начал тестировать, обнаружил, что присутствие определяется с хорошей точностью. Позже повешу хотя бы выключение света на него. А то вышел — забыл. С сенсором присутствия же можно настроить на выключение через определенный период после ухода.
Итоги
Получилось вполне гибкая и удобная подсветка, да еще с кучей приятных бонусов. Если захочу — сделаю еще больше блоков и какую-нибудь анимацию прикручу. За счет модульности ленты это вполне возможно и просто реализовать.
Подсветку достаточно просто адаптировать и для квадратных полок. Разве что квадратные часы смотрятся не столь презентабельно… Но можно и что-нибудь другое придумать. Конечно, я еще поменял бы материал корпуса, а то жалко светильники разбирать, хоть их платы не повреждаются и можно их распихать в по другим корпусам.
Внутрянка у главного модуля не сильно опрятная, зато работает. Скорее всего, второй раз делал бы на макетке, либо плату бы развел. Но переделывать уже не стану :)
Единственный большой минус такой реализации — один канал светодиодов. Нельзя, как в более дорогих решениях, соединять как попало несколько модулей, только паровозиком. Будь электронный мозг у каждого блока, было бы возможно их как-то синхронизировать через общую физическую шину. Но в целом, никто не мешает сделать несколько «головных блоков» и управлять ими синхронно.
Внешний вид и функциональность, я думаю, оценят в комментариях (ссылочки на модели могу там же оставить, кстати, если надо). А я пойду пилить свои девайсы дальше. Чао!
| +57 |
2652
42
|
| +110 |
3639
46
|