Восемь устройств моего дома должны засыпать в полночь: ночник приглушается, фонарь у калитки горит вполсилы, опрос датчиков реже, насос молчит. Первая версия была честная и мучительная: на каждой плате — свои часы, свои пороги «ночи» и правка при переходе на летнее время. Восемь плат — восемь мест, где время живёт своей жизнью, и однажды они не сошлись: насос проснулся на час раньше всех и разбудил меня.
Проще оказалось вынести время в одно место: планировщик на сервере в 23:00 публикует одно сообщение home/mode = night с флагом retain — и все платы, слушающие этот топик, засыпают сами. Утром home/mode = day — дом просыпается. Режим — не команда конкретному устройству, а погода: она одна на всех, а каждый трактует её по-своему.
Здесь разбираю, кто и как публикует режим, зачем retain, как плата трактует режим локально и почему кнопка у кровати даёт обход с тайм-аутом, не ломая общий режим.
Режим — не команда, а погода
Публикует режим тот, у кого живёт расписание. У меня это cron на сервере: две строки — в 23:00 mosquitto_pub -r -t home/mode -m night, в 7:00 -m day; флаг -r делает сообщение retained. Кто живёт в Node-RED или Home Assistant — ставит там расписание-автоматизацию, разница только в интерфейсе. Суть одна: системная таймзона и её правка — в одном месте, а не в восьми прошивках. Плата про время вообще не знает: ей не нужны ни RTC, ни NTP, ни понятие «полночь». Часы на самой плате нужны в обратном случае — автономный узел без брокера, такое мы собирали в уроке про будильник DS3231.
Retain делает режим последним фактом, а не событием. Плата подключилась в три ночи — при подписке она сразу получает night, не дожидаясь следующего часа Х. Плата пересобралась после ребута — та же история: первый пакет после subscribe приносит актуальный режим. Это тот же механизм, что в уроке про восстановление статусов при рестарте: плата спрашивает у брокера правду и получает её одним сообщением, без EEPROM и без перепрошивки. Режимы при этом остаются словами из короткого словаря: night, day, у меня когда-то был away — четыре значения потолок, иначе каждая плата заводит ветку, которую никто никогда не проверит.
Самое важное в режиме — что его трактует плата, а не брокер. Ночнику night означает яркость 30 из 255, фонарю — «горит вполсилы», опросу DHT — раз в минуту вместо тридцати секунд, как к конфиг-интервалам мы подходили в уроке про DHT22 и интервал из брокера. Брокер не командует пинами — он меняет погоду. Это спасает при добавлении устройства: новое приходит, читает home/mode и само решает, что ночь значит для него; ни одна существующая прошивка не меняется.
#include <WiFi.h>#include <PubSubClient.h> const char* WIFI_SSID = "HomeNet";const char* WIFI_PASS = "supernet123";const char* MQTT_SERVER = "192.168.1.50"; WiFiClient net;PubSubClient client(net); const char* MODE_TOPIC = "home/mode"; const int PIN_BTN = 4; // кнопка у кроватиconst int PIN_LAMP = 25; // ночник, ШИМconst int PIN_LANTERN = 26; // фонарь у калитки, реле String mode = "day"; // режим из брокераint overrideOn = 0;unsigned long overrideEnd = 0;const unsigned long OVERRIDE_MS = 180000; // обход: три минуты int btnWas = 0; void applyMode() { // ночник: каждую волну погоды трактуем по-своему if (overrideOn == 1) { analogWrite(PIN_LAMP, 255); // обход: полный накал } else if (mode == "night") { analogWrite(PIN_LAMP, 30); // ночь: приглушённо } else { analogWrite(PIN_LAMP, 0); // день: спит } // фонарь: в ночь горит, днём спит if (mode == "night") { digitalWrite(PIN_LANTERN, HIGH); } else { digitalWrite(PIN_LANTERN, LOW); }} void callback(char* topic, byte* payload, unsigned int length) { char buf[16]; unsigned int n = length; if (n > 15) { n = 15; } memcpy(buf, payload, n); buf[n] = 0; String m = String(buf); m.trim(); if (m == "night" || m == "day") { overrideOn = 0; // новый режим важнее старого обхода mode = m; Serial.print("[rezhim] "); Serial.println(mode); applyMode(); }} void setup() { Serial.begin(115200); pinMode(PIN_BTN, INPUT); pinMode(PIN_LAMP, OUTPUT); pinMode(PIN_LANTERN, OUTPUT); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) { delay(300); } client.setServer(MQTT_SERVER, 1883); client.connect("esp32-bedroom"); client.subscribe(MODE_TOPIC); // retain принесёт режим сразу Serial.println("slushayu home/mode, zhdu retain");} void loop() { if (!client.connected()) { if (client.connect("esp32-bedroom")) { client.subscribe(MODE_TOPIC); } delay(2000); return; } client.loop(); // ручной обход: кнопка сильнее режима, но не навсегда int btn = digitalRead(PIN_BTN); if (btn == HIGH && btnWas == LOW) { overrideOn = 1; overrideEnd = millis() + OVERRIDE_MS; Serial.println("[obhod] nochnik yarko na 3 minuty"); applyMode(); } btnWas = btn; // тайм-аут обхода: возврат к режиму без участия брокера if (overrideOn == 1 && millis() >= overrideEnd) { overrideOn = 0; Serial.println("[obhod] istek, vozvrat k rezhimu"); applyMode(); }}Обход: кнопка сильнее расписания, но не навсегда
Полночь, ребёнок проснулся — ночник нужен ярко, но не до утра. Кнопка ставит обход: overrideEnd = millis() + 180000 — и до истечения ночник горит на полную, а время выйдет — плата вернётся к режиму сама, без брокера и без повторного сообщения. Тот же millis-таймер, что и в любом другом месте дома; наехавший на delay скетч с обходом не справится, механика уже разбиралась в уроке про millis вместо delay.
Обход живёт на плате и не публикуется в home/mode — это главное правило. Опубликованная кнопкой погода стала бы погодой для всего дома: одна просьба приглушить свет в спальне вырубила бы фонарь у калитки и усыпила насос. Хотите показать обход панелям — публикуйте в свой топик home/bedroom/override с остатком минут; групповая механика и её эхо-ловушки уже разобраны в уроке про групповые команды — там же приоритет локальной команды над групповой, родственный, но другой случай: там команды, здесь отклонение от среды с обязательным возвратом.
Конфликт расписания и обхода решается в одну строку: смена режима сбрасывает обход — новый режим важнее старого просьбы. Иначе рассвет застанет спальню с ночным обходом и ночник догорит свои три минуты среди дня. После ребута millis() начинается с нуля и обход сгорает честно: ночник встанет в режим без яркой вспышки. Обход короткий, минуты, терять его не жалко; а вот режим и настройки — уже про EEPROM, это другая история с другими ценами.
Порог обхода подбирается по случаю, а не по вкусу. Три минуты — для ночника в спальне: глаз успевает, а тайм-аут не будит батарею кнопочного узла зря. Пять-десять минут — для света на лестнице с датчиком движения. Ловушка одна: слишком длинный обход начинает жить своей жизнью — я однажды поставил полчаса и через неделю ловил дом «в разгар обхода» утром, потому что уже никто не помнил, кто и зачем нажимал кнопку. Короткий обход строже к нажатию, зато не оставляет последствий.
| Источник расписания | Где живёт время | Правка в одном месте | Когда выбирать |
|---|---|---|---|
| cron + mosquitto_pub -r | сервер, системная таймзона | да: одна строка crontab | сервер уже есть, платы без часов |
| Node-RED / HA automation | сервер, визуальное расписание | да | дом уже живёт в HA |
| RTC на плате | батарейка DS3231 на каждой плате | нет: N плат — N правок | автономный узел без брокера |
Считать выгоду удобно по публикациям: режим меняется дважды в сутки — две публикации против восьми таймеров, восьми батарейных часов и восьми правок на летнее время. Третий режим away добавляется одним словом в словарь планировщика: платы, у которых away в трактовке не заведён, честно его игнорируют по словарю и продолжают жить по ночи и дню — дом расширяется без правки существующих прошивок.
Журнал ночи стоит собирать и на платах, и у брокера: смена режима от планировщика, приход retain при каждом подключении, обход с причиной — кнопка или команда. Через неделю записей видно, что ночь у дома не гладкая: плата на балконе переподключается дважды за ночь — Wi-Fi проседает, а насос будится в 00:07, потому что планировщик стоит на 00:05, и зазор между публикацией и подпиской тянется минуты. Обе находки чинятся на сервере, одной строкой, а не в восьми прошивках — лучшая иллюстрация того, зачем режиму жить у брокера.
И мост назад, к командам с аргументом: timer 20 из прошлого урока и ночной режим встречаются в одной кнопке. «Гирлянда на двадцать минут» во время ночи — это обход по команде с тем же тайм-аутом: брокер сказал night, вы сказали timer 20, плата горит ярко и сама возвращается к погоде. Домашнее задание ровно в этом: соединить два скетча так, чтобы обход работал и от кнопки, и от команды, а журнал показывал обе причины.
- При старте найдите в мониторе строку про retained: плата узнала ночь до всякого события — так же работает и настоящая подписка с retain.
- Нажмите кнопку: обход на 10 секунд, затем самостоятельный возврат к уровню 30; найдите ветку тайм-аута и проверьте, что брокер в ней не участвовал.
- Во время обхода щёлкните тумблер: обход сброшен, новый режим применился сразу — найдите строку сброса и обоснуйте её порядок перед applyMode.
- Поменяйте 30 на 10 и 255 на 120: две правки трактовки в одной функции — брокер и словарь режимов не тронуты, и это главный признак правильной архитектуры.
- Режим — состояние среды: одно retained-сообщение home/mode = night вместо восьми таймеров на платах
- Расписание живёт в одном месте — cron, Node-RED или HA; таймзона правится однажды, платы про время не знают
- Retain приносит режим при каждом подключении: ребут и офлайн-пауза не ломают картину
- Трактовка режима локальна: ночник 30, фонарь горит, опрос реже — брокер не командует пинами
- Обход с тайм-аутом на millis; смена режима сбрасывает обход; обход не публикуется в home/mode
- 1. Зачем флаг retain на топике режима?
- 2. Сколько мест правки при переходе на летнее время в схеме с cron-планировщиком?
- 3. Кнопка у кровати просит яркий свет в ночь. Что публикует плата?
- 4. Пришёл day от брокера, а обход ещё активен. Как ведёт себя скетч урока?
- 5. Что случится с активным обходом после перезагрузки платы?
Частые вопросы по теме урока
- Зачем флаг retain на топике режима?
- Retained-сообщение приходит при подписке: плата, подключившаяся в три ночи, сразу знает про night. Без retain режим пришлось бы ждать до следующей смены. Чтобы свежеподключившаяся плата сразу получила текущий режим
- Сколько мест правки при переходе на летнее время в схеме с cron-планировщиком?
- Время живёт в одном месте — на сервере. Плата про полночь не знает ничего: она читает слово night и трактует его своими значениями. Ноль: одна системная таймзона сервера
- Кнопка у кровати просит яркий свет в ночь. Что публикует плата?
- Обход — локальное отклонение от режима с возвратом по таймеру. Опубликованная кнопкой погода стала бы погодой для всего дома: фонарь и насос об этом не просили. Ничего в home/mode: обход локален, с тайм-аутом на millis
- Пришёл day от брокера, а обход ещё активен. Как ведёт себя скетч урока?
- Смена режима обнуляет обход до применения: иначе спальня догорала бы обход среди дня. Порядок в callback ровно такой: сброс, запись режима, applyMode. Обход сбросится: новый режим важнее старой просьбы
- Что случится с активным обходом после перезагрузки платы?
- Обход живёт в millis-таймере, который после ребута пуст. Короткий обход терять не жалко; долгие настройки — тема EEPROM, и цены там другие. millis() начнётся с нуля, обход сгорит: ночник встанет в режим