ГлавнаяАкадемияОсновы умного дома → Понятие состояния (State)

Понятие состояния (State)

Урок 2 · Основы умного дома · 30 мин · theory

От События к Состоянию: Фундаментальная разница

В предыдущих уроках мы установили, что автоматизация строится на обработке событий. Событие — это мгновенное, атомарное происшествие в системе: кнопка была нажата, датчик движения зафиксировал активность, поступила команда по сети. Однако для создания по-настоящему интеллектуальных и предсказуемых сценариев одного лишь реагирования на события недостаточно. Нам необходимо ввести второе фундаментальное понятие — состояние (State).

> 🔗 Связанный материал: Для более глубокого понимания концепции событий и обязательного к применению «контракта сообщения», настоятельно рекомендуем повторить материал урока COURSE-01-M02-L02 «От сигнала к событию: контракт сообщения».

Состояние — это срез данных системы, описывающий ее свойства в конкретный момент времени. В отличие от события, которое является действием, состояние — это характеристика, которая сохраняется на протяжении некоторого периода.

Проведем простую аналогию из грамматики:

Событие — это глагол. Оно отвечает на вопрос «что произошло?». Нажали кнопку, сработал датчик, повысилась* температура. Состояние — это прилагательное. Оно отвечает на вопрос «какой? что есть?». Свет включен, дверь закрыта, температура высокая*.

Рассмотрим на практическом примере управления освещением:

Разделение этих понятий критически важно. Система, оперирующая только событиями, "слепа". Она может послать команду "включить свет", но не знает, был ли он уже включен. Система, отслеживающая состояния, обладает "памятью". Она знает текущее положение дел и может принимать более сложные решения.

| Характеристика | Событие (Event) | Состояние (State) |

| :------------- | :----------------------------------------------------- | :------------------------------------------------------------- |

| Природа | Мгновенное действие, точка на временной шкале | Длительное свойство, отрезок на временной шкале |

| Аналогия | Глагол ("включить", "закрыть", "обнаружить") | Прилагательное ("включенный", "закрытая", "обнаружено") |

| Пример | Нажатие кнопки выключателя | Свет горит или не горит |

| Информация | "Что-то произошло в 15:30:01" | "С 15:30:01 и до сих пор система находится в этом положении" |

| Влияние | Инициирует переход из одного состояния в другое | Определяет, как система должна реагировать на следующее событие |

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

---

Источник Истины (Source of Truth): Где и как хранить состояния?

Если состояние так важно, возникает логичный вопрос: где его хранить? Место, где система хранит авторитетную, единственно верную информацию о своем состоянии, называется Источник Истины (Source of Truth, SoT). Наличие единого и чётко определённого SoT — это архитектурное требование для любой надёжной системы автоматизации. Без него разные подсистемы (например, логика в Node-RED, мобильное приложение, настенная панель) могут иметь разное представление о состоянии одного и того же устройства, что приводит к хаосу.

> 💡 Подсказка: В большинстве проектов на базе нашего контроллера, использующего MQTT, именно MQTT-топики являются естественным и рекомендуемым Источником Истины. Состояния устройств (включено/выключено, значение датчика) уже публикуются в них с флагом `retain`, что делает их идеальным SoT.

Давайте рассмотрим и сравним основные подходы к хранению состояний:

1. Хранение на конечном устройстве

Состояние хранится непосредственно в памяти самого устройства (реле, датчика, диммера). Контроллер для получения состояния должен явно опросить устройство по шине (например, Modbus).

* Создает высокую нагрузку на шину из-за постоянных опросов.

* Контроллер получает состояние с задержкой.

* Другие системы (мобильное приложение) не могут получить состояние напрямую, им нужен "посредник" в виде контроллера.

* При перезагрузке контроллера все сведения о состояниях теряются до следующего успешного опроса.

2. Хранение в переменных контроллера (контекст Node-RED)

Состояние хранится в оперативной памяти контроллера с использованием переменных контекста (`flow` или `global` в Node-RED).

* Данные теряются при перезагрузке контроллера или перезапуске Node-RED (если не настроен персистентный контекст, что является более сложной темой).

* Состояние "заперто" внутри Node-RED. Внешние системы (например, голосовой ассистент) не могут его прочитать без создания специального API (например, через узел `http in`).

* Усложняет создание распределенных систем с несколькими контроллерами.

3. Хранение в MQTT-брокере (с флагом `retain`)

Это наиболее предпочтительный и мощный механизм для нашей платформы. Состояние публикуется в виде MQTT-сообщения в специальный топик с флагом `retain=true`.

📋 Ключевые понятия: Флаг `retain` в MQTT — это инструкция для брокера сохранить последнее сообщение, опубликованное в данный топик. Когда новый клиент подписывается на этот топик, брокер немедленно отправляет ему это сохраненное сообщение.

* Единый Источник Истины: Любой клиент в сети (Node-RED, мобильное приложение, другой контроллер) может подписаться на топик и мгновенно получить актуальное состояние.

* Отказоустойчивость: Если Node-RED перезагружается, после восстановления подписки на топики он сразу же "узнает" все состояния в системе и может продолжить работу корректно.

* Развязка (Decoupling): Логика, генерирующая состояние (например, драйвер датчика), полностью отделена от логики, использующей это состояние. Они общаются только через брокер.

* Требует внимательного проектирования иерархии MQTT-топиков.

Есть риск неправильного использования: публикация событий* (например, "нажатие кнопки") с флагом `retain` является грубой ошибкой.

Сравнение подходов:

| Подход | Скорость доступа | Устойчивость к перезагрузке | Доступность для других систем | Рекомендация для платформы HI |

| :------------------------------------- | :--------------: | :-------------------------: | :---------------------------: | :---------------------------: |

| На конечном устройстве | Низкая | Высокая (для устройства) | Низкая | Только для специфических задач |

| В контексте Node-RED | Очень высокая | Низкая (по умолчанию) | Средняя (требует API) | Для временных, некритичных данных |

| В MQTT-брокере (`retain=true`) | Высокая | Высокая | Высокая | Основной рекомендуемый метод |

---

Практика: Управление состоянием через MQTT Retain

Теория важна, но давайте посмотрим, как это работает на практике. Мы будем использовать стандартные утилиты командной строки `mosquitto_pub` и `mosquitto_sub`, которые доступны на контроллере HI "из коробки".

> ⚠️ Внимание: Никогда не используйте флаг `retain` для публикации событий, которые не отражают состояние (например, "кнопка нажата", "команда выполнена"). Это приводит к тому, что при перезагрузке или переподключении клиента система будет реагировать на "фантомные" события из прошлого, вызывая нежелательные действия. Retain — только для состояний!

Шаг 1: Публикация состояния

Откройте терминал контроллера и выполните команду. Эта команда публикует сообщение `"ON"` в топик `hi/living_room/light_main/state` и, благодаря флагу `-r`, говорит брокеру сохранить это значение.

mosquitto_pub -h localhost -t 'hi/living_room/light_main/state' -m 'ON' -r

Брокер получил это сообщение и сохранил его у себя. Теперь, даже если мы отправим еще 100 сообщений в другие топики, для топика `hi/living_room/light_main/state` последним сохраненным значением будет `"ON"`.

Шаг 2: Подписка и получение состояния

Теперь представим, что запускается новый сервис (или мы просто открываем новый терминал), которому нужно узнать текущее состояние главного света в гостиной. Он подписывается на тот же топик:

mosquitto_sub -h localhost -t 'hi/living_room/light_main/state'

Сразу после выполнения этой команды вы увидите на экране:

ON

Обратите внимание: мы не ждали, пока кто-то снова включит свет. Брокер немедленно отдал нам последнее сохраненное значение. В этом и заключается магия флага `retain`.

Шаг 3: Разделение топиков для команд и состояний

Хорошей практикой является разделение топиков для управления (`command` или `set`) и для получения статуса (`state`).

* Сюда клиенты (мобильное приложение, Node-RED) публикуют команды `ON` или `OFF`.

* Сообщения в этот топик никогда не публикуются с флагом `retain`.

* Логика, непосредственно управляющая реле, после успешного выполнения команды публикует сюда фактическое состояние: `ON` или `OFF`.

* Сообщения в этот топик всегда публикуются с флагом `retain=true`.

Шаг 4: Отображение состояния в Node-RED

Создадим простой поток для демонстрации.

  • Перетащите на поле узел `mqtt in`.
  • Настройте его:
  • * Server: Выберите ваш локальный MQTT-брокер.

    * Topic: `hi/living_room/light_main/state`

    * QoS: `2`

  • Подключите к выходу узла `mqtt in` узел `debug`.
  • Нажмите Deploy.
  • Сразу после развертывания вы увидите в панели отладки сообщение с `payload: "ON"`. Ваш поток Node-RED, как новый подписчик, мгновенно получил сохраненное состояние от брокера, не дожидаясь нового события. Теперь он знает, что свет включен.

    ---

    Логика на основе состояний (Stateful) vs. без состояний (Stateless)

    Теперь, когда у нас есть надежный способ хранить и получать состояния, мы можем строить более совершенную логику.

    Stateless (Без состояния) логика

    Stateless-логика реагирует только на входящее событие, не имея представления о прошлом или текущем контексте системы. Решение принимается исключительно на основе данных в текущем `msg`. Пример: Наивный сценарий "свет по движению"
    [Событие: Движение] -> [Команда: Включить свет] -> [Задержка 5 минут] -> [Команда: Выключить свет]
    
    Проблема: Если в течение 5-минутной задержки произойдет новое событие движения, запустится еще один параллельный таймер. В результате свет может выключиться, даже если в комнате кто-то есть. Этот сценарий "глухой" к своему собственному состоянию.

    Stateful (С состоянием) логика

    Stateful-логика принимает решение на основе комбинации входящего события и текущего состояния системы. Пример: Умный сценарий "свет по движению"

    Давайте реализуем его в Node-RED. Нам понадобится два потока:

  • Поток, хранящий состояние освещенности.
  • Поток, реализующий логику на основе этого состояния.
  • Поток 1: Источник Истины для света

    Этот поток имитирует обратную связь от реле. Он слушает команды и публикует состояние с флагом `retain`.

    [mqtt in: hi/living_room/light_main/set] -> [change: установить msg.retain=true] -> [mqtt out: hi/living_room/light_main/state]
    
    Поток 2: Stateful-логика управления

    Этот поток будет реагировать на движение, но с умом.

           [mqtt in: hi/sensors/motion_living_room]
    

    |

    (событие "движение") v

    |----------------------+

    |

    [mqtt in: hi/living_room/light_main/state] |

    (получаем текущее состояние света) |

    | |

    v v

    [function: "Stateful Logic"] ---------> [mqtt out: hi/living_room/light_main/set]

    В данном примере мы используем `function` узел, который будет хранить последнее известное состояние света.

    Код для узла `function: "Stateful Logic"`:
    // Получаем и сохраняем состояние света, когда оно приходит.
    

    // Сохраняем в контексте узла для быстрого доступа.

    if (msg.topic === 'hi/living_room/light_main/state') {

    context.set('lightState', msg.payload);

    // Это сообщение нам дальше не нужно, оно только обновило состояние

    return null;

    }

    // Если пришло событие движения

    if (msg.topic === 'hi/sensors/motion_living_room') {

    // Читаем текущее состояние света из контекста

    const lightState = context.get('lightState') || 'OFF';

    // Если свет уже включен, ничего не делаем.

    // Мы просто ждем, когда он выключится по другому таймеру,

    // или можем сбросить таймер (более сложная логика).

    if (lightState === 'ON') {

    node.status({fill:"green", shape:"dot", text:"Light is already ON"});

    return null; // Прерываем поток

    }

    // Если свет выключен, формируем команду на включение.

    msg.payload = 'ON';

    node.status({fill:"blue", shape:"dot", text:"Motion detected, turning ON"});

    return msg; // Отправляем команду 'ON' в топик /set

    }

    JSON-пример сообщения о движении:
    // msg, входящее в узел function от датчика движения
    

    {

    "topic": "hi/sensors/motion_living_room",

    "payload": {

    "motion": true,

    "illuminance": 30,

    "ts": 1678890000000

    }

    }

    Этот сценарий уже гораздо умнее: он не будет слать лишних команд, если свет уже включен. Его можно усложнить, добавив таймер выключения, который сбрасывается при каждом новом движении (для этого идеально подходит узел `trigger`), но фундаментальный принцип остается тем же: Проверь состояние -> Прими решение.

    ---

    Резюме и Ключевые Выводы

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

    📋 Ключевые выводы:

    Что дальше?

    Теперь, вооружившись знаниями о состояниях и событиях, мы готовы к созданию полноценных сценариев. В следующем уроке мы подробно разберем, как реализовать наиболее востребованные сценарии автоматизации для умного дома, активно используя stateful-подход на практике в среде Node-RED.