ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → Практика: Реализация 'Проветривания'

Практика: Реализация 'Проветривания'

Урок 6 · Сценарии умного дома: режимы, состояния, приоритеты · 30 мин · theory

Введение: Декомпозиция сценария 'Проветривание'

Данный урок посвящен практической реализации одного из ключевых сценариев энергосбережения — «Проветривание» (идентификатор SCN-CLIMATE-008). Его главная задача — предотвратить бессмысленную трату тепловой или холодильной энергии, когда пользователь открывает окно для проветривания помещения. Автоматическое отключение отопления или кондиционирования на этот период позволяет добиться существенной экономии и является неотъемлемой частью современного умного дома.

> ℹ️ Информация: Данный сценарий является одним из базовых и наиболее эффективных с точки зрения быстрой окупаемости. Его реализация — обязательный стандарт для современных инсталляций, повышающий как комфорт, так и экономическую эффективность системы автоматизации.

Декомпозиция задачи на составные части позволяет нам спроектировать надежное и масштабируемое решение. Для реализации сценария нам потребуются следующие компоненты:
  • Триггер (Источник события): Датчик открытия на окне или балконной двери. В контексте платформы HI это, как правило, магнитоконтактный датчик (геркон), подключенный к универсальному входу контроллера в режиме "сухого контакта". Сигнал от датчика по протоколу MQTT сообщает системе об изменении состояния окна.
  • Объект управления (Исполнительное устройство): Климатическая установка, отвечающая за поддержание температуры в данной зоне. Это может быть:
  • * Радиатор отопления с термоголовкой, управляемой через релейный выход контроллера.

    * Электромагнитный клапан на коллекторе теплого пола.

    * Фанкойл, управляемый по протоколу Modbus.

  • Логический контроллер: Ядро нашей системы, контроллер HI с запущенным Node-RED, который связывает триггер и объект управления, реализуя заданную логику.
  • Ключевой аспект успешной реализации — изоляция логики. Сценарий «Проветривание» должен быть применен к конкретной климатической зоне (комнате). Открытие окна в гостиной не должно влиять на работу отопления в спальне. Это требует разработки потока Node-RED, который легко масштабируется и дублируется для каждой новой зоны с минимальными изменениями в конфигурации (преимущественно, в MQTT-топиках).

    В рамках этого урока мы шаг за шагом создадим поток, который будет:

    ---

    Шаг 1: Получение и нормализация данных с датчиков открытия

    Первым шагом в построении любого сценария является получение достоверных и унифицированных данных от триггерных устройств. В нашем случае — это датчики открытия окон. Они могут быть от разных производителей и передавать информацию в различных форматах.

    Подписка на MQTT-топики

    Мы используем узел `mqtt in` для подписки на топик, в который датчик публикует свое состояние. Согласно стандартам нашей академии, топики имеют следующую структуру:

    `hi/devices/__/state`

    Например, для датчика на окне в гостиной топик будет: `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`).

    Логика при открытии окна

  • Прочитать текущее состояние термостата. Прежде чем что-либо менять, мы должны узнать, работал ли вообще радиатор и на какой температуре он был настроен. Эти данные обычно доступны в соответствующем MQTT-топике состояния термостата, например, `hi/devices/thermostat_livingroom/state`. В реальной системе нужно было бы сделать запрос и дождаться ответа, но для упрощения урока мы предположим, что это состояние уже хранится в контексте благодаря работе сценария термостата (`SCN-CLIMATE-001`).
  • Сохранить состояние в `flow.context`. Мы создадим специальный объект, который будет хранить "снимок" состояния климатической системы перед ее отключением.
  • Отправить команду на отключение. После сохранения состояния мы формируем и отправляем MQTT-сообщение для отключения радиатора.
  • Вот как это выглядит в узле `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` появятся две переменные:

    Структура исходящего сообщения для управления:
    {
    

    "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` "Обработка: Окно закрыто" должен выполнить следующие действия:

  • Снять флаг-блокировку. Первым делом мы снимаем блокировку, чтобы разрешить термостату снова управлять радиатором.
  • Проверить наличие сохраненного состояния. Если в контексте нет записи `livingroom_state_before_airing`, значит, отопление и так было выключено, и делать ничего не нужно.
  • Проверить глобальные режимы. Перед восстановлением состояния мы обязаны проверить, не активен ли сейчас режим, который запрещает отопление (например, `Away` или `Summer`).
  • Восстановить состояние. Если все проверки пройдены, мы считываем сохраненный "снимок" и отправляем команду на восстановление работы климатической системы.
  • Очистить контекст. После успешного восстановления временные данные нужно удалить, чтобы не вызывать ложных срабатываний в будущем.
  • // 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": []

    }

    ]

    Методология тестирования

  • Начальные условия: Убедитесь, что сценарий термостата (`SCN-CLIMATE-001`) работает и в `flow.context` есть переменная `livingroom_thermostat_state`, например, `{ "active": true, "setpoint": 22.0 }`.
  • Тест 1: Открытие окна.
  • * С помощью узла `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` со значением, которое было до теста.

  • Тест 2: Закрытие окна (штатный режим).
  • * С помощью узла `inject` отправьте сообщение `msg.payload = "CLOSED"`.

    * Ожидаемый результат:

    * В `flow.context` переменная`livingroom_airing_lock` должна стать `false`, а `livingroom_state_before_airing` должна быть удалена (стать `null`).

    * Переменная `livingroom_thermostat_state` должна вернуться к своему исходному значению. Узел `mqtt out` не должен ничего отправлять, так как за это теперь отвечает сценарий термостата.

  • Тест 3: Закрытие окна (конфликт приоритетов).
  • * Перед тестом установите глобальную переменную: `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/...` топиках и убедиться, что система ведет себя предсказуемо.

    Что дальше

    В этом уроке мы создали надежный и интеллектуальный сценарий "Проветривание". Мы научились обрабатывать и нормализовать данные, управлять состоянием с помощью контекста и, что самое важное, разрешать конфликты приоритетов между различными сценариями. В следующем уроке мы рассмотрим еще один важный аспект климат-контроля — интеграцию с прогнозом погоды для предиктивного управления отоплением и системами полива.