Newer
Older
smart-home-server / docs / events-from-devices.md

События от устройств (Events)

Этот документ описывает систему событий от устройств — основной механизм реакции автоматизации на физические действия (нажатия кнопок, срабатывание датчиков и т.д.).


1. Как устройство отправляет событие

Устройство отправляет событие на сервер через POST /events/new:

{
  "device_id": "ecf0a1b5c9d74f9a8e294c1f67b0a8b9",
  "event_name": "press",
  "data": { "channel": 0 }
}

Требования:

  • Заголовок Authorization: Bearer <device_token> (токен из таблицы device_auth)
  • device_id — это device_hard_id из БД (уникальный ID устройства)

2. Жизненный цикл события на сервере

[Устройство] 
    POST /events/new (device_id, event_name, data)
        ↓
[EventsController::new_event()]
    1. Находит устройство по device_hard_id
    2. Проверяет, что auth активен
    3. Логирует событие
    4. Обновляет last_contact и connection_status = "active"
    5. Отвечает 200 OK немедленно
    6. fastcgi_finish_request() — ответ отправлен, но скрипт продолжается
        ↓
[EventsModel] — триггерит 5 вариантов событий через Fury Events

Почему 5 вариантов?
Одно физическое событие (например, нажатие кнопки) триггерит 5 разных имён событий — это позволяет подписываться на разном уровне детализации.


3. Пять паттернов имён событий

Для устройства типа button с alias kitchen_btns, канал 2, событие press:

Паттерн Имя события Когда использовать
1 {event_name} press Глобально — любое устройство, любое нажатие
2 {device_type}.{event_name} button.press По типу — все кнопки системы
3 {device_type}@{alias}.{event_name} button@kitchen_btns.press По alias — конкретная кнопка (все каналы)
4 {device_type}({ch}).{event_name} button(2).press По каналу — все кнопки, канал 2
5 {device_type}@{alias}({ch}).{event_name} button@kitchen_btns(2).press Точно — конкретная кнопка, конкретный канал

4. Как подписаться на событие (Control Scripts)

В register_events_handlers() Scope-класса:

<?php

namespace ControlScripts\Scopes;

class KitchenScope extends \SHServ\Middleware\ControlScripts 
                     implements \SHServ\Implements\ControlScriptsInterface {

    public function register_events_handlers(): void {
        
        // 1. Глобально: любая кнопка системы
        $this->add_event_handler("press", function(Device $device, array $data) {
            logging()->info('KitchenScope', 'Any button pressed', [
                'device' => $device->alias,
                'channel' => $data['channel'] ?? null
            ]);
        });

        // 2. Все кнопки типа button
        $this->add_event_handler("button.press", function(Device $device, array $data) {
            logging()->info('KitchenScope', 'Button pressed', ['device' => $device->alias]);
        });

        // 3. Конкретная кнопка (все каналы)
        $this->add_event_handler("button@kitchen_btns.press", function(Device $device, array $data) {
            logging()->info('KitchenScope', 'Kitchen button pressed');
        });

        // 4. Все кнопки, канал 2
        $this->add_event_handler("button(2).press", function(Device $device, array $data) {
            logging()->info('KitchenScope', 'Channel 2 pressed on some button');
        });

        // 5. Точно: кухня, канал 2
        $this->add_event_handler("button@kitchen_btns(2).press", function(Device $device, array $data) {
            // Самая частая форма — реакция на конкретную физическую кнопку
            $relay = $this->devices()->by_alias("kitchen_relay");
            $relay->device_api()->toggle_channel(0);
        });

        // 6. Устройство онлайн (например, после перезагрузки)
        $this->add_event_handler("button@kitchen_btns.online", function(Device $device, array $data) {
            // Синхронизировать индикаторы после появления кнопки в сети
            $this->helper()->sync_btn_channels($this->sync_map(), $device->alias);
        });
    }
}

Параметры хендлера:

  • Device $device — объект устройства, отправившего событие (методы: id(), alias, device_type, device_api(), ...)
  • array $data — данные от устройства (обычно channel, иногда дополнительные поля)

5. Известные имена событий (event_name)

button

event_name Описание data
press Нажатие кнопки { channel: 0 }
online Устройство вышло в сеть {}

relay

event_name Описание data
limit_switch_activated Сработал концевик (опционально, если прошивка поддерживает) { channel: 0, state: "open" }

hatch

event_name Описание data
limit_switch_activated Сработал концевик закрытия { channel: 0 }
calibration_failed Не удалась калибровка { reason: "timeout" }

sensor

event_name Описание data
presence_changed Изменение присутствия в помещении { present: true }
online Датчик вышел в сеть {}

Примечание: Конкретные event_name зависят от прошивки устройства. Смотрите документацию конкретного типа устройства в docs/devices/.


6. Как EventsModel триггерит события

Файл: server/SHServ/Models/EventsModel.php

// 1. Если есть канал — триггерит alias+channel и type+channel
if(isset($data["channel"])) {
    $events_model->channel_alias_device_event_call($device, $event_name, $channel, $data);
    // → button@kitchen_btns(2).press
    $events_model->channel_device_event_call($device, $event_name, $channel, $data);
    // → button(2).press
}

// 2. Триггерит alias (все каналы)
$events_model->alias_device_event_call($device, $event_name, $data);
// → button@kitchen_btns.press

// 3. Триггерит по типу устройства
$events_model->global_device_event_call($device, $event_name, $data);
// → button.press

// 4. Триггерит глобально (любое устройство)
$events_model->global_any_device_event_call($device, $event_name, $data);
// → press

Порядок важен:
События триггерятся от наиболее специфичного к наиболее общему. Если вы подписаны на button@kitchen_btns(2).press — сработает только этот хендлер. Если на press — сработает на любое нажатие в системе.


7. Асинхронность и fastcgi_finish_request

EventsController отвечает устройству немедленно (строки 48–62):

$response = json_encode(['status' => 'ok']);
http_response_code(200);
header("Content-Type: application/json; charset=utf-8");
header("Content-Length: " . strlen($response));
header("Connection: close");
echo $response;

if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();  // ← ответ отправлен, устройство ушло
} else {
    ob_flush();
    flush();
}

// ← С этого момента обработчики работают асинхронно
$events_model->channel_alias_device_event_call(...);

Что это значит для Control Scripts:

  • Обработчики событий не блокируют ответ устройству
  • Обработчики могут занимать время (HTTP-запросы к другим устройствам, сложная логика)
  • Устройство уже получило 200 OK и считает событие доставленным
  • Если обработчик упадёт с ошибкой — устройство не узнает (ошибка логируется на сервере)

8. Синхронизация индикаторов кнопок (Sync Map)

Sync Map — декларативное описание связей «реле ↔ кнопки» для автоматической синхронизации индикаторов.

Регистрация связи

public function register_sync_map(): void {
    $this->add_sync_connection([
        // Первый элемент — источник состояния (реле)
        ["type" => "relay",  "alias" => "kitchen_relay", "channel" => 0],
        // Остальные — кнопки, чьи индикаторы синхронизируются
        ["type" => "button", "alias" => "kitchen_btns",  "channel" => 1],
        ["type" => "button", "alias" => "hall_btns",     "channel" => 0],
    ]);
}

Хелперы синхронизации

// После переключения реле — синхронизировать все кнопки из sync_map
$relay_api->toggle_channel(0);
$this->helper()->sync_relay_to_btns($this->sync_map(), "kitchen_relay");

// При появлении кнопки онлайн — синхронизировать её индикаторы с реле
$this->add_event_handler("button@kitchen_btns.online", function(Device $device, array $data) {
    $this->helper()->sync_btn_channels($this->sync_map(), $device->alias);
});

Методы DeviceScriptsHelper: | Метод | Описание | |-------|---------| | sync_relay_to_btn_channel($relay_api, $btn_api, $relay_ch, $btn_ch) | Синхронизировать один канал реле с одним каналом кнопки | | sync_relay_to_btns($sync_map, $relay_alias) | Синхронизировать все реле с кнопками по sync_map | | sync_btn_channels($sync_map, $btn_alias) | Синхронизировать индикаторы кнопки с реле по sync_map | | get_sync_entries_by_type($sync_map, $type) | Получить все записи указанного типа из sync_map | | prepare_sync_map_by_alias($sync_map, $alias) | Подготовить sync_map для указанного устройства |


9. Отладка событий

Логи сервера

События логируются в server/SHServ/Logs/:

{"level":"info","timestamp":"2026-06-08T14:23:45+03:00","scope":"php:Events","message":"Event received","device_id":12,"alias":"kitchen_btns","event_name":"press","channel":2}

Проверка подписок

# Убедиться, что Scope загружен
curl http://smart-home-serv.local/api/v1/scripts/scopes/list | jq '.data.scopes[] | select(.name=="KitchenScope")'

# Проверить, что хендлер зарегистрирован (в логе при старте сервера)
grep "handler.*app:button@kitchen_btns" server/SHServ/Logs/*.log

10. Полный пример Scope-класса

<?php

namespace ControlScripts\Scopes;

class LightHubScope extends \SHServ\Middleware\ControlScripts 
                      implements \SHServ\Implements\ControlScriptsInterface {

    public function register_sync_map(): void {
        $this->add_sync_connection([
            ["type" => "relay",  "alias" => "kitchen_relay", "channel" => 0],
            ["type" => "button", "alias" => "kitchen_btns",  "channel" => 1],
        ]);
    }

    public function register_events_handlers(): void {
        // Нажатие кнопки → переключить реле
        $this->add_event_handler("button@kitchen_btns(1).press", function(Device $device, array $data) {
            $relay = $this->devices()->by_alias("kitchen_relay");
            $relay_api = $relay->device_api();
            $relay_api->toggle_channel(0);
            
            // Синхронизировать индикаторы всех кнопок
            $this->helper()->sync_relay_to_btns($this->sync_map(), "kitchen_relay");
        });

        // Кнопка онлайн → синхронизировать индикатор
        $this->add_event_handler("button@kitchen_btns.online", function(Device $device, array $data) {
            $this->helper()->sync_btn_channels($this->sync_map(), $device->alias);
        });
    }

    public function register_actions_scripts(): void {
        $this->add_action_script([
            "alias" => "kitchen_light_toggle",
            "name"  => "Свет на кухне",
            "icon"  => '<i class="ph ph-lightbulb"></i>',
        ], function($params) {
            $relay = $this->devices()->by_alias("kitchen_relay");
            $relay->device_api()->toggle_channel(0);
            $this->helper()->sync_relay_to_btns($this->sync_map(), "kitchen_relay");
            return ["result" => true];
        });
    }

    public function register_regular_scripts(): void {
        // Не используется
    }
}

См. также

  • docs/control-scripts-guide.md — полное руководство по Control Scripts
  • docs/architecture.md — общая архитектура системы
  • docs/devices/button.md — спецификация устройства button
  • server/SHServ/Models/EventsModel.php — исходный код триггеринга событий
  • server/SHServ/Helpers/DeviceScriptsHelper.php — хелперы синхронизации