diff --git a/docs/architecture.md b/docs/architecture.md index fc8ac44..74e947d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -192,33 +192,86 @@ ## Система событий (Events от устройств) -Устройство отправляет событие на `POST /events/new` сервера: +### Отправка события устройством + +Устройство отправляет событие на `POST /events/new`: ```json { - "device_id": "", - "event_name": "button_press", + "device_id": "ecf0a1b5c9d74f9a8e294c1f67b0a8b9", + "event_name": "press", "data": { "channel": 0 } } ``` -Сервер (`EventsController`) ищет устройство по `device_hard_id`, проверяет авторизацию, немедленно отвечает `200 OK`, а затем (после `fastcgi_finish_request`) вызывает `EventsModel`, который триггерит события через Fury Events в нескольких форматах: +Требует `Authorization: Bearer `. -| Паттерн события | Пример | Описание | -|----------------|---------|---------| -| `{event_name}` | `button_press` | Глобальный — любое устройство | -| `{device_type}.{event_name}` | `button.button_press` | По типу устройства | -| `{device_type}@{alias}.{event_name}` | `button@kitchen_btns.button_press` | По alias устройства | -| `{device_type}({channel}).{event_name}` | `button(2).button_press` | По типу + каналу | -| `{device_type}@{alias}({channel}).{event_name}` | `button@kitchen_btns(2).button_press` | По alias + каналу (самый точный) | +### Жизненный цикл на сервере + +``` +[EventsController::new_event()] + 1. Найти устройство по device_hard_id + 2. Проверить auth (device_auth) + 3. Залогировать событие + 4. Обновить last_contact и connection_status + 5. Ответить 200 OK немедленно + 6. fastcgi_finish_request() — асинхронное продолжение + ↓ +[EventsModel] — триггерит 5 вариантов через Fury Events +``` + +**Почему 5 вариантов?** Одно физическое событие триггерит 5 имён — это позволяет подписываться на разном уровне детализации. + +### Пять паттернов событий + +Для `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` | Конкретное устройство | +| 4 | `{device_type}({ch}).{event_name}` | `button(2).press` | Все устройства, канал 2 | +| 5 | `{device_type}@{alias}({ch}).{event_name}` | `button@kitchen_btns(2).press` | Точно: устройство + канал | + +### EventsModel (server/SHServ/Models/EventsModel.php) + +```php +// 1. Канал + alias + channel +$events_model->channel_alias_device_event_call($device, $event_name, $channel, $data); +// → button@kitchen_btns(2).press + +// 2. Канал + type + channel +$events_model->channel_device_event_call($device, $event_name, $channel, $data); +// → button(2).press + +// 3. Alias (все каналы) +$events_model->alias_device_event_call($device, $event_name, $data); +// → button@kitchen_btns.press + +// 4. Тип устройства +$events_model->global_device_event_call($device, $event_name, $data); +// → button.press + +// 5. Глобально (любое устройство) +$events_model->global_any_device_event_call($device, $event_name, $data); +// → press +``` + +Полное описание: `docs/events-from-devices.md`. --- -## Control Scripts (ControlScripts/Scopes/) +## Control Scripts (server/SHServ/Middleware/ControlScripts.php) -Скрипты автоматизации — это PHP-классы в `server/ControlScripts/Scopes/`, наследующие `ControlScripts` и реализующие `ControlScriptsInterface`. Все Scope-файлы загружаются автоматически при старте приложения. +Скрипты автоматизации — это PHP-классы (Scope), расположенные в `server/ControlScripts/Scopes/` (или другом месте, если настроено иначе). -### Структура Scope-класса +### Базовый класс + +Все Scope-классы: +- Наследуют `\SHServ\Middleware\ControlScripts` +- Реализуют `\SHServ\Implements\ControlScriptsInterface` +- Автоматически загружаются при старте сервера ```php class MyScope extends \SHServ\Middleware\ControlScripts @@ -231,26 +284,35 @@ } ``` +### Четыре регистрационных метода + +| Метод | Назначение | Когда вызывается | +|-------|------------|------------------| +| `register_sync_map()` | Связи реле ↔ кнопки для синхронизации индикаторов | При старте сервера | +| `register_events_handlers()` | Подписка на события от устройств | При старте сервера | +| `register_actions_scripts()` | Action-скрипты (ручной запуск) | При старте сервера | +| `register_regular_scripts()` | Regular-скрипты (периодические) | При старте сервера | + ### Типы скриптов -**Action-скрипты** — вызываются вручную через API (`POST /api/v1/scripts/actions/run`): +**Action-скрипты** — ручной запуск через `POST /api/v1/scripts/actions/run`: ```php $this->add_action_script([ - "alias" => "my_action", - "name" => "Имя", + "alias" => "kitchen_light_toggle", + "name" => "Свет на кухне", "icon" => '', - "author" => "Name" ], function($params) { - // логика - return ["result" => ...]; + $relay = $this->devices()->by_alias("kitchen_relay"); + $relay->device_api()->toggle_channel(0); + return ["result" => true]; }); ``` -**Regular-скрипты** — запускаются по cron (`GET /cron/regular-scripts`): +**Regular-скрипты** — периодический запуск через `GET /cron/regular-scripts`: ```php $this->add_regular_script([ - "alias" => "my_regular", - "name" => "Имя" + "alias" => "check_door_sensor", + "name" => "Проверка датчика двери", ], function() { // периодическая логика }); @@ -258,57 +320,101 @@ **Event-хендлеры** — реакция на события от устройств: ```php -$this->add_event_handler("button@{$alias}({$channel}).press", function(Device $device, array $data) { - // логика +// Конкретная кнопка, канал 1 +$this->add_event_handler("button@kitchen_btns(1).press", function(Device $device, array $data) { + $relay = $this->devices()->by_alias("kitchen_relay"); + $relay->device_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); }); ``` ### Sync map -Декларативное описание связей «реле-канал ↔ кнопки-каналы» для синхронизации индикаторов: +Декларативное описание связей «реле ↔ кнопки» для синхронизации индикаторов: + ```php -$this->add_sync_connection([ - ["type" => "relay", "alias" => "kitchen_relay", "channel" => 0], - ["type" => "button", "alias" => "kitchen_btns", "channel" => 1], -]); +public function register_sync_map(): void { + $this->add_sync_connection([ + ["type" => "relay", "alias" => "kitchen_relay", "channel" => 0], + ["type" => "button", "alias" => "kitchen_btns", "channel" => 1], + ]); +} ``` -`DeviceScriptsHelper::sync_relay_to_btns()` читает sync map и обновляет индикаторы кнопок при изменении состояния реле. +**Хелперы синхронизации** (`DeviceScriptsHelper`): +- `sync_relay_to_btns($sync_map, $relay_alias)` — синхронизировать кнопки с реле +- `sync_btn_channels($sync_map, $btn_alias)` — синхронизировать индикаторы кнопки с реле + +Полное описание: `docs/control-scripts-guide.md`, `docs/events-from-devices.md`. --- ## Веб-клиент -SPA на vanilla JS + esbuild. Роутинг через hash `#!/route`. +### Vue Client (webclient/) — основной + +Современный SPA на Vue 3 + Pinia + Vite + vue-router (hash mode). ``` -src/js/ -├── index.js — init: SmartHomeApi, Screens, routes() -├── routes.js — привязка роутов к экранам -├── DataProvider.js — простое key-value хранилище состояния (window.DataProvider) -├── sh/SmartHomeApi.js — HTTP-клиент (fetch + callback-style) -│ └── modules/ — DevicesApi, ScriptsApi, AreasApi -└── components/ - ├── Screens.js — переключение экранов по hash - ├── hud.js — верхний HUD с навигацией - ├── modals.js — модальные окна - ├── toasts.js — уведомления - └── screens/ — экраны devices, areas, scripts +webclient/ +├── index.php — entry point (serve dist/index.html) +├── vite.config.js — Vite конфиг (dev server + build) +├── src/ +│ ├── app/main.js — Vue app entry (createApp + Pinia + router) +│ ├── router/routes.js — hash-роуты +│ ├── router/index.js — настройки роутера + навигация логи +│ ├── api/ +│ │ ├── client.js — базовый API клиент +│ │ ├── http.js — HTTP слой (fetch, логирование) +│ │ ├── mappers.js — маппинг DTO → domain +│ │ └── modules/ — devices, areas, scripts, scanning +│ ├── stores/ — Pinia stores (Composition API) +│ │ ├── devices.js +│ │ ├── areas.js +│ │ ├── scripts.js +│ │ └── favorites.js +│ ├── features/ — feature-модули (pages, components) +│ │ ├── devices/pages/ +│ │ ├── areas/pages/ +│ │ └── scripts/pages/ +│ ├── components/ — общие UI компоненты +│ └── utils/logger.js — централизованное логирование +└── dist/ — production build ``` -### SmartHomeApi - -Callback-style клиент. Все запросы идут через `proxy.php` (опция `proxy_path`): - -```js -const api = new SmartHomeApi({ - base_url: API_BASEURL, - token: "YOUR_TOKEN", - proxy_path: "/proxy.php", -}); - -api.devices.list((err, data) => { ... }); -api.scripts.run({ alias: "my_action" }, (err, data) => { ... }); +**Команды:** +```bash +cd webclient +npm run dev # dev server с прокси на PHP +npm run build # production build → dist/ +npm test # Vitest тесты ``` -Константа `API_BASEURL` инжектируется через esbuild при сборке (`webclient/gulpfile.js`). +**Стилевые правила:** +- Использовать семантические классы `gnexus-ui-kit` (`.text-success`, `.badge-warning`, и т.д.) +- Не использовать CSS custom properties (`--color-*`) напрямую + +### Legacy Client (webclient_legacy/) — архив + +Vanilla JS + esbuild + SCSS. Поддерживается для обратной совместимости. + +``` +webclient_legacy/ +├── src/js/ +│ ├── index.js — entry point +│ ├── sh/SmartHomeApi.js — callback-style API клиент +│ ├── routes.js — hash роутинг +│ └── components/ — UI компоненты (Screens, modals, toasts) +└── dist/ — сборка через gulp +``` + +**Команды:** +```bash +cd webclient_legacy +npm start # gulp: SCSS → CSS, JS bundle, live-reload +``` diff --git a/docs/control-scripts-guide.md b/docs/control-scripts-guide.md index da2e370..7df4d76 100644 --- a/docs/control-scripts-guide.md +++ b/docs/control-scripts-guide.md @@ -1,192 +1,224 @@ # Руководство по написанию Control Scripts -Control Scripts — это PHP-классы автоматизации, живущие в `server/ControlScripts/Scopes/`. Все файлы в этой папке загружаются автоматически при старте сервера. +Control Scripts — это PHP-классы автоматизации. Базовый класс находится в `server/SHServ/Middleware/ControlScripts.php`, интерфейс — в `server/SHServ/Implements/ControlScriptsInterface.php`. + +Scope-классы располагаются в `server/ControlScripts/Scopes/` (или другом месте, если настроено иначе). Все Scope-файлы загружаются автоматически при старте сервера. --- -## Структура класса +## Базовый класс + +Все Scope-классы наследуют `\SHServ\Middleware\ControlScripts` и реализуют `\SHServ\Implements\ControlScriptsInterface`: ```php button для синхронизации индикаторов - public function register_sync_map(): void { } - - // Подписаться на события от устройств - public function register_events_handlers(): void { } - - // Зарегистрировать action-скрипты (запуск вручную через UI/API) - public function register_actions_scripts(): void { } - - // Зарегистрировать regular-скрипты (запуск по cron) - public function register_regular_scripts(): void { } + // ... } ``` ---- - -## Action-скрипты - -Запускаются вручную через `POST /api/v1/scripts/actions/run` или из UI. - -```php -$this->add_action_script([ - "alias" => "kitchen_light_toggle", // уникальный alias - "name" => "Свет на кухне", - "icon" => '', // Phosphor Icons - "description" => "Включить/выключить основной свет", - "author" => "Eugene Sukhodolskiy" -], function($params) { - $relay_api = $this->devices()->by_alias("kitchen_relay")->device_api(); - - if ($relay_api instanceof \SHServ\Tools\DeviceAPI\Relay) { - $relay_api->toggle_channel(0); - } - - return ["result" => true]; -}); -``` - -Включение/выключение конкретного action-скрипта: -- `GET /api/v1/scripts/actions/alias/{alias}/enable` -- `GET /api/v1/scripts/actions/alias/{alias}/disable` +**Автоматическая загрузка:** +При старте сервера каждый Scope-класс: +1. Проверяет состояние в БД (таблица `scripts`, тип `scope`, имя = название класса) +2. Если scope включён (или это первый запуск) — вызывает 4 регистрационных метода +3. Методы регистрируют обработчики в статические коллекции Fury Events --- -## Regular-скрипты - -Запускаются периодически через cron: `GET /cron/regular-scripts`. - -```php -$this->add_regular_script([ - "alias" => "check_door_sensor", - "name" => "Проверка датчика двери", -], function() { - $sensor = $this->devices()->by_alias("door_sensor"); - // ... -}); -``` - -Включение/выключение: -- `GET /api/v1/scripts/actions/regular/{alias}/enable` -- `GET /api/v1/scripts/actions/regular/{alias}/disable` - --- -## Event-хендлеры +## Четыре регистрационных метода -Подписка на события от устройств. Обработчик вызывается **после** ответа устройству (через `fastcgi_finish_request`), поэтому может занимать время. +Каждый Scope-класс обязан реализовать 4 метода. Они вызываются при старте сервера (если scope включён в БД). -```php -// Нажатие кнопки (канал 0) конкретного устройства -$this->add_event_handler("button@my_btns(0).press", function(Device $device, array $data) { - $relay = $this->devices()->by_alias("my_relay"); - $relay->device_api()->toggle_channel(0); -}); +### 1. `register_sync_map(): void` -// Приход устройства онлайн -$this->add_event_handler("button@my_btns.online", function(Device $device, array $data) { - // синхронизировать индикаторы - $this->helper()->sync_btn_channels($this->sync_map(), $device->alias); -}); -``` - -### Паттерны имён событий - -| Паттерн | Пример | Когда срабатывает | -|---------|---------|------------------| -| `{event_name}` | `button_press` | Любое устройство, любое событие с таким именем | -| `{type}.{event_name}` | `button.button_press` | Все устройства типа `button` | -| `{type}@{alias}.{event_name}` | `button@kitchen_btns.online` | Конкретное устройство | -| `{type}({ch}).{event_name}` | `button(2).button_press` | Все устройства типа, канал 2 | -| `{type}@{alias}({ch}).{event_name}` | `button@kitchen_btns(2).button_press` | Конкретное устройство, канал 2 | - -### Известные event_name (от устройств) - -| Устройство | event_name | Описание | -|-----------|-----------|---------| -| button | `press` | Нажатие кнопки | -| button | `online` | Устройство вышло в сеть | -| relay | `limit_switch_activated` | Сработал концевик | -| hatch | `limit_switch_activated` | Сработал концевик закрытия | -| hatch | `calibration_failed` | Не удалась калибровка | -| sensor | `presence_changed` | Изменение присутствия в помещении | - ---- - -## Sync map - -Описывает связи «какой канал реле ↔ какие каналы кнопок». Используется для автоматической синхронизации индикаторов кнопок с состоянием реле. +Регистрирует связи «реле ↔ кнопки» для синхронизации индикаторов. ```php 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 хранится в статической переменной `ControlScripts::$sync_map_storage`. -### Хелперы синхронизации +--- + +### 2. `register_events_handlers(): void` + +Подписывается на события от устройств. Обработчик вызывается **асинхронно** после ответа устройству (`fastcgi_finish_request`). ```php -// При нажатии кнопки — переключить реле и синхронизировать все кнопки -$relay_api->toggle_channel($relay_channel); -$this->helper()->sync_relay_to_btns($this->sync_map(), $relay_alias); +public function register_events_handlers(): void { + // Нажатие кнопки (канал 1) конкретного устройства + $this->add_event_handler("button@kitchen_btns(1).press", function(Device $device, array $data) { + $relay = $this->devices()->by_alias("kitchen_relay"); + $relay->device_api()->toggle_channel(0); + $this->helper()->sync_relay_to_btns($this->sync_map(), "kitchen_relay"); + }); -// При появлении кнопки онлайн — синхронизировать её индикаторы с реле -$this->helper()->sync_btn_channels($this->sync_map(), $btn_alias); + // Устройство вышло в сеть (после перезагрузки) + $this->add_event_handler("button@kitchen_btns.online", function(Device $device, array $data) { + $this->helper()->sync_btn_channels($this->sync_map(), $device->alias); + }); +} ``` +**Паттерны имён событий:** + +| Паттерн | Пример | Описание | +|---------|---------|---------| +| `{event_name}` | `press` | Любое устройство, любое событие | +| `{type}.{event_name}` | `button.press` | Все устройства типа `button` | +| `{type}@{alias}.{event_name}` | `button@kitchen_btns.press` | Конкретное устройство (все каналы) | +| `{type}({ch}).{event_name}` | `button(1).press` | Все устройства типа, канал 1 | +| `{type}@{alias}({ch}).{event_name}` | `button@kitchen_btns(1).press` | Конкретное устройство, канал 1 | + +Полное описание: `docs/events-from-devices.md`. + +--- + +### 3. `register_actions_scripts(): void` + +Регистрирует action-скрипты (ручной запуск через UI/API). + +```php +public function register_actions_scripts(): void { + $this->add_action_script([ + "alias" => "kitchen_light_toggle", + "name" => "Свет на кухне", + "icon" => '', + "description" => "Включить/выключить основной свет", + "author" => "Eugene Sukhodolskiy" + ], 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]; + }); +} +``` + +**API:** +- `POST /api/v1/scripts/actions/run` — запустить скрипт +- `GET /api/v1/scripts/actions/alias/{alias}/enable` — включить +- `GET /api/v1/scripts/actions/alias/{alias}/disable` — выключить + +--- + +### 4. `register_regular_scripts(): void` + +Регистрирует regular-скрипты (периодический запуск по cron). + +```php +public function register_regular_scripts(): void { + $this->add_regular_script([ + "alias" => "check_door_sensor", + "name" => "Проверка датчика двери", + ], function() { + $sensor = $this->devices()->by_alias("door_sensor"); + // ... + }); +} +``` + +**API:** +- `GET /cron/regular-scripts` — запустить все enabled regular-скрипты (cron) +- `GET /api/v1/scripts/regular/alias/{alias}/enable` — включить +- `GET /api/v1/scripts/regular/alias/{alias}/disable` — выключить + --- ## Доступные методы базового класса +| Метод | Возвращает | Описание | +|-------|------------|----------| +| `$this->devices()` | `\SHServ\Models\Devices` | Поиск устройств по alias, id, hard_id | +| `$this->helper()` | `\SHServ\Helpers\DeviceScriptsHelper` | Хелперы синхронизации реле/кнопок | +| `$this->sync_map()` | `array` | Текущий sync_map (статическое хранилище) | +| `$this->add_event_handler($name, $cb)` | `void` | Подписаться на событие | +| `$this->add_action_script($attrs, $cb)` | `bool` | Зарегистрировать action-скрипт | +| `$this->add_regular_script($attrs, $cb)` | `bool` | Зарегистрировать regular-скрипт | +| `$this->add_sync_connection($entries)` | `void` | Добавить связь в sync_map | + +--- + +## DeviceScriptsHelper + +Класс `\SHServ\Helpers\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 для указанного устройства | + +### Пример использования + ```php -$this->devices() // → Models\Devices (поиск по alias, id, hard_id) -$this->helper() // → DeviceScriptsHelper (синхронизация) -$this->sync_map() // → текущий sync_map_storage -$this->add_event_handler($name, $cb) -$this->add_action_script($attrs, $cb) -$this->add_regular_script($attrs, $cb) -$this->add_sync_connection($entries) +// В event-хендлере: переключить реле и синхронизировать кнопки +$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->helper()->sync_btn_channels($this->sync_map(), "kitchen_btns"); ``` --- ## Управление Scope через API -Включение/выключение целого Scope (всех его скриптов): -- `GET /api/v1/scripts/actions/scope/{name}/enable` -- `GET /api/v1/scripts/actions/scope/{name}/disable` +| Endpoint | Описание | +|----------|---------| +| `GET /api/v1/scripts/actions/scope/{name}/enable` | Включить Scope (в БД) | +| `GET /api/v1/scripts/actions/scope/{name}/disable` | Выключить Scope (в БД) | +| `GET /api/v1/scripts/scopes/list` | Список всех Scope с состоянием | +| `GET /api/v1/scripts/scopes/name/{name}` | Исходный код PHP-файла Scope | +| `POST /api/v1/scripts/scopes/update` | Обновить код Scope | -Scope отключается через БД — при следующем запуске сервера его скрипты не зарегистрируются. +**Важно:** Scope отключается через БД — при следующем запуске сервера его скрипты не зарегистрируются. --- -## Общий trait Common +## Trait Common (опционально) -`server/ControlScripts/Common.php` — trait с готовыми хелперами для Scope-классов: +Некоторые проекты используют trait `\ControlScripts\Common` с готовыми хелперами: ```php use \ControlScripts\Common; -// Зарегистрировать глобальный sync_map (все реле и кнопки системы) -$this->register_global_device_sync_map(); - -// Установить индикаторы заглушённых каналов при появлении кнопки онлайн -$this->btn_on_online("my_btns", [/* muted channels */]); - -// Установить обработчики нажатий для кнопок из sync_map -$this->set_btns_click_handlers("my_btns"); +class MyScope extends \SHServ\Middleware\ControlScripts { + use Common; + + public function register_sync_map(): void { + // Зарегистрировать глобальный sync_map (все реле и кнопки системы) + $this->register_global_device_sync_map(); + } + + public function register_events_handlers(): void { + // Установить обработчики нажатий для кнопок из sync_map + $this->set_btns_click_handlers("kitchen_btns"); + } +} ``` + +Проверьте наличие trait в вашем проекте: `server/ControlScripts/Common.php`. diff --git a/docs/events-from-devices.md b/docs/events-from-devices.md new file mode 100644 index 0000000..a554bf1 --- /dev/null +++ b/docs/events-from-devices.md @@ -0,0 +1,336 @@ +# События от устройств (Events) + +Этот документ описывает систему событий от устройств — основной механизм реакции автоматизации на физические действия (нажатия кнопок, срабатывание датчиков и т.д.). + +--- + +## 1. Как устройство отправляет событие + +Устройство отправляет событие на сервер через `POST /events/new`: + +```json +{ + "device_id": "ecf0a1b5c9d74f9a8e294c1f67b0a8b9", + "event_name": "press", + "data": { "channel": 0 } +} +``` + +**Требования:** +- Заголовок `Authorization: Bearer ` (токен из таблицы `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 +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` + +```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): + +```php +$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 — декларативное описание связей «реле ↔ кнопки» для автоматической синхронизации индикаторов. + +### Регистрация связи + +```php +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], + ]); +} +``` + +### Хелперы синхронизации + +```php +// После переключения реле — синхронизировать все кнопки из 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/`: + +```json +{"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} +``` + +### Проверка подписок + +```bash +# Убедиться, что 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 +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" => '', + ], 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` — хелперы синхронизации diff --git a/docs/server-api.md b/docs/server-api.md index b5c531a..9023c49 100644 --- a/docs/server-api.md +++ b/docs/server-api.md @@ -478,9 +478,10 @@ ## События от устройств ### `POST /events/new` -Endpoint для устройств (не для клиента). Принимает событие, немедленно отвечает `200 OK`, затем асинхронно запускает обработчики из Control Scripts. -**Тело (от устройства):** +**Назначение:** Принимать события от устройств (не для клиентского кода). + +**Тело запроса (от устройства):** ```json { "device_id": "ecf0a1b5c9d74f9a8e294c1f67b0a8b9", @@ -489,7 +490,40 @@ } ``` -Требует `Authorization: Bearer `. +**Требования:** +- Заголовок `Authorization: Bearer ` (токен из `device_auth`) +- `device_id` — `device_hard_id` устройства (уникальный ID из БД) + +**Ответ:** +```json +{ + "status": "ok" +} +``` + +**Жизненный цикл:** +1. `EventsController` находит устройство по `device_hard_id` +2. Проверяет, что `device_auth` активен +3. Логирует событие (`php:Events`) +4. Обновляет `last_contact` и `connection_status = "active"` +5. **Немедленно отвечает 200 OK** +6. `fastcgi_finish_request()` — ответ отправлен +7. Асинхронно: `EventsModel` триггерит 5 вариантов событий через Fury Events + +**Пять паттернов событий (триггерятся одновременно):** + +| Паттерн | Пример имени | Описание | +|---------|-------------|---------| +| `{event_name}` | `press` | Глобально — любое устройство | +| `{device_type}.{event_name}` | `button.press` | Все устройства типа | +| `{device_type}@{alias}.{event_name}` | `button@kitchen_btns.press` | Конкретное устройство | +| `{device_type}({channel}).{event_name}` | `button(2).press` | Все устройства, канал 2 | +| `{device_type}@{alias}({channel}).{event_name}` | `button@kitchen_btns(2).press` | Точно: устройство + канал | + +**Обработчики событий:** +Control Scripts подписываются через `$this->add_event_handler()` в `register_events_handlers()`. Обработчики вызываются после ответа устройству (асинхронно). + +Полное описание: `docs/events-from-devices.md`. ---