Этот документ описывает систему событий от устройств — основной механизм реакции автоматизации на физические действия (нажатия кнопок, срабатывание датчиков и т.д.).
Устройство отправляет событие на сервер через POST /events/new:
{
"device_id": "ecf0a1b5c9d74f9a8e294c1f67b0a8b9",
"event_name": "press",
"data": { "channel": 0 }
}
Требования:
Authorization: Bearer <device_token> (токен из таблицы device_auth)device_id — это device_hard_id из БД (уникальный ID устройства)[Устройство]
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 разных имён событий — это позволяет подписываться на разном уровне детализации.
Для устройства типа 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 |
Точно — конкретная кнопка, конкретный канал |
В 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, иногда дополнительные поля)| event_name | Описание | data |
|---|---|---|
press |
Нажатие кнопки | { channel: 0 } |
online |
Устройство вышло в сеть | {} |
| event_name | Описание | data |
|---|---|---|
limit_switch_activated |
Сработал концевик (опционально, если прошивка поддерживает) | { channel: 0, state: "open" } |
| event_name | Описание | data |
|---|---|---|
limit_switch_activated |
Сработал концевик закрытия | { channel: 0 } |
calibration_failed |
Не удалась калибровка | { reason: "timeout" } |
| event_name | Описание | data |
|---|---|---|
presence_changed |
Изменение присутствия в помещении | { present: true } |
online |
Датчик вышел в сеть | {} |
Примечание: Конкретные
event_nameзависят от прошивки устройства. Смотрите документацию конкретного типа устройства вdocs/devices/.
Файл: 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 — сработает на любое нажатие в системе.
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:
200 OK и считает событие доставленным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 для указанного устройства |
События логируются в 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
<?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 Scriptsdocs/architecture.md — общая архитектура системыdocs/devices/button.md — спецификация устройства buttonserver/SHServ/Models/EventsModel.php — исходный код триггеринга событийserver/SHServ/Helpers/DeviceScriptsHelper.php — хелперы синхронизации