Ни веб-клиент, ни сами умные устройства не имеют права напрямую управлять другими устройствами через внешний 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 |
| 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": "<device_hard_id>",
"event_name": "button_press",
"data": { "channel": 0 }
}
Сервер (EventsController) ищет устройство по device_hard_id, проверяет авторизацию, немедленно отвечает 200 OK, а затем (после fastcgi_finish_request) вызывает EventsModel, который триггерит события через Fury Events в нескольких форматах:
| Паттерн события | Пример | Описание |
|---|---|---|
{event_name} |
button_press |
Глобальный — любое устройство |
{device_type}.{event_name} |
button.button_press |
По типу устройства |
{device_type}@{alias}.{event_name} |
button@kitchen_btns.button_press |
По alias устройства |
{device_type}({channel}).{event_name} |
button(2).button_press |
По типу + каналу |
{device_type}@{alias}({channel}).{event_name} |
button@kitchen_btns(2).button_press |
По alias + каналу (самый точный) |
Скрипты автоматизации — это PHP-классы в server/ControlScripts/Scopes/, наследующие ControlScripts и реализующие ControlScriptsInterface. Все Scope-файлы загружаются автоматически при старте приложения.
class 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 { ... }
}
Action-скрипты — вызываются вручную через API (POST /api/v1/scripts/actions/run):
$this->add_action_script([
"alias" => "my_action",
"name" => "Имя",
"icon" => '<i class="ph ph-lightbulb"></i>',
"author" => "Name"
], function($params) {
// логика
return ["result" => ...];
});
Regular-скрипты — запускаются по cron (GET /cron/regular-scripts):
$this->add_regular_script([
"alias" => "my_regular",
"name" => "Имя"
], function() {
// периодическая логика
});
Event-хендлеры — реакция на события от устройств:
$this->add_event_handler("button@{$alias}({$channel}).press", function(Device $device, array $data) {
// логика
});
Декларативное описание связей «реле-канал ↔ кнопки-каналы» для синхронизации индикаторов:
$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 и обновляет индикаторы кнопок при изменении состояния реле.
SPA на vanilla JS + esbuild. Роутинг через hash #!/route.
src/js/
├── index.js — init: SmartHomeApi, Screens, routes()
├── routes.js — привязка роутов к экранам
├── DataProvider.js — простое key-value хранилище состояния (window.DataProvider)
├── sh/SmartHomeApi.js — HTTP-клиент (fetch + callback-style)
│ └── modules/ — DevicesApi, ScriptsApi, AreasApi
└── components/
├── Screens.js — переключение экранов по hash
├── hud.js — верхний HUD с навигацией
├── modals.js — модальные окна
├── toasts.js — уведомления
└── screens/ — экраны devices, areas, scripts
Callback-style клиент. Все запросы идут через proxy.php (опция proxy_path):
const api = new SmartHomeApi({
base_url: API_BASEURL,
token: "YOUR_TOKEN",
proxy_path: "/proxy.php",
});
api.devices.list((err, data) => { ... });
api.scripts.run({ alias: "my_action" }, (err, data) => { ... });
Константа API_BASEURL инжектируется через esbuild при сборке (webclient/gulpfile.js).