Практика: Реализация 'Проветривания'
Введение: Декомпозиция сценария 'Проветривание'
Данный урок посвящен практической реализации одного из ключевых сценариев энергосбережения — «Проветривание» (идентификатор SCN-CLIMATE-008). Его главная задача — предотвратить бессмысленную трату тепловой или холодильной энергии, когда пользователь открывает окно для проветривания помещения. Автоматическое отключение отопления или кондиционирования на этот период позволяет добиться существенной экономии и является неотъемлемой частью современного умного дома.
> ℹ️ Информация: Данный сценарий является одним из базовых и наиболее эффективных с точки зрения быстрой окупаемости. Его реализация — обязательный стандарт для современных инсталляций, повышающий как комфорт, так и экономическую эффективность системы автоматизации.
Декомпозиция задачи на составные части позволяет нам спроектировать надежное и масштабируемое решение. Для реализации сценария нам потребуются следующие компоненты:* Радиатор отопления с термоголовкой, управляемой через релейный выход контроллера.
* Электромагнитный клапан на коллекторе теплого пола.
* Фанкойл, управляемый по протоколу Modbus.
Ключевой аспект успешной реализации — изоляция логики. Сценарий «Проветривание» должен быть применен к конкретной климатической зоне (комнате). Открытие окна в гостиной не должно влиять на работу отопления в спальне. Это требует разработки потока Node-RED, который легко масштабируется и дублируется для каждой новой зоны с минимальными изменениями в конфигурации (преимущественно, в MQTT-топиках).
В рамках этого урока мы шаг за шагом создадим поток, который будет:
- Получать и обрабатывать данные с датчиков.
- Сохранять и восстанавливать состояние климатических приборов.
- Учитывать приоритеты других сценариев автоматизации.
---
Шаг 1: Получение и нормализация данных с датчиков открытия
Первым шагом в построении любого сценария является получение достоверных и унифицированных данных от триггерных устройств. В нашем случае — это датчики открытия окон. Они могут быть от разных производителей и передавать информацию в различных форматах.
Подписка на MQTT-топики
Мы используем узел `mqtt in` для подписки на топик, в который датчик публикует свое состояние. Согласно стандартам нашей академии, топики имеют следующую структуру:
`hi/devices/
Например, для датчика на окне в гостиной топик будет: `hi/devices/sensor_window_livingroom/state`.
Проблема разнородности данных и их нормализация
На практике сообщения, приходящие в `msg.payload`, могут сильно отличаться. Это одна из самых частых проблем при интеграции, которую необходимо решить на самом первом этапе.
| Пример `msg.payload` | Значение | Интерпретация |
| :------------------- | :------------------- | :-------------- |
| `true` | Булево | Окно открыто |
| `"true"` | Строка | Окно открыто |
| `1` | Число | Окно открыто |
| `"1"` | Строка | Окно открыто |
| `"OPEN"` | Строка | Окно открыто |
| `false` | Булево | Окно закрыто |
| `0` | Число | Окно закрыто |
| `"CLOSED"` | Строка | Окно закрыто |
Чтобы остальная часть логики была простой и надежной, мы должны привести все эти варианты к единому формату. Целевой формат — булево значение, где `true` означает "окно открыто", а `false` — "окно закрыто".
Для этого можно использовать узел `change` для простых замен (например, `"OPEN"` -> `true`), но более гибким и универсальным решением является узел `function`.
Реализация нормализации в узле `function`
Создадим узел `function` с названием "Нормализация состояния окна" и поместим в него следующий код:
// Получаем сырое значение msg.payload
let rawState = msg.payload;
// Переменная для нормализованного состояния
let isOpened = false;
// Проверяем все возможные варианты для "открытого" состояния
if (
rawState === true ||
rawState === 'true' ||
rawState === 1 ||
rawState === '1' ||
String(rawState).toUpperCase() === 'OPEN'
) {
isOpened = true;
}
// Приводим msg.payload к нашему стандартному булеву формату
msg.payload = isOpened;
// Для отладки добавим визуальный статус на узел
if (isOpened) {
node.status({ fill: "blue", shape: "dot", text: "Окно открыто" });
} else {
node.status({ fill: "grey", shape: "ring", text: "Окно закрыто" });
}
return msg;
После этого узла `function` мы можем быть уверены, что `msg.payload` всегда будет либо `true`, либо `false`, что значительно упрощает дальнейшую логику в узле `switch`.
Пример трансформации объекта `msg`:
До нормализации:{
"_msgid": "a1b2c3d4.5e4f32",
"topic": "hi/devices/sensor_window_livingroom/state",
"payload": "OPEN",
"qos": 1,
"retain": false
}
После нормализации:
{
"_msgid": "a1b2c3d4.5e4f32",
"topic": "hi/devices/sensor_window_livingroom/state",
"payload": true,
"qos": 1,
"retain": false
}
Теперь наш поток готов к реализации основной бизнес-логики.
---
Шаг 2: Управление состоянием отопления и сохранение контекста
Когда мы получаем сигнал об открытом окне (`msg.payload = true`), мы должны отключить климатическую установку. Однако просто отправить команду "ВЫКЛ" недостаточно. Что произойдет, когда окно закроется? Система должна "вспомнить", в каком состоянии находился радиатор до проветривания, и восстановить его. Этот механизм называется логикой с сохранением состояния (Stateful Logic).
Для изоляции логики комнаты мы используем `flow` контекст. Принципы выбора между `flow` и `global` контекстами были подробно рассмотрены в уроке [ID канонического урока, например, COURSE-07-M01-L04].
Для хранения состояния мы будем использовать контекст потока (`flow context`).
Логика при открытии окна
Вот как это выглядит в узле `function` с названием "Обработка: Окно открыто":
// Предполагаем, что сценарий термостата SCN-CLIMATE-001
// уже хранит свое состояние в контексте потока
let thermostatState = flow.get('livingroom_thermostat_state') || { active: false, setpoint: 21.0 };
// 1. Сохраняем "снимок" состояния ПЕРЕД отключением.
// Мы создаем отдельную переменную для этого, чтобы не смешивать
// с основным состоянием термостата.
flow.set('livingroom_state_before_airing', thermostatState);
// 2. Устанавливаем флаг-блокировку. Это запретит сценарию термостата
// включать радиатор, пока идет проветривание.
flow.set('livingroom_airing_lock', true);
// 3. Формируем команду на отключение радиатора
msg.topic = "hi/devices/radiator_livingroom/set";
msg.payload = {
"state": "OFF"
};
// Визуализируем статус
node.status({ fill: "red", shape: "dot", text: "Отопление ОТКЛЮЧЕНО (проветривание)" });
// Отправляем команду на исполнительное устройство
return msg;
Структура данных в контексте:
После выполнения этого кода в `flow.context` появятся две переменные:
- `livingroom_state_before_airing`: `{ "active": true, "setpoint": 22.5 }` (пример)
- `livingroom_airing_lock`: `true`
{
"topic": "hi/devices/radiator_livingroom/set",
"payload": {
"state": "OFF"
}
}
Таким образом, мы не только безопасно отключили отопление, но и подготовили систему к его корректному восстановлению, а также защитились от конфликтов с другими сценариями.
---
Шаг 3: Логика восстановления и обработка конфликтов приоритетов
Самая сложная часть сценария — это корректное восстановление системы после закрытия окна. Просто вернуть все, как было, — неверный подход. За время проветривания могли измениться другие, более важные условия.
> 🔗 Связанный материал: Принципы построения иерархии сценариев и управления приоритетами детально рассмотрены в Модуле 2 (COURSE-07-M02). Рекомендуется повторить урок LESSON-07-M02-L01, посвященный этой теме, перед реализацией данной логики.
Иерархия приоритетов в нашем случае выглядит так:`Глобальный режим ('Away', 'Night')` > `Сценарий 'Проветривание' (SCN-CLIMATE-008)` > `Сценарий 'Термостат' (SCN-CLIMATE-001)`
Это означает, что «Проветривание» может "перебить" работу термостата, но само оно будет проигнорировано, если активен более приоритетный глобальный режим экономии.
Логика при закрытии окна
Когда мы получаем сигнал о закрытом окне (`msg.payload = false`), наш узел `function` "Обработка: Окно закрыто" должен выполнить следующие действия:
// 1. Снимаем флаг-блокировку немедленно, чтобы термостат мог
// принять управление после нас.
flow.set('livingroom_airing_lock', false);
// 2. Получаем сохраненное состояние из контекста
const stateBeforeAiring = flow.get('livingroom_state_before_airing');
// Если записи нет, значит, восстанавливать нечего. Выходим.
if (!stateBeforeAiring) {
node.status({ fill: "green", shape: "ring", text: "Восстановление не требуется" });
return null; // Прерываем поток
}
// 3. Проверка на конфликты с более высоким приоритетом
const houseMode = global.get('house_mode'); // Например, 'Home', 'Away', 'Night'
if (houseMode === 'Away' || houseMode === 'Summer') {
// Режим высшего приоритета запрещает отопление.
// Мы снимаем блокировку, но не включаем радиатор.
// Удаляем временные данные.
flow.set('livingroom_state_before_airing', null);
node.status({ fill: "yellow", shape: "dot", text: `Блокировка снята, но режим ${houseMode} активен` });
return null; // Прерываем поток
}
// 4. Если конфликтов нет, восстанавливаем состояние
// Мы не отправляем команду напрямую, а восстанавливаем состояние
// самого термостата, который затем сам примет решение.
// Это более правильная архитектура.
flow.set('livingroom_thermostat_state', stateBeforeAiring);
// 5. Очищаем временные данные
flow.set('livingroom_state_before_airing', null);
node.status({ fill: "green", shape: "dot", text: "Состояние термостата восстановлено" });
// Мы не возвращаем msg, т.к. наша задача - изменить состояние
// другого сценария (термостата), а не отправлять команду напрямую.
// Термостат SCN-CLIMATE-001 должен среагировать на изменение
// своего состояния в контексте и сам включить радиатор.
return null;
Эта логика обеспечивает предсказуемое и иерархически правильное поведение системы автоматизации, предотвращая конфликты и обеспечивая комфорт для пользователя.
---
Итоговая сборка потока Node-RED и тестирование
Теперь соберем все компоненты в единый, работающий поток Node-RED.
> 💡 Подсказка: Для отладки сложных взаимодействий, как в этом сценарии, установите для узла `debug` режим вывода "complete message object" и направляйте его на "Debug window and console". Это позволит инспектировать не только `msg.payload`, но и все свойства объекта, включая `_msgid` для трассировки пути сообщения через разные узлы.
Схема потока (ASCII)
+---------------------------+
+->| Function: Окно открыто |--+
| +---------------------------+ |
[mqtt in] [function] | v [mqtt out]
hi/.../state -> Нормализация ->[switch] (команда -> hi/.../set
| payload? "OFF")
| ^
+->| Function: Окно закрыто |--+
+---------------------------+ (поток прерывается)
JSON-код для импорта в Node-RED
Вы можете импортировать этот поток, скопировав следующий код и вставив его через меню "Import" в Node-RED. Не забудьте адаптировать MQTT-топики под ваш проект.
> ⚠️ Внимание: после импорта необходимо вручную выбрать ваш MQTT-брокер в настройках узлов `mqtt in` и `mqtt out`. Плейсхолдер `YOUR_MQTT_BROKER_ID` используется для примера.
[
{
"id": "c1f7b8a2.1e9a48",
"type": "tab",
"label": "SCN-CLIMATE-008: Проветривание (Гостиная)",
"disabled": false,
"info": "Реализация сценария отключения отопления при открытии окна для гостиной."
},
{
"id": "a1b2c3d4.5e4f32",
"type": "mqtt in",
"z": "c1f7b8a2.1e9a48",
"name": "Датчик окна (гостиная)",
"topic": "hi/devices/sensor_window_livingroom/state",
"qos": "1",
"datatype": "auto",
"broker": "YOUR_MQTT_BROKER_ID",
"x": 150,
"y": 100,
"wires": [
[
"d5e6f7g8.9h1i2j"
]
]
},
{
"id": "d5e6f7g8.9h1i2j",
"type": "function",
"z": "c1f7b8a2.1e9a48",
"name": "Нормализация состояния окна",
"func": "let rawState = msg.payload;\nlet isOpened = false;\n\nif (\n rawState === true ||\n rawState === 'true' ||\n rawState === 1 ||\n rawState === '1' ||\n String(rawState).toUpperCase() === 'OPEN'\n) {\n isOpened = true;\n}\n\nmsg.payload = isOpened;\n\nif (isOpened) {\n node.status({ fill: \"blue\", shape: \"dot\", text: \"Окно открыто\" });\n} else {\n node.status({ fill: \"grey\", shape: \"ring\", text: \"Окно закрыто\" });\n}\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 380,
"y": 100,
"wires": [
[
"k3l4m5n6.o7p8q9"
]
]
},
{
"id": "k3l4m5n6.o7p8q9",
"type": "switch",
"z": "c1f7b8a2.1e9a48",
"name": "Окно открыто/закрыто?",
"property": "payload",
"propertyType": "msg",
"rules": [
{
"t": "true"
},
{
"t": "false"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 600,
"y": 100,
"wires": [
[
"r9s0t1u2.v3w4x5"
],
[
"y6z7a8b9.c0d1e2"
]
]
},
{
"id": "r9s0t1u2.v3w4x5",
"type": "function",
"z": "c1f7b8a2.1e9a48",
"name": "Обработка: Окно открыто",
"func": "let thermostatState = flow.get('livingroom_thermostat_state') || { active: false, setpoint: 21.0 };\n\nflow.set('livingroom_state_before_airing', thermostatState);\nflow.set('livingroom_airing_lock', true);\n\nmsg.topic = \"hi/devices/radiator_livingroom/set\";\nmsg.payload = {\n \"state\": \"OFF\",\n \"source\": \"SCN-CLIMATE-008\"\n};\n\nnode.status({ fill: \"red\", shape: \"dot\", text: \"Отопление ОТКЛЮЧЕНО (проветривание)\" });\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 840,
"y": 80,
"wires": [
[
"f3g4h5i6.j7k8l9"
]
]
},
{
"id": "y6z7a8b9.c0d1e2",
"type": "function",
"z": "c1f7b8a2.1e9a48",
"name": "Обработка: Окно закрыто",
"func": "flow.set('livingroom_airing_lock', false);\nconst stateBeforeAiring = flow.get('livingroom_state_before_airing');\n\nif (!stateBeforeAiring) {\n node.status({ fill: \"green\", shape: \"ring\", text: \"Восстановление не требуется\" });\n return null;\n}\n\nconst houseMode = global.get('house_mode') || 'Home';\n\nif (houseMode === 'Away' || houseMode === 'Summer') {\n flow.set('livingroom_state_before_airing', null);\n node.status({ fill: \"yellow\", shape: \"dot\", text: `Блокировка снята, но режим ${houseMode} активен` });\n return null;\n}\n\nflow.set('livingroom_thermostat_state', stateBeforeAiring);\nflow.set('livingroom_state_before_airing', null);\n\nnode.status({ fill: \"green\", shape: \"dot\", text: \"Состояние термостата восстановлено\" });\n\nreturn null;",
"outputs": 1,
"noerr": 0,
"x": 840,
"y": 140,
"wires": [
[]
]
},
{
"id": "f3g4h5i6.j7k8l9",
"type": "mqtt out",
"z": "c1f7b8a2.1e9a48",
"name": "Управление радиатором",
"topic": "",
"qos": "1",
"retain": "false",
"broker": "YOUR_MQTT_BROKER_ID",
"x": 1080,
"y": 80,
"wires": []
}
]
Методология тестирования
* С помощью узла `inject` отправьте в узел "Нормализация" сообщение `msg.payload = "OPEN"`.
* Ожидаемый результат:
* На выходе узла `mqtt out` должно появиться сообщение `{"state":"OFF"}` в топике `hi/devices/radiator_livingroom/set`.
* В `flow.context` (`Context Data` на боковой панели) должны появиться переменные `livingroom_airing_lock: true` и `livingroom_state_before_airing` со значением, которое было до теста.
* С помощью узла `inject` отправьте сообщение `msg.payload = "CLOSED"`.
* Ожидаемый результат:
* В `flow.context` переменная`livingroom_airing_lock` должна стать `false`, а `livingroom_state_before_airing` должна быть удалена (стать `null`).
* Переменная `livingroom_thermostat_state` должна вернуться к своему исходному значению. Узел `mqtt out` не должен ничего отправлять, так как за это теперь отвечает сценарий термостата.
* Перед тестом установите глобальную переменную: `global.set('house_mode', 'Away')`.
* Отправьте сообщение `msg.payload = "CLOSED"`.
* Ожидаемый результат:
* В `flow.context` `livingroom_airing_lock` станет `false`, `livingroom_state_before_airing` будет удалена.
* `livingroom_thermostat_state` НЕ должна восстановиться. Система останется в режиме экономии.
* На узле "Обработка: Окно закрыто" появится статус "Блокировка снята, но режим Away активен".
Использование внешнего клиента, такого как MQTT Explorer, позволит вам в реальном времени наблюдать за сообщениями во всех `hi/devices/...` топиках и убедиться, что система ведет себя предсказуемо.
Что дальше
В этом уроке мы создали надежный и интеллектуальный сценарий "Проветривание". Мы научились обрабатывать и нормализовать данные, управлять состоянием с помощью контекста и, что самое важное, разрешать конфликты приоритетов между различными сценариями. В следующем уроке мы рассмотрим еще один важный аспект климат-контроля — интеграцию с прогнозом погоды для предиктивного управления отоплением и системами полива.