Newer
Older
smart-home-server / docs / control-scripts-guide.md

Руководство по написанию Control Scripts

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-класс:

  1. Проверяет состояние в БД (таблица scripts, тип scope, имя = название класса)
  2. Если scope включён (или это первый запуск) — вызывает 4 регистрационных метода
  3. Методы регистрируют обработчики в статические коллекции Fury Events


Рекомендуемая структура Scope-классов

По умолчанию один Scope = одна физическая комната или чёткая зона. Это совпадает с тем, как человек мыслит свой дом: «включить свет на кухне», «автоматика в спальне».

Что кладём в Scope комнаты

Что Пример
Sync map Кнопки и реле, физически находящиеся в этой комнате
Events button@kitchen_buttons_1.press, motion@hall_sensor.online
Actions Ручные скрипты для устройств комнаты
Regular Автоматика комнаты: автоотключение света в ванне через 10 мин

Исключения — функциональные скоупы

Когда логика явно не привязана к одной комнате, выделяйте отдельный Scope:

Тип Пример
Уличное / общее OutdoorScope — прожекторы, кнопки у входных дверей
Сценарии ScenesScope — «Спокойной ночи», «Ушёл из дома»
Инфраструктура SystemScope — health-check'и, резервное копирование, watchdog

Cross-room связи

Кнопки из одной комнаты часто управляют светом в другой (например, выключатель у входной двери — холл и улица). Sync map остаётся централизованным в \ControlScripts\Common::register_global_device_sync_map() — так проще искать, с чем синхронизируется конкретное устройство, не перебирая все Scope.

Trait Common

use \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 включён в БД).

1. 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.


2. 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.


3. 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 — выключить

4. 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

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 для указанного устройства

Пример использования

// В 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

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 Common

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.


Таймеры и задержки (Timers)

Scope-классы могут планировать отложенный запуск action-скриптов, regular-скриптов или событий.

API внутри Scope

Метод Описание
$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");
    });
}

Как это работает

  1. delay_* создаёт запись в таблице shserv_timers со статусом pending
  2. Cron-задача GET /cron/timers (метод CronController::run_timers()) раз в минуту выбирает pending таймеры, у которых execute_at <= NOW()
  3. Таймер выполняется inline с try/catch:
    • actionControlScripts::run_action_script()
    • eventevents()->app_call()
    • regularControlScripts::run_regular_script()
  4. После выполнения статус меняется на executed или failed (с логом ошибки)

REST API для таймеров

Endpoint Описание
GET /api/v1/scripts/timers/list Список таймеров (все статусы)
POST /api/v1/scripts/timers/cancel Отменить pending-таймер по timer_alias

Params Schema для action scripts

Action-скрипты могут декларировать схему входных параметров. Vue-клиент автоматически отображает форму в модалке при запуске скрипта. Сервер валидирует параметры по схеме перед вызовом closure.

Объявление в Scope

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

Правила валидации

  • Валидация на сервере: перед вызовом closure POST /api/v1/scripts/actions/run проверяет params по schema. При ошибке — invalid_params с массивом failed_fields.
  • Default values: если поле не передано и не required, сервер подставляет default из schema.
  • Required: поле обязано присутствовать в запросе (для text/textarea — не пустая строка).
  • Границы: для number и range проверяется min и max.
  • Select: значение должно быть ключом из options.

Хелперы для упрощения написания скриптов

Base class ControlScripts предоставляет high-level обёртки над повторяющимися паттернами.

Action script generators

Метод Описание
$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 автоматически синхронизируют индикаторы кнопок.


Системные моды (System Modes)

Моды — это 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).

API внутри Scope

Метод Описание
$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');
        // ... выключить весь свет, включить охрану
    });
}

REST API для модов

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');.