Control Scripts — это PHP-классы автоматизации. Базовый класс находится в server/SHServ/Middleware/ControlScripts.php, интерфейс — в server/SHServ/Implements/ControlScriptsInterface.php.
Scope-классы располагаются в automation/Scopes/ (или другом месте, если настроено иначе). Все Scope-файлы загружаются автоматически при старте сервера.
Все Scope-классы наследуют \SHServ\Middleware\ControlScripts и реализуют \SHServ\Implements\ControlScriptsInterface:
<?php
namespace ControlScripts\Scopes;
class MyScope extends \SHServ\Middleware\ControlScripts
implements \SHServ\Implements\ControlScriptsInterface {
// ...
}
Автоматическая загрузка:
При старте сервера каждый Scope-класс:
scripts, тип scope, имя = название класса)По умолчанию один Scope = одна физическая комната или чёткая зона. Это совпадает с тем, как человек мыслит свой дом: «включить свет на кухне», «автоматика в спальне».
| Что | Пример |
|---|---|
| Sync map | Кнопки и реле, физически находящиеся в этой комнате |
| Events | button@kitchen_buttons_1.press, motion@hall_sensor.online |
| Actions | Ручные скрипты для устройств комнаты |
| Regular | Автоматика комнаты: автоотключение света в ванне через 10 мин |
Когда логика явно не привязана к одной комнате, выделяйте отдельный Scope:
| Тип | Пример |
|---|---|
| Уличное / общее | OutdoorScope — прожекторы, кнопки у входных дверей |
| Сценарии | ScenesScope — «Спокойной ночи», «Ушёл из дома» |
| Инфраструктура | SystemScope — health-check'и, резервное копирование, watchdog |
Кнопки из одной комнаты часто управляют светом в другой (например, выключатель у входной двери — холл и улица). Sync map остаётся централизованным в \ControlScripts\Common::register_global_device_sync_map() — так проще искать, с чем синхронизируется конкретное устройство, не перебирая все Scope.
Commonuse \ControlScripts\Common;
class KitchenScope extends ControlScripts implements ControlScriptsInterface {
use Common; // только register_global_device_sync_map()
public function register_sync_map(): void {
$this->register_global_device_sync_map();
}
}
Common содержит только sync map. Все хелперы (auto_button_bindings, add_toggle_action, group_toggle и т.д.) живут в базовом классе ControlScripts.
Каждый Scope-класс обязан реализовать 4 метода. Они вызываются при старте сервера (если scope включён в БД).
register_sync_map(): voidРегистрирует связи «реле ↔ кнопки» для синхронизации индикаторов.
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.
register_events_handlers(): voidПодписывается на события от устройств. Обработчик вызывается асинхронно после ответа устройству (fastcgi_finish_request).
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->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.
register_actions_scripts(): voidРегистрирует action-скрипты (ручной запуск через UI/API).
public function register_actions_scripts(): void {
$this->add_action_script([
"alias" => "kitchen_light_toggle",
"name" => "Свет на кухне",
"icon" => '<i class="ph ph-lightbulb"></i>',
"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 — выключитьregister_regular_scripts(): voidРегистрирует regular-скрипты (периодический запуск по cron).
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 |
Класс \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 для указанного устройства |
// В 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");
| 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 отключается через БД — при следующем запуске сервера его скрипты не зарегистрируются.
Trait \ControlScripts\Common хранит централизованную карту синхронизации всех устройств дома:
use \ControlScripts\Common;
class KitchenScope extends \SHServ\Middleware\ControlScripts {
use Common;
public function register_sync_map(): void {
$this->register_global_device_sync_map();
}
}
Почему sync map централизован: кнопки из одной комнаты управляют светом в другой, и искать связи по всем Scope неудобно. Common остаётся единственным источником правды для sync_map.
Все остальные хелперы (auto_button_bindings, add_toggle_action, group_toggle и т.д.) находятся в базовом классе ControlScripts.
Проверьте наличие trait в вашем проекте: automation/Common.php.
Scope-классы могут планировать отложенный запуск action-скриптов, regular-скриптов или событий.
| Метод | Описание |
|---|---|
$this->delay_action($timer_alias, $action_alias, $params, $delay_seconds) |
Запланировать запуск action-скрипта через N секунд |
$this->delay_event($timer_alias, $event_name, $params, $delay_seconds) |
Запланировать вызов события через N секунд |
$this->delay_regular($timer_alias, $regular_alias, $delay_seconds) |
Запланировать запуск regular-скрипта через N секунд |
$this->cancel_timer($timer_alias) |
Отменить ожидающий таймер с указанным alias |
Повторный delay_* с тем же timer_alias внутри одного Scope заменяет старый pending-таймер.
public function register_events_handlers(): void {
// Двойное нажатие кнопки включает свет на 5 минут
$this->add_event_handler("button@kitchen_btns(1).double_press", function($device, $data) {
$this->devices()->by_alias("kitchen_relay")->device_api()->toggle_channel(0);
// Автовыключение через 5 минут
$this->delay_action("kitchen_auto_off", "kitchen_light_toggle", [], 300);
});
// Одинарное нажатие отменяет таймер (свет остаётся включённым)
$this->add_event_handler("button@kitchen_btns(1).press", function($device, $data) {
$this->cancel_timer("kitchen_auto_off");
});
}
delay_* создаёт запись в таблице shserv_timers со статусом pendingGET /cron/timers (метод CronController::run_timers()) раз в минуту выбирает pending таймеры, у которых execute_at <= NOW()try/catch:
action → ControlScripts::run_action_script()event → events()->app_call()regular → ControlScripts::run_regular_script()executed или failed (с логом ошибки)| Endpoint | Описание |
|---|---|
GET /api/v1/scripts/timers/list |
Список таймеров (все статусы) |
POST /api/v1/scripts/timers/cancel |
Отменить pending-таймер по timer_alias |
Action-скрипты могут декларировать схему входных параметров. Vue-клиент автоматически отображает форму в модалке при запуске скрипта. Сервер валидирует параметры по схеме перед вызовом closure.
public function register_actions_scripts(): void {
$this->add_action_script([
"alias" => "dim_lights",
"name" => "Диммер",
"description" => "Установить яркость в комнате",
"params_schema" => [
"level" => [
"type" => "range",
"label" => "Яркость, %",
"min" => 0,
"max" => 100,
"step" => 5,
"default" => 50,
"required" => true,
],
"room" => [
"type" => "select",
"label" => "Комната",
"options" => [
"kitchen" => "Кухня",
"hall" => "Зал",
"bedroom" => "Спальня",
],
"default" => "hall",
"required" => true,
],
"instant" => [
"type" => "toggle",
"label" => "Мгновенно (без плавного диммирования)",
"default" => false,
],
],
], function($params) {
$level = $params['level'] ?? 50;
$room = $params['room'] ?? 'hall';
$instant = $params['instant'] ?? false;
// ...
});
}
| Тип | Компонент Vue | Параметры schema |
|---|---|---|
text |
GnInput |
label, placeholder, required, default |
number |
GnInput type="number" |
label, placeholder, min, max, required, default |
range |
GnRange |
label, min, max, step, required, default |
select |
GnSelect |
label, options (ассоциативный массив value => label), required, default |
toggle |
GnSwitch |
label, default |
textarea |
GnTextarea |
label, placeholder, required, default |
POST /api/v1/scripts/actions/run проверяет params по schema. При ошибке — invalid_params с массивом failed_fields.required, сервер подставляет default из schema.text/textarea — не пустая строка).number и range проверяется min и max.options.Base class ControlScripts предоставляет high-level обёртки над повторяющимися паттернами.
| Метод | Описание |
|---|---|
$this->add_toggle_action($alias, $name, $relay_alias, $channel=0, $extra_attrs=[]) |
Зарегистрировать action script toggle + auto-sync |
$this->add_on_action($alias, $name, $relay_alias, $channel=null, $extra_attrs=[]) |
Включить реле (одно- или многоканальное) + auto-sync |
$this->add_off_action($alias, $name, $relay_alias, $channel=null, $extra_attrs=[]) |
Выключить реле + auto-sync |
$this->add_hatch_action($alias, $name, $hatch_alias, $op, $percent=100, $extra_attrs=[]) |
Открыть/закрыть люк |
$this->add_group_toggle_action($alias, $name, $pairs, $extra_attrs=[]) |
Групповой toggle нескольких реле |
Пример — было / стало:
// Было: 9 строк boilerplate
$this->add_action_script([
"alias" => "computer_table_lamp_switch",
"name" => "Комп. лампа",
"icon" => '<i class="ph ph-lamp"></i>',
], function($params) {
return $this->lamp_switch("computer_table_lamp", 0);
});
// Стало: 1 строка
$this->add_toggle_action("computer_table_lamp_switch", "Комп. лампа", "computer_table_lamp", 0,
["icon" => '<i class="ph ph-lamp"></i>']
);
// Было: 16 вызовов в LightHubScope
$this->btn_on_online("master_room_btns", [2, 3]);
$this->set_btns_click_handlers("master_room_btns");
$this->btn_on_online("btns_hall2_1");
$this->set_btns_click_handlers("btns_hall2_1");
// ... ещё 6 раз
// Стало: 1 вызов
$this->auto_button_bindings([
"master_room_btns" => ["mute" => [2, 3]],
"btns_hall2_1" => [],
"hall_secondary" => [],
"bed_btns_right_1" => [],
"kitchen_buttons_1" => [],
]);
// Выключить всё разом (внутри action script или event handler)
$this->group_off([
"hall_relay",
["light_hub_1", 0],
["light_hub_1", 1],
]);
// Групповой toggle
$this->group_toggle([
"spotlight_main_front_1",
"spotlight_main_back_1",
"spotlight_main_back_2",
]);
Методы group_set_state и group_toggle автоматически синхронизируют индикаторы кнопок.
Моды — это boolean-теги (не взаимоисключающие), которые задают текущий контекст системы: «Дома», «Не дома», «Ночь», «Сон», «Кино» и т.д. Scope-классы могут проверять активные моды в хендлерах и action-скриптах, меняя поведение в зависимости от ситуации.
Все моды декларируются централизованно в automation/ModesRegistry.php:
class ModesRegistry {
public static function definitions(): array {
return [
'home' => [
'label' => 'Дома',
'description' => 'Основной режим присутствия. Все системы работают в штатном режиме.',
],
'away' => [
'label' => 'Не дома',
'description' => 'Никого нет дома. Активны охранные сценарии и энергосбережение.',
],
'night' => [
'label' => 'Ночь',
'description' => 'Пониженная яркость освещения, тихие уведомления.',
],
'sleep' => [
'label' => 'Сон',
'description' => 'Минимум света и звука. Датчики движения переведены в тихий режим.',
],
];
}
}
Label и description живут в коде — БД (
shserv_modes) хранит только runtime-состояние (is_active). При первом обращении кModesContextреестр автоматически синхронизируется с таблицей (INSERT IGNORE).
| Метод | Описание |
|---|---|
$this->mode()->is('night') |
Проверить, активен ли конкретный мод |
$this->mode()->any(['night', 'sleep']) |
Хотя бы один из модов активен |
$this->mode()->all(['away', 'sleep']) |
Все перечисленные моды активны |
$this->mode()->active() |
Массив активных тегов |
$this->mode()->enable('away') |
Активировать мод |
$this->mode()->disable('away') |
Деактивировать мод |
$this->mode()->toggle('movie') |
Переключить мод |
$this->mode()->meta('away') |
Получить ['label' => ..., 'description' => ...] |
public function register_events_handlers(): void {
// При движении в коридоре — яркость зависит от времени суток
$this->add_event_handler("motion.hall", function($device, $data) {
if ($this->mode()->any(['night', 'sleep'])) {
$this->run_action('dim_hall_lights', ['level' => 20]);
} else {
$this->run_action('dim_hall_lights', ['level' => 80]);
}
});
}
public function register_actions_scripts(): void {
// Ручной скрипт «Уйти из дома»
$this->add_action_script([
"alias" => "set_mode_away",
"name" => "Не дома",
], function($params) {
$this->mode()->enable('away');
$this->mode()->disable('home');
// ... выключить весь свет, включить охрану
});
}
| Endpoint | Описание |
|---|---|
GET /api/v1/modes/list |
Список всех модов с label, description, is_active |
GET /api/v1/modes/active |
Массив активных тегов |
POST /api/v1/modes/{tag}/enable |
Активировать мод |
POST /api/v1/modes/{tag}/disable |
Деактивировать мод |
Моды не взаимоисключающие — можно одновременно включить
away+sleep. Если нужно эмулировать взаимоисключение (например, «Дома» vs «Не дома»), управляйте этим явно в action-скрипте:$this->mode()->enable('away'); $this->mode()->disable('home');.