Ни веб-клиент, ни сами умные устройства не имеют права напрямую управлять другими устройствами через внешний REST API.
Всё управление устройствами — только через скрипты (ControlScripts). Скрипты являются единственной точкой логики автоматизации. У них три механики взаимодействия с системой:
| Механика | Описание | Как запускается |
|---|---|---|
| Events | Реакция на события — в том числе события от устройств (button_press, presence_changed и т.д.) |
Автоматически, при получении события от устройства через POST /events/new |
| Regular | Периодические задачи | По cron через GET /cron/regular-scripts |
| Actions | Именованные операции, запускаемые явно | Через POST /api/v1/scripts/actions/run из клиента |
Это намеренная политика: бизнес-логика управления устройствами сосредоточена исключительно в ControlScripts/Scopes/, а не размазана по клиентскому коду или прошивкам.
[ESP8266/ESP32 устройства]
↕ HTTP REST (локальная сеть, только сервер инициирует)
[PHP-сервер SHServ]
↕ HTTP REST (proxy.php)
[Веб-клиент (JS/SCSS)]
Каждое устройство — это Arduino-прошивка, собранная поверх библиотеки sh_core_esp8266.
devices/sh_core_esp8266/src/ — разделяемая библиотека для всех устройств. Она отвечает за:
/about, /status, /action, /setup, /set_token, /reboot, /reset, /set_device_name, /channels_schema, /set_channels_schema)core_post_json_to_server()Каждый .ino-файл обязан реализовать 4 функции (они вызываются из sh_core):
// Добавить поля в JSON-ответ /status
void appendStatusJsonFields(String &json);
// Добавить поля в JSON-ответ /about
void appendAboutJsonFields(String &json);
// Обработать входящее действие /action
bool deviceHandleAction(const String &action, const String ¶msJson,
String &errorCode, String &errorMessage);
// Выполнить сброс к заводским настройкам
void deviceHandleReset();
И задать три глобальных константы:
const char* DEVICE_TYPE = "relay"; // тип устройства const char* FW_VERSION = "1.0"; // версия прошивки const uint8_t CHANNEL_NUM = 8; // число каналов (0 если не используются)
| Адрес | Длина | Содержимое |
|---|---|---|
| 0 | 32 | SSID |
| 32 | 64 | WiFi password |
| 96 | 1 | Device mode (0=setup, 1=normal, 2=error, 3=updating) |
| 97 | 64 | Device name |
| 161 | 64 | Auth token |
| 225 | 32 | Server base URL |
Для локальной разработки и тестирования без реального железа используется виртуальный эмулятор на Python + Flask (tools/virtual_devices/).
Эмулятор повторяет полный REST-контракт реальных устройств:
GET /about, GET /status, POST /actionPOST /set_token, POST /reset, POST /rebootGET /channels_schema, POST /set_channels_schemaПоддерживаемые типы:
set_state: on/off)POST /events/new на сервер)Регистрация в сервере происходит через стандартный API POST /api/v1/devices/setup/new-device. Сервер устанавливает токен через /set_token так же, как и для реального устройства.
Детальное руководство: docs/virtual-device-emulator.md. | 257 | 36 | Channel schema (4 meta + 32 data) | | 512–1023 | 512 | Свободно для устройства (DEVICE_EEPROM_START) |
Схема — 8 каналов × 4 байта:
SH_CH_PIN)SH_CH_INDICATOR)SH_CH_FEEDBACK)SH_CH_FLAG_INVERT) = инверсия каналаЗначение 0xFF (SH_PIN_UNUSED) означает «пин не используется».
| Режим | Описание |
|---|---|
setup |
Не подключено к серверу. /setup и /set_token доступны без токена. |
normal |
Работает штатно. Все запросы требуют Authorization: Bearer <token>. |
error |
Ошибка. |
updating |
OTA-обновление. |
При переходе setup → normal: устройство запоминает IP сервера и принимает авторизованные запросы только с этого адреса.
PHP-приложение поверх собственного микрофреймворка Fury.
server/Fury/ содержит минималистичный MVC-фреймворк:
events().uri(), .get(), .post()events()->handler(name, cb), events()->app_call(name, data))server/index.php → Fury\Kernel\Init → Fury\Kernel\Bootstrapkernel:Bootstrap.ready_appEventsHandlers ловит событие → вызывает routes->routes_init() → router->start_routing()Controller@methodSHServ/ ├── App.php — точка входа (new App()), инициализация ├── config.php — DB, IP-диапазон устройств, devmode ├── Routes.php — объединяет все trait-роуты ├── Routes/ — trait DevicesRESTAPI_v1, ScriptsRESTAPI_v1, AreasRESTAPI_v1, DevMode ├── Controllers/ — по одному контроллеру на группу роутов ├── Models/ — DB-слой (Devices, Areas, Scripts, EventsModel, ...) ├── Entities/ — объекты предметной области (Device, Area, Script, ...) ├── Middleware/ — базовые классы Controller, Model, Entity, ControlScripts ├── Helpers/ — DeviceScriptsHelper, MetaImplementation, Validator, ... ├── Tools/DeviceAPI/ — HTTP-клиенты к устройствам (Base, Relay, Button, Sensor, Hatch) ├── Tools/DeviceScanner.php — параллельное сканирование сети через curl_multi └── Logs/ — JSON-логи по дням
GET /api/v1/devices/scanning/setup — сервер параллельно опрашивает диапазон IP (из config.php: device_ip_range), возвращает устройства в режиме setupPOST /api/v1/devices/setup/new-device — сервер:
/about у устройстваdevicesdevice_authPOST /set_tokenPOST /set_device_nameСерверная сторона HTTP-клиентов к устройствам:
Base — базовые методы: get_about(), get_status(), post_action(), remote_set_token(), reboot(), reset(), set_device_name()Relay extends Base — toggle_channel(ch), set_state(bool), set_channel_state(bool, ch)Button extends Base — get_indicators(), get_indicator_state(ch), set_channel_state(mode, ch)Sensor extends BaseHatch extends BaseDevice::device_api() создаёт нужный экземпляр по device_type и автоматически подставляет токен из device_auth.
Два endpoint'а для запуска по cron:
| URL | Действие |
|---|---|
GET /cron/regular-scripts |
Запускает все зарегистрированные regular-скрипты (с проверкой флага enabled в БД) |
GET /cron/status-update-scanning |
Сканирует сеть, обновляет connection_status и device_ip устройств в БД |
Устройство отправляет событие на POST /events/new:
{
"device_id": "ecf0a1b5c9d74f9a8e294c1f67b0a8b9",
"event_name": "press",
"data": { "channel": 0 }
}
Требует Authorization: Bearer <device_token>.
[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 |
Точно: устройство + канал |
// 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.
Скрипты автоматизации — это PHP-классы (Scope), расположенные в server/ControlScripts/Scopes/ (или другом месте, если настроено иначе).
Все Scope-классы:
\SHServ\Middleware\ControlScripts\SHServ\Implements\ControlScriptsInterfaceclass MyScope extends \SHServ\Middleware\ControlScripts
implements \SHServ\Implements\ControlScriptsInterface {
public function register_sync_map(): void { ... }
public function register_events_handlers(): void { ... }
public function register_actions_scripts(): void { ... }
public function register_regular_scripts(): void { ... }
}
| Метод | Назначение | Когда вызывается |
|---|---|---|
register_sync_map() |
Связи реле ↔ кнопки для синхронизации индикаторов | При старте сервера |
register_events_handlers() |
Подписка на события от устройств | При старте сервера |
register_actions_scripts() |
Action-скрипты (ручной запуск) | При старте сервера |
register_regular_scripts() |
Regular-скрипты (периодические) | При старте сервера |
Action-скрипты — ручной запуск через POST /api/v1/scripts/actions/run:
$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);
return ["result" => true];
});
Regular-скрипты — периодический запуск через GET /cron/regular-scripts:
$this->add_regular_script([
"alias" => "check_door_sensor",
"name" => "Проверка датчика двери",
], function() {
// периодическая логика
});
Event-хендлеры — реакция на события от устройств:
// Конкретная кнопка, канал 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);
});
Декларативное описание связей «реле ↔ кнопки» для синхронизации индикаторов:
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, $relay_alias) — синхронизировать кнопки с релеsync_btn_channels($sync_map, $btn_alias) — синхронизировать индикаторы кнопки с релеПолное описание: docs/control-scripts-guide.md, docs/events-from-devices.md.
Современный SPA на Vue 3 + Pinia + Vite + vue-router (hash mode).
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
Команды:
cd webclient npm run dev # dev server с прокси на PHP npm run build # production build → dist/ npm test # Vitest тесты
Стилевые правила:
gnexus-ui-kit (.text-success, .badge-warning, и т.д.)--color-*) напрямую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
Команды:
cd webclient_legacy npm start # gulp: SCSS → CSS, JS bundle, live-reload