diff --git a/CLAUDE.md b/CLAUDE.md index 7af1001..b5a2dca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,5 +120,6 @@ | `docs/architecture.md` | Full architecture: firmware contract, events routing, sync map, Fury framework | | `docs/firmware-dev-guide.md` | How to write firmware for a new device type | | `docs/control-scripts-guide.md` | How to write automation scripts (Scope classes) | +| `docs/virtual-device-emulator.md` | Virtual device emulator: Flask-based dev/test tool | | `webclient/docs/migration-plan.md` | Vue client migration plan (Phases 1–6) | | `webclient/docs/smoke-checklist.md` | UI smoke checklist for releases | diff --git a/docs/architecture.md b/docs/architecture.md index c5f978b..fc8ac44 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -77,6 +77,23 @@ | 97 | 64 | Device name | | 161 | 64 | Auth token | | 225 | 32 | Server base URL | + +### Виртуальный эмулятор (для разработки) + +Для локальной разработки и тестирования без реального железа используется виртуальный эмулятор на Python + Flask (`tools/virtual_devices/`). + +Эмулятор повторяет полный REST-контракт реальных устройств: +- `GET /about`, `GET /status`, `POST /action` +- `POST /set_token`, `POST /reset`, `POST /reboot` +- `GET /channels_schema`, `POST /set_channels_schema` + +Поддерживаемые типы: +- **relay** — управление каналами (`set_state: on/off`) +- **button** — генерация событий (`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`) | diff --git a/docs/virtual-device-emulator.md b/docs/virtual-device-emulator.md new file mode 100644 index 0000000..265f293 --- /dev/null +++ b/docs/virtual-device-emulator.md @@ -0,0 +1,313 @@ +# Virtual Device Emulator + +Руководство по виртуальному эмулятору устройств умного дома. Предназначен для локальной разработки и тестирования без реальных ESP8266/ESP32. + +## Содержание + +- [Назначение](#назначение) +- [Архитектура](#архитектура) +- [Подготовка](#подготовка) +- [Создание устройства](#создание-устройства) +- [Регистрация в сервере](#регистрация-в-сервере) +- [Управление через CLI](#управление-через-cli) +- [Web UI](#web-ui) +- [Интеграция с сервером](#интеграция-с-сервером) +- [Типы устройств](#типы-устройств) +- [Контракт эндпоинтов](#контракт-эндпоинтов) +- [End-to-end пример](#end-to-end-пример) +- [Ограничения](#ограничения) + +--- + +## Назначение + +Эмулятор позволяет: + +- Разрабатывать и тестировать `ControlScripts` (automation-логику) без физических устройств +- Проверять серверные API: provisioning (`/devices/setup/new-device`), управление (`/devices/action`), события (`/events/new`) +- Тестировать Vue-клиент: виртуальные устройства индistinguishable от реальных в UI +- Отлаживать end-to-end цепочки: кнопка → событие → скрипт → реле + +## Архитектура + +``` +┌─────────────────┐ HTTP ┌─────────────────┐ +│ Vue Client │◄───────────────►│ PHP Server │ +│ (browser) │ /api/v1/... │ (nginx+php-fpm)│ +└─────────────────┘ └─────────────────┘ + │ + │ POST /events/new + ▼ + ┌─────────────────────┐ + │ Virtual Device │ + │ (Flask, :9001) │ + │ - /about, /status │ + │ - /action │ + │ - /simulate-event │ + └─────────────────────┘ +``` + +### Компоненты + +| Компонент | Файл | Роль | +|-----------|------|------| +| CLI | `cli.py` | Создание, запуск, остановка, регистрация устройств | +| Emulator | `emulator.py` | Flask-сервер одного устройства | +| State Store | `state.py` | JSON-файлы `devices/.json` | +| Device Models | `device/{base,relay,button}.py` | Поведение типов устройств | +| Web UI | inline в `emulator.py` | HTML + JS для ручного управления | + +Каждое устройство — отдельный Flask-процесс на своём порту. Состояние персистентно между перезапусками. + +## Подготовка + +```bash +cd tools/virtual_devices +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +## Создание устройства + +```bash +python cli.py create \ + --type relay \ + --alias virt_relay \ + --name "Виртуальное реле" \ + --channels 4 \ + --port 9001 \ + --server-url http://smart-home-serv.local \ + --ip 127.0.0.1 +``` + +Параметры: + +| Флаг | Значение по умолчанию | Описание | +|------|----------------------|----------| +| `--type` | — | `relay` или `button` | +| `--alias` | — | Уникальный идентификатор (a-z0-9_) | +| `--name` | alias | Человекочитаемое имя | +| `--channels` | 4 | Количество каналов | +| `--port` | 9001 | HTTP-порт эмулятора | +| `--server-url` | http://localhost | URL PHP-сервера (для отправки событий) | +| `--ip` | 127.0.0.1 | IP, который устройство сообщает в `/about` | + +Создаётся файл `devices/virt_relay.json` с начальным состоянием (`status: setup`). + +## Регистрация в сервере + +Виртуальное устройство регистрируется через **стандартный API сервера**, как и реальное устройство: + +```bash +python cli.py start --alias virt_relay +python cli.py register --alias virt_relay --server-url http://smart-home-serv.local +``` + +Процесс регистрации: + +1. CLI вызывает `GET http://127.0.0.1:9001/about` — проверяет `status: setup` +2. CLI отправляет `POST /api/v1/devices/setup/new-device` на PHP-сервер +3. Сервер вызывает `GET /about` на эмуляторе +4. Сервер генерирует токен и вызывает `POST /set_token` на эмуляторе +5. Эмулятор переходит в `status: normal`, сохраняет токен +6. Сервер вставляет запись в `devices` и `device_auth` + +**Важно:** для поддержки нестандартных портов сервер разбирает `device_ip` как `host:port` и валидирует только IP-часть. + +## Управление через CLI + +```bash +# Список устройств и их статус +python cli.py list + +# Запустить эмулятор (background-процесс) +python cli.py start --alias virt_relay [--host 0.0.0.0] [--port 9001] + +# Остановить +python cli.py stop --alias virt_relay + +# Посмотреть JSON-состояние +python cli.py status --alias virt_relay + +# Нажать кнопку (для button-типа) +python cli.py click --alias virt_btn --channel 0 + +# Удалить состояние +python cli.py remove --alias virt_relay +``` + +## Web UI + +Каждый эмулятор отдаёт UI на `GET /`: + +``` +http://127.0.0.1:9001/ +``` + +### Relay +- Тумблеры **Turn ON / Turn OFF** для каждого канала +- Зелёная подсветка (`button.active`) — канал `on` +- Автообновление статуса каждые 2 секунды + +### Button +- Кнопки **Click** для каждого канала +- Последнее событие (`last_event`) и время (`last_event_time`) +- Автообновление статуса каждые 2 секунды + +## Интеграция с сервером + +### Сканирование + +Виртуальные устройства **не появляются** в `GET /api/v1/devices/scanning/all`, потому что `DeviceScanner` сканирует сетевые IP. Это by design — сканер для поиска физических устройств. + +Для регистрации используйте `POST /api/v1/devices/setup/new-device` напрямую (через CLI `register` или через UI). + +### Управление + +После регистрации виртуальное устройство неотличимо от реального: + +- `GET /api/v1/devices/id/{id}/status` — работает +- `POST /api/v1/devices/action` — работает +- `POST /api/v1/devices/{id}/reboot` — заглушка, возвращает `ok` +- `POST /api/v1/devices/{id}/reset` — сбрасывает эмулятор в `setup` + +### События + +При нажатии виртуальной кнопки: + +1. Эмулятор обновляет состояние канала (`last_event: click`) +2. Эмулятор шлёт `POST http://smart-home-serv.local/events/new`: + ```json + { + "event_name": "click", + "device_id": "virt-...", + "data": {"channel": 0} + } + ``` +3. PHP-сервер обрабатывает событие через `EventsController::new_event` +4. `EventsModel` вызывает зарегистрированные handlers в `ControlScripts` + +## Типы устройств + +### Relay + +- `device_type`: `relay` +- Действие: `set_state` (`params.state: "on" | "off"`) +- Статус: массив `channels` с полем `state` + +### Button + +- `device_type`: `button` +- Действие: `simulate_click` (`params.channel: int`) +- При клике шлёт `POST /events/new` на `server_url` +- Статус: массив `channels` с полями `last_event`, `last_event_time` + +## Контракт эндпоинтов + +Полная совместимость с `docs/device-spec.md`. + +| Method | Path | Auth | Request | Response | +|--------|------|------|---------|----------| +| GET | `/` | — | — | Web UI HTML | +| GET | `/about` | — | — | `{device_name, device_type, firmware_version, device_id, server, status, ip_address, mac_address, uptime}` | +| GET | `/status` | Bearer | — | `{channels: [...]}` | +| POST | `/action` | Bearer | `{action, params}` | `{status, message}` или `{status, error, message}` | +| POST | `/set_token` | — (setup) / Bearer (normal) | `{token}` | `{status, message}` | +| POST | `/reset` | Bearer | `{}` | `{status, message}` | +| POST | `/reboot` | Bearer | `{}` | `{status, message}` | +| POST | `/set_device_name` | Bearer | `{device_name}` | `{status, message}` | +| GET | `/channels_schema` | — (setup) / Bearer (normal) | — | `{status, schema: [int]}` | +| POST | `/set_channels_schema` | — (setup) / Bearer (normal) | `{schema: [int]}` | `{status, message}` | +| GET | `/setup` | — | — | `{status, message}` (только в setup) | +| POST | `/setup` | — | `{ssid, password}` | `{status, message}` (только в setup) | +| POST | `/simulate-event` | — | `{event_name, channel}` | `{status, message}` | + +## End-to-end пример + +Настройка кнопки, которая переключает реле через automation-скрипт. + +### 1. Создать устройства + +```bash +cd tools/virtual_devices +source .venv/bin/activate + +python cli.py create --type relay --alias lamp_relay --port 9001 +python cli.py create --type button --alias wall_btn --port 9002 +``` + +### 2. Запустить + +```bash +python cli.py start --alias lamp_relay +python cli.py start --alias wall_btn +``` + +### 3. Зарегистрировать + +```bash +python cli.py register --alias lamp_relay --server-url http://smart-home-serv.local +python cli.py register --alias wall_btn --server-url http://smart-home-serv.local +``` + +### 4. Написать скрипт + +В `automation/Scopes/MyTestScope.php`: + +```php +add_action_script("toggle_lamp", function($params) { + $devices_model = new \SHServ\Models\Devices(); + $relay = $devices_model->by_alias("lamp_relay"); + if($relay) { + $status = $relay->device_api()->get_status(); + $current = $status["channels"][0]["state"] ?? "off"; + $new = $current == "on" ? "off" : "on"; + $relay->device_api()->post_action("set_state", ["channel" => 0, "state" => $new]); + } + }); + } + + public function register_events_handlers() { + events()->handler("button@wall_btn.click", function($params) { + \SHServ\Middleware\ControlScripts::run_action_script("toggle_lamp", []); + }); + } +} +``` + +Обновить `automation/scopes-manifest.json`: +```json +{ + "scopes": ["LightHubScope", "OfficeRoomScope", "SpotlightsScope", "TestScriptsScope", "MyTestScope"] +} +``` + +### 5. Нажать кнопку + +```bash +python cli.py click --alias wall_btn --channel 0 +``` + +### 6. Проверить реле + +```bash +curl -s http://127.0.0.1:9001/status -H "Authorization: Bearer ..." | python3 -m json.tool +``` + +Канал 0 должен переключиться в `on`. + +## Ограничения + +- **Нет Wi-Fi конфигурации** — `/setup` — заглушка +- **Нет реальной перезагрузки** — `/reboot` возвращает `ok`, процесс не перезапускается +- **Best-effort события** — `requests.post` с `timeout=3`, ошибки игнорируются +- **Локальное хранилище** — JSON без шифрования, только для dev +- **Нет OTA** — невозможно обновить "прошивку" +- **Нет сетевых задержек** — ping ≈ 0 ms +- **Один процесс = одно устройство** — для множества устройств нужно несколько портов diff --git a/tools/virtual_devices/README.md b/tools/virtual_devices/README.md index 0d01358..a78670a 100644 --- a/tools/virtual_devices/README.md +++ b/tools/virtual_devices/README.md @@ -1,103 +1,221 @@ # Virtual Device Emulator -Эмуляторы устройств умного дома на Python + Flask. Полностью повторяют REST-контракт реальных ESP8266/ESP32 устройств. +Эмуляторы устройств умного дома на Python + Flask. Полностью повторяют REST-контракт реальных ESP8266/ESP32 устройств и позволяют разрабатывать и тестировать серверную логику без железа. -## Типы устройств +## Зачем нужен -- **relay** — 4 канала реле. Управление `set_state` (`on`/`off`). -- **button** — 4 канала кнопок. При клике шлёт событие `POST /events/new` на сервер. +- Разрабатывать automation-скрипты (`ControlScripts`) без реальных устройств +- Тестировать серверные API: `/api/v1/devices/setup/new-device`, `/devices/action`, `/events/new` +- Проверять end-to-end цепочку: виртуальная кнопка → событие на сервер → скрипт → виртуальное реле +- Отлаживать Vue-клиент: устройства отображаются в UI как настоящие ## Установка ```bash cd tools/virtual_devices python3 -m venv .venv -source .venv/bin/activate # или .venv\Scripts\activate на Windows +source .venv/bin/activate # .venv\Scripts\activate на Windows pip install -r requirements.txt ``` +## Быстрый старт + +```bash +# 1. Создать реле и кнопки +python cli.py create --type relay --alias virt_relay --name "Виртуальное реле" --port 9001 --server-url http://smart-home-serv.local +python cli.py create --type button --alias virt_btn --name "Виртуальные кнопки" --port 9002 --server-url http://smart-home-serv.local + +# 2. Запустить +python cli.py start --alias virt_relay +python cli.py start --alias virt_btn + +# 3. Зарегистрировать в сервере (они должны быть в режиме setup) +python cli.py register --alias virt_relay --server-url http://smart-home-serv.local +python cli.py register --alias virt_btn --server-url http://smart-home-serv.local + +# 4. Посмотреть список +python cli.py list + +# 5. Открыть Web UI +# http://127.0.0.1:9001/ — реле +# http://127.0.0.1:9002/ — кнопки +``` + ## CLI +### `create` +Создаёт файл состояния устройства в `devices/.json`. + ```bash -# Создать виртуальное реле -python cli.py create --type relay --alias virt_relay --name "Virtual Relay" --port 9001 - -# Создать виртуальные кнопки -python cli.py create --type button --alias virt_btn --name "Virtual Buttons" --port 9002 - -# Запустить эмулятор -python cli.py start --alias virt_relay -python cli.py start --alias virt_btn - -# Список устройств -python cli.py list - -# Остановить -python cli.py stop --alias virt_relay - -# Статус (JSON) -python cli.py status --alias virt_relay - -# Нажать кнопку (для button-типа) -python cli.py click --alias virt_btn --channel 0 - -# Удалить устройство -python cli.py remove --alias virt_relay +python cli.py create \ + --type relay|button \ + --alias virt_relay \ + --name "Виртуальное реле" \ + --channels 4 \ + --port 9001 \ + --server-url http://smart-home-serv.local ``` -## Регистрация в PHP-сервере +### `start` +Запускает Flask-эмулятор как background-процесс. ```bash -# Убедитесь, что сервер доступен по нужному URL +python cli.py start --alias virt_relay [--host 0.0.0.0] [--port 9001] +``` + +### `stop` +Убивает процесс по PID-файлу. + +```bash +python cli.py stop --alias virt_relay +``` + +### `list` +Показывает все виртуальные устройства и статус (`running` / `stopped`). + +```bash +python cli.py list +``` + +### `status` +Выводит JSON-состояние устройства (каналы, токен, режим и т.д.). + +```bash +python cli.py status --alias virt_relay +``` + +### `click` +Симулирует нажатие кнопки (для `button`-типа). Шлёт `POST /simulate-event` на эмулятор. + +```bash +python cli.py click --alias virt_btn --channel 0 +``` + +### `register` +Регистрирует устройство в PHP-сервере через стандартный API `POST /api/v1/devices/setup/new-device`. + +```bash python cli.py register --alias virt_relay --server-url http://smart-home-serv.local ``` -Команда `register`: -1. Проверяет `/about` эмулятора (должен быть `setup`) +Устройство должно быть в режиме `setup`. Команда: +1. Проверяет `/about` 2. Отправляет `POST /api/v1/devices/setup/new-device` на сервер -3. Сервер сам устанавливает токен и переводит устройство в `normal` +3. Сервер сам устанавливает токен (`/set_token`) и переводит устройство в `normal` + +### `remove` +Удаляет файл состояния. Устройство должно быть остановлено. + +```bash +python cli.py remove --alias virt_relay +``` ## Web UI -Каждый эмулятор предоставляет UI на корневом URL: -``` -http://127.0.0.1:9001/ +Каждый эмулятор отдаёт UI на корневом URL (`GET /`). + +### Relay +- Тумблеры **Turn ON / Turn OFF** для каждого канала +- Зелёная подсветка — канал `on` +- Автообновление статуса каждые 2 секунды + +### Button +- Кнопки **Click** для каждого канала +- Последнее событие и время +- Автообновление статуса каждые 2 секунды + +## Ручное управление через curl + +```bash +# /about — всегда без авторизации +curl http://127.0.0.1:9001/about | python3 -m json.tool + +# Установить токен (в режиме setup) +curl -X POST http://127.0.0.1:9001/set_token \ + -H "Content-Type: application/json" \ + -d '{"token":"debug123"}' + +# Получить статус (normal — требует Bearer) +curl http://127.0.0.1:9001/status \ + -H "Authorization: Bearer debug123" + +# Переключить реле +curl -X POST http://127.0.0.1:9001/action \ + -H "Authorization: Bearer debug123" \ + -H "Content-Type: application/json" \ + -d '{"action":"set_state","params":{"channel":0,"state":"on"}}' + +# Нажать кнопку +curl -X POST http://127.0.0.1:9002/simulate-event \ + -H "Content-Type: application/json" \ + -d '{"event_name":"click","channel":0}' + +# Сбросить в setup +curl -X POST http://127.0.0.1:9001/reset \ + -H "Authorization: Bearer debug123" ``` -- Для реле — тумблеры on/off -- Для кнопок — кнопки Click и лог событий -- Обновление каждые 2 секунды - -## Эндпоинты эмулятора +## Полный список эндпоинтов эмулятора | Method | Path | Auth | Описание | |--------|------|------|----------| +| GET | `/` | — | Web UI | | GET | `/about` | — | Информация об устройстве | | GET | `/status` | Bearer | Состояние каналов | | POST | `/action` | Bearer | Управление (`set_state`, `simulate_click`) | | POST | `/set_token` | — (setup) / Bearer (normal) | Установка токена | -| POST | `/reset` | Bearer | Сброс в setup | -| GET | `/channels_schema` | Bearer (или — в setup) | Схема каналов | +| POST | `/reset` | Bearer | Сброс в `setup`, очистка токена | +| POST | `/reboot` | Bearer | Stub — заглушка | +| POST | `/set_device_name` | Bearer | Смена имени | +| GET | `/channels_schema` | — (setup) / Bearer (normal) | Схема 8 каналов | +| POST | `/set_channels_schema` | — (setup) / Bearer (normal) | Задать схему | | POST | `/simulate-event` | — | Debug: ручной триггер события | ## Структура ``` virtual_devices/ -├── cli.py # Управление устройствами -├── emulator.py # Flask-приложение эмулятора -├── state.py # Хранение состояния (JSON) +├── cli.py # Click CLI — управление устройствами +├── emulator.py # Flask-приложение одного эмулятора +├── state.py # JSON-хранилище DeviceState ├── device/ -│ ├── base.py # Базовый класс устройства -│ ├── relay.py # Реле -│ └── button.py # Кнопки -├── templates/ -│ └── control.html # UI (inline в emulator.py) -├── devices/ # Состояния (*.json, .gitignore) -└── pids/ # PID-файлы запущенных процессов +│ ├── __init__.py +│ ├── base.py # Базовый класс: auth, channels_schema, reset +│ ├── relay.py # 4 канала, set_state(on|off) +│ └── button.py # 4 канала, trigger_click → POST /events/new +├── requirements.txt # Flask, requests, click +├── devices/ # *.json — состояния (в .gitignore) +├── pids/ # *.pid — PID файлы (в .gitignore) +└── .gitignore # .venv/, __pycache__/, *.pid ``` -## Изменения в сервере +## End-to-end пример: нажатие кнопки → сервер → реле -Для поддержки `IP:port` в `device_ip` при регистрации: -- `server/SHServ/Controllers/DevicesRESTAPIController.php` — валидация `setup_new_device` теперь разбирает `host:port` и валидирует только host. +```bash +# 1. Создать устройства +python cli.py create --type relay --alias relay_lamp --port 9001 --server-url http://smart-home-serv.local +python cli.py create --type button --alias btn_living --port 9002 --server-url http://smart-home-serv.local + +# 2. Запустить +python cli.py start --alias relay_lamp +python cli.py start --alias btn_living + +# 3. Зарегистрировать в сервере +python cli.py register --alias relay_lamp --server-url http://smart-home-serv.local +python cli.py register --alias btn_living --server-url http://smart-home-serv.local + +# 4. Нажать кнопку +python cli.py click --alias btn_living --channel 0 + +# 5. Проверить, что реле переключилось (если скрипт настроен) +curl -s http://127.0.0.1:9001/status -H "Authorization: Bearer debug123" | python3 -m json.tool +``` + +## Ограничения + +- Нет реальной Wi-Fi конфигурации (`/setup` — заглушка) +- `/reboot` — заглушка, процесс не перезапускается +- События от button — best-effort (`requests.post` с `timeout=3`, игнорируются ошибки) +- Состояние хранится в JSON-файле, без шифрования (только для локальной разработки) +- Нет OTA-обновлений прошивки +- Не эмулирует реальные задержки сети и потери пакетов