Newer
Older
smart-home-server / docs / planning / automation-scripts-audit.md

Аудит Control Scripts — текущее состояние и предложения по улучшению

Дата: 2026-06-08
Аудитор: Claude Code
Область: automation/, server/SHServ/Middleware/ControlScripts.php, сопутствующие сервисы.


Резюме

Система Control Scripts работает, но имеет критические баги, архитектурные ограничения и существенные пробелы в DX. Главные риски:

  1. Regular-скрипты через cron фактически сломаны — CLI entry point не реализован.
  2. Весь runtime скриптов — static state, что блокирует тестирование, горячую перезагрузку и масштабируемость.
  3. Добавление любой новой автоматизации требует git commit + deploy — нет runtime-конфигурации через UI/DB.
  4. Нет защиты от ошибок — exception в event handler убивает весь async-контекст без логов.

Выполнено (2026-06-08)

  • P0.1CronController::run_regular_cron_scripts() запускает regular scripts inline (без exec/console.php), с set_time_limit(300) и flock.
  • P0.2ControlScripts::add_event_handler() обёрнут в try/catch, ошибки логируются.
  • P0.3POST /api/v1/scripts/scopes/update реализован. Требует scripts.edit. Валидация php -l, namespace, class name. Backup + opcache_invalidate.
  • P1.4ScriptsRegistry заменяет static state. Живёт в app(), flush per request.
  • P1.5 — Autodiscover Scope по automation/Scopes/*.php. Удалён scopes-manifest.json. Проверка implements ControlScriptsInterface + namespace ControlScripts\Scopes.
  • P1.7set_time_limit(300) + flock в regular scripts cron.
  • P2 — Timers / delays — таблица shserv_timers, модель Timers, методы delay_action/delay_event/delay_regular/cancel_timer в ControlScripts, cron GET /cron/timers, REST endpoints, тесты TimersTest.php.
  • ✅ Документация обновлена: docs/control-scripts-guide.md, docs/server-api-v1/scripts.md, docs/architecture.md, docs/server-api.md, docs/events-from-devices.md.

Осталось

  • P1.3 Namespace хардкожен — низкий приоритет, не критично для текущей структуры.
  • P1.6 get_source_code() reflection — ограничение известное, не блокирует.
  • P2 — System modes, params schema, execution log — отложено по решению владельца.
  • P3 — Scope versioning / rollback, DI в ControlScripts — отложено.

Часть 1. Что работает сейчас (кратко)

Компонент Статус Примечание
Базовый класс ControlScripts 4 abstract метода, static хранилища
Scope-автозагрузка Autodiscover: glob(automation/Scopes/*.php) + implements ControlScriptsInterface
Event handlers Fury pub/sub, 5 паттернов имён, async через fastcgi_finish_request
Action scripts Регистрация, enable/disable, запуск через API, логирование времени
Sync map + DeviceScriptsHelper Связи relay↔button, синхронизация индикаторов
Common trait register_global_device_sync_map(), set_btns_click_handlers(), btn_on_online()
API list / state toggle ScriptsRESTAPIController + Scripts model
Legacy client support scope_file отдаёт source code

Часть 2. Критические баги (P0) — чинить немедленно

2.1. Regular scripts через cron не запускаются

Где: server/SHServ/Controllers/CronController.php:54, server/console.php

// CronController.php
$this -> run_script_cli($alias);
// → exec("php console.php run-regular-script ...")

// console.php — НЕТ такой команды!

Последствие: Все regular-скрипты (включая spotlights_by_time) молча не выполняются.

Починка: Добавить в console.php обработку run-regular-script или отказаться от CLI-обёртки и запускать inline через ControlScripts::run_regular_script().


2.2. Нет endpoint'а обновления кода Scope

Где: docs/control-scripts-guide.md обещает POST /api/v1/scripts/scopes/update, но в ScriptsRESTAPI_v1.php и ScriptsRESTAPIController его нет. В ScriptsRESTAPIController.php:8 висит TODO:

- Реализовать апдейт скриптов

Последствие: Редактирование Scope через UI невозможно. Legacy client может читать код (scope_file), но не писать.


2.3. Exception в event handler = тихий провал

Где: ControlScripts::add_event_handler() — callback не обёрнут в try/catch.

events() -> handler("app:{$event_name}", function(Array $params) use ($handler) {
    $handler($params["device"], $params["data"]);
    // ↑ если тут Exception — Fury Events не ловит, лог не пишется
});

Последствие: Ошибка в automation logic (например, устройство offline → $relay_api->toggle_channel() бросает Exception) убивает весь async-контекст. Устройство получило 200 OK, но автоматизация не сработала, и в лог попадёт только PHP fatal, если вообще попадёт.


Часть 3. Архитектурные недостатки (P1)

3.1. Static state — ядро всех проблем

protected static $regular_scripts = [];
protected static $actions_scripts = [];
protected static $sync_map_storage = ["connections" => []];

Что ломает:

  • Нельзя перезагрузить скрипты без перезапуска процессаflush_statics() есть, но после него нужно заново инстанцировать все Scope. В PHP-FPM это означает kill -USR1 или ждать child restart.
  • Нельзя unit-testить — static state течёт между тестами. PHPUnit в одном процессе получит скрипты от предыдущего теста.
  • Нельзя иметь несколько окружений в одном процессе.

Направление решения: Перейти на Registry patternScriptsRegistry как singleton через app(), а static поля заменить на instance-поля реестра. Тогда app()->scripts_registry->flush() работает в рамках одного request lifecycle.


3.2. Ручной манифест scopes-manifest.json

Любой новый Scope требует правки JSON. Это незабываемый шаг, легко ошибиться.

Направление решения: Autodiscover по automation/Scopes/*.php через glob() + get_declared_classes() или ReflectionClass на файлы. Достаточно проверять implements ControlScriptsInterface.


3.3. Namespace и путь хардкожены

// App.php:90
$full_script_name = "\\ControlScripts\\Scopes\\{$script_name}";

Нельзя положить Scope в под-пакет (например, ControlScripts\Scopes\Security\AlarmScope).


3.4. Хрупкий парсинг имени Scope в constructor

// ControlScripts.php:24
list($scope_folder, $scope_name) = explode("\\",
    str_replace("\\Scopes", "", str_replace("SHServ", "", static::class)));

Это ломается, если класс не в ControlScripts\Scopes\ (например, Vendor\ControlScripts\Scopes\X).


3.5. get_source_code() — хрупкая отражение

$ref_func = new \ReflectionFunction($func);
$file_name = $ref_func -> getFileName();
$start_line = $ref_func -> getStartLine();
// ...

Проблемы:

  • Если файл обновился после загрузки (git pull) — отображаемый код ≠ исполняемому.
  • Захватывает только closure body, не полную функцию.
  • Не работает, если closure создан динамически (eval, create_function).

3.6. Regular scripts без таймаута и без защиты от overlap

// CronController.php
foreach($regular_scripts as $alias => $script) {
    $this -> run_script_cli($alias);  // последовательный exec
}
  • Нет таймаута — если скрипт висит на HTTP, cron висит.
  • Нет flock/lock — если cron job запускается чаще, чем выполняется (например, каждую минуту), получаем pile-up процессов.
  • Последовательный запуск — скрипты независимы, но ждут друг друга.

3.7. Action scripts без таймаута и без sandbox

run_action_script() напрямую вызывает callable. Если внутри HTTP-запрос к offline-устройству — API endpoint зависает до таймаута PHP-FPM.


3.8. Нет dependency injection

protected function devices(): Devices {
    if(!$this->devices_model) {
        $this->devices_model = new Devices();  // жёстко зашито
    }
    return $this->devices_model;
}

Нельзя подменить Devices на mock в тестах. Нельзя подменить DeviceScriptsHelper.


Часть 4. DX-проблемы (P2) — разработка и поддержка

4.1. Sync map захардкожен в PHP-коде (Common trait)

Common::register_global_device_sync_map() содержит ~25 связей с конкретными alias'ами (spotlight_main_back_1, kitchen_buttons_1 и т.д.). Любое новое устройство = git commit + deploy.

Направление решения: Вынести sync map в JSON/YAML/DB-таблицу device_sync_map, редактируемую через UI. Scope должен читать её, а не хардкодить.


4.2. Нельзя создать Scope через UI

Сейчас Scope — это PHP-класс. Нельзя из веб-интерфейса нажать «Создать автоматизацию» и описать логику.

Направление решения: Rule Engine — хранить скрипты как JSON-структуры в БД (типа "IF event=X THEN action=Y"). Для сложного — оставить PHP Scope, для простого — Rule Engine.


4.3. Нет runtime-логов по скриптам

Есть глобальное логирование (logging()->info(...)), но нет таблицы script_runs с историей:

  • кто запустил action script (user_id)
  • с какими params
  • результат (success/failure)
  • execution time
  • stack trace при ошибке

3.4. Нет rollback / версионирования Scope

Если обновить PHP-файл Scope и он сломается — нет способа откатиться, кроме git revert. Нет таблицы scope_versions.


4.5. Документация устарела

docs/control-scripts-guide.md указывал путь server/ControlScripts/Scopes/исправлено, актуальный путь automation/Scopes/.


Часть 5. Функциональные пробелы (P3) — расширение возможностей

5.1. Нет conditional logic / rule engine

Сейчас только "event → action" 1:1. Нельзя выразить:

"Если датчик движения сработал И свет выключен И время между 22:00 и 06:00, то включить ночник на 30%"

Направление: Добавить Condition interface + composite conditions (AndCondition, TimeRangeCondition, DeviceStateCondition).


5.2. Нет state machine / modes

Нельзя переключать "режимы" системы: "Дома", "Не дома", "Ночь", "Отпуск". В каждом режиме — разная реакция на одно и то же событие.

Направление: Таблица system_modes + ModeContext, который Scope проверяет в handler'ах.


5.3. Нет timers / delays

Нельзя сделать "выключить свет через 5 минут" или "если дверь открыта более 2 минут — писать в лог".

Направление: Таблица script_timers (alias, fire_at, payload) + cron-обработчик GET /cron/script-timers.


5.4. Нет chaining / вызова action из action

Action script не может вызвать другой action script по alias. Приходится дублировать код.

// Хотелось бы:
$this->run_action_script("night_mode_on");

5.5. Нет retry logic

Если устройство offline и HTTP падает — скрипт падает. Нет retry(3, 500ms).


5.6. Нет параметризации action scripts через UI

Action scripts принимают $params, но нет JSON Schema для параметров. UI не знает, какие поля показать пользователю (input field, slider, select).

Направление: Добавить params_schema в add_action_script():

$this->add_action_script([
    "alias" => "dim_lights",
    "params_schema" => [
        ["name" => "brightness", "type" => "int", "min" => 0, "max" => 100, "default" => 50],
        ["name" => "room", "type" => "select", "options" => ["kitchen", "bedroom"]]
    ]
], function($params) { ... });

Часть 6. Предложенный план улучшений

Phase A. Critical fixes (1–2 дня)

  1. Починить regular scripts cron:
    • Вариант A: Добавить run-regular-script в console.php.
    • Вариант B (лучше): Запускать inline ControlScripts::run_regular_script() прямо в CronController, без CLI обёртки. Добавить try/catch + лог.
  2. Защитить event handlers: Обёрнуть $handler() в try/catch в add_event_handler(), логировать Exception через logging()->error('php:ControlScripts', ...).
  3. Починить документацию ✅ Выполнено — пути обновлены во всех документах.
  4. Добавить flock в cron: flock -n /var/lock/shserv-regular-scripts.lock php ...

Phase B. Architecture refactor (3–5 дней)

  1. Registry pattern: Создать ScriptsRegistry (не-static), положить в app(). Перенести $actions_scripts, $regular_scripts, $sync_map_storage туда. ControlScripts получает registry через constructor или app()->scripts_registry.
  2. Autodiscover scopes: Убрать scopes-manifest.json, искать классы по automation/Scopes/*.php автоматически.
  3. DI в базовом классе: __construct(Devices $devices, DeviceScriptsHelper $helper, ScriptsRegistry $registry) или фабрика.
  4. Таймауты: Добавить curl timeout в DeviceAPI\Base (сейчас, вероятно, default). Убедиться, что action script не висит >5s.

Phase C. Rule Engine + DB-driven sync map (1–2 недели)

  1. Sync map в БД: Таблица device_sync_map (id, relay_alias, relay_channel, button_alias, button_channel, area_id). Scope читает её. UI для редактирования.
  2. Simple Rule Engine (JSON-driven):
    • Таблица script_rules (id, name, event_pattern, conditions_json, actions_json, state, priority).
    • conditions_json: [{"type":"time_range","from":"22:00","to":"06:00"}, {"type":"device_state","alias":"kitchen_relay","channel":0,"state":"off"}]
    • actions_json: [{"type":"toggle","alias":"kitchen_relay","channel":0}]
    • Scope RuleEngineScope подписывается на * и исполняет matching rules.
  3. Параметризация action scripts: params_schema → отображение в UI Vue-клиента.

Phase D. Advanced features (2+ недели)

  1. Timers / delays: Таблица script_timers, cron endpoint.
  2. System modes: Таблица system_modes, API toggle, ModeContext в Scope.
  3. Script execution log: Таблица script_runs + UI history.
  4. Retry logic: DeviceAPI\Base с retry(3, 500ms) для POST /action.
  5. Scope versioning: Таблица scope_versions, сохранять source code перед обновлением, rollback.

Часть 7. Рекомендуемый приоритет

Приоритет Задача ROI Сложность
🔴 P0 Починить regular scripts cron Высокий Низкая
🔴 P0 try/catch в event handlers Высокий Низкая
🟡 P1 Registry вместо static Высокий Средняя
🟡 P1 Autodiscover scopes Средний Низкая
🟡 P1 Таймауты + overlap lock Высокий Низкая
🟢 P2 Sync map в БД Высокий Средняя
🟢 P2 Rule Engine (simple JSON) Очень высокий Средняя
🟢 P2 Script execution log Средний Низкая
Timers / delays Средний Средняя
🔵 P3 System modes Средний Средняя
🔵 P3 Params schema для UI Высокий Низкая

Приложение: Сводка файлов

Файл Роль Проблемы
automation/Scopes/*.php Scope-классы Хардкод alias'ов, нет версионирования
automation/Common.php Общие хелперы Sync map захардкожен
server/SHServ/Middleware/ControlScripts.php Базовый класс Static state, хрупкий парсинг имени, нет DI
server/SHServ/Controllers/CronController.php Cron Inline запуск regular scripts + timers, flock, timeout
server/SHServ/Middleware/ScriptsRegistry.php Registry Instance-based хранилище скриптов
server/SHServ/Models/Timers.php DB model Таймеры: create, cancel, get_pending, mark_status
server/SHServ/Routes/ScriptsRESTAPI_v1.php Роуты Добавлены POST /scopes/update и POST /timers/cancel
docs/control-scripts-guide.md Документация Актуализирована (пути, timers)