Newer
Older
smart-home-server / docs / architecture.md

Архитектура системы

Политика управления устройствами

Ни веб-клиент, ни сами умные устройства не имеют права напрямую управлять другими устройствами через внешний 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)]

Устройства (Firmware)

Каждое устройство — это Arduino-прошивка, собранная поверх библиотеки sh_core_esp8266.

Роль sh_core

devices/sh_core_esp8266/src/ — разделяемая библиотека для всех устройств. Она отвечает за:

  • WiFi подключение и AP-режим при первом запуске
  • HTTP-сервер и регистрацию стандартных роутов (/about, /status, /action, /setup, /set_token, /reboot, /reset, /set_device_name, /channels_schema, /set_channels_schema)
  • EEPROM: хранение SSID/пароля, токена, адреса сервера, имени устройства, схемы каналов
  • OTA-обновление
  • Универсальный POST-хелпер для отправки событий на сервер: 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 &paramsJson,
                        String &errorCode, String &errorMessage);

// Выполнить сброс к заводским настройкам
void deviceHandleReset();

И задать три глобальных константы:

const char* DEVICE_TYPE = "relay";   // тип устройства
const char* FW_VERSION  = "1.0";     // версия прошивки
const uint8_t CHANNEL_NUM = 8;       // число каналов (0 если не используются)

EEPROM layout

Адрес Длина Содержимое
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)

Каналы (channels schema)

Схема — 8 каналов × 4 байта:

  • байт 0: GPIO пин канала (SH_CH_PIN)
  • байт 1: GPIO пин индикатора или номер LED (SH_CH_INDICATOR)
  • байт 2: GPIO пин обратной связи / концевика (SH_CH_FEEDBACK)
  • байт 3: флаги — бит 0 (SH_CH_FLAG_INVERT) = инверсия канала

Значение 0xFF (SH_PIN_UNUSED) означает «пин не используется».

Режимы устройства

Режим Описание
setup Не подключено к серверу. /setup и /set_token доступны без токена.
normal Работает штатно. Все запросы требуют Authorization: Bearer <token>.
error Ошибка.
updating OTA-обновление.

При переходе setup → normal: устройство запоминает IP сервера и принимает авторизованные запросы только с этого адреса.


Сервер (SHServ)

PHP-приложение поверх собственного микрофреймворка Fury.

Fury framework

server/Fury/ содержит минималистичный MVC-фреймворк:

  • Bootstrap — точка входа, инициализирует приложение через events()
  • Router — маршрутизация через .uri(), .get(), .post()
  • ThinBuilder — простой SQL query builder поверх PDO
  • Events — простой pub/sub (events()->handler(name, cb), events()->app_call(name, data))
  • Template — шаблонизатор (используется мало)

Жизненный цикл запроса

  1. server/index.phpFury\Kernel\InitFury\Kernel\Bootstrap
  2. Bootstrap генерирует событие kernel:Bootstrap.ready_app
  3. EventsHandlers ловит событие → вызывает routes->routes_init()router->start_routing()
  4. Router находит совпадение и вызывает Controller@method

Структура SHServ

SHServ/
├── 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-логи по дням

Добавление устройства на сервер

  1. GET /api/v1/devices/scanning/setup — сервер параллельно опрашивает диапазон IP (из config.php: device_ip_range), возвращает устройства в режиме setup
  2. POST /api/v1/devices/setup/new-device — сервер:
    • Запрашивает /about у устройства
    • Создаёт запись в таблице devices
    • Генерирует токен, сохраняет в device_auth
    • Отправляет токен на устройство через POST /set_token
    • Устанавливает имя устройства через POST /set_device_name

DeviceAPI (Tools/DeviceAPI/)

Серверная сторона HTTP-клиентов к устройствам:

  • Base — базовые методы: get_about(), get_status(), post_action(), remote_set_token(), reboot(), reset(), set_device_name()
  • Relay extends Basetoggle_channel(ch), set_state(bool), set_channel_state(bool, ch)
  • Button extends Baseget_indicators(), get_indicator_state(ch), set_channel_state(mode, ch)
  • Sensor extends Base
  • Hatch extends Base

Device::device_api() создаёт нужный экземпляр по device_type и автоматически подставляет токен из device_auth.

Cron-маршруты

Два endpoint'а для запуска по cron:

URL Действие
GET /cron/regular-scripts Запускает все зарегистрированные regular-скрипты (с проверкой флага enabled в БД)
GET /cron/status-update-scanning Сканирует сеть, обновляет connection_status и device_ip устройств в БД

Система событий (Events от устройств)

Устройство отправляет событие на 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 + каналу (самый точный)

Control Scripts (ControlScripts/Scopes/)

Скрипты автоматизации — это PHP-классы в server/ControlScripts/Scopes/, наследующие ControlScripts и реализующие ControlScriptsInterface. Все Scope-файлы загружаются автоматически при старте приложения.

Структура 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) {
    // логика
});

Sync map

Декларативное описание связей «реле-канал ↔ кнопки-каналы» для синхронизации индикаторов:

$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

SmartHomeApi

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