Руководство по виртуальному эмулятору устройств умного дома. Предназначен для локальной разработки и тестирования без реальных ESP8266/ESP32.
Эмулятор позволяет:
ControlScripts (automation-логику) без физических устройств/devices/setup/new-device), управление (/devices/action), события (/events/new)┌─────────────────┐ 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/<alias>.json |
| Device Models | device/{base,relay,button}.py |
Поведение типов устройств |
| Web UI | inline в emulator.py |
HTML + JS для ручного управления |
Каждое устройство — отдельный Flask-процесс на своём порту. Состояние персистентно между перезапусками.
cd tools/virtual_devices python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt
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 сервера, как и реальное устройство:
python cli.py start --alias virt_relay python cli.py register --alias virt_relay --server-url http://smart-home-serv.local
Процесс регистрации:
GET http://127.0.0.1:9001/about — проверяет status: setupPOST /api/v1/devices/setup/new-device на PHP-серверGET /about на эмулятореPOST /set_token на эмулятореstatus: normal, сохраняет токенdevices и device_authВажно: для поддержки нестандартных портов сервер разбирает device_ip как host:port и валидирует только IP-часть.
# Список устройств и их статус 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
Каждый эмулятор отдаёт UI на GET /:
http://127.0.0.1:9001/
button.active) — канал onlast_event) и время (last_event_time)Виртуальные устройства не появляются в 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 — заглушка, возвращает okPOST /api/v1/devices/{id}/reset — сбрасывает эмулятор в setupПри нажатии виртуальной кнопки:
last_event: click)POST http://smart-home-serv.local/events/new:
{
"event_name": "click",
"device_id": "virt-...",
"data": {"channel": 0}
}EventsController::new_eventEventsModel вызывает зарегистрированные handlers в ControlScriptsdevice_type: relayset_state (params.state: "on" | "off")channels с полем statedevice_type: buttonsimulate_click (params.channel: int)POST /events/new на server_urlchannels с полями last_event, last_event_timeПолная совместимость с docs/device-spec.md.
| Method | Path | Auth | Request | Response |
|---|---|---|---|---|
| GET | / |
— | — | Web UI HTML |
| GET | /about |
— | — | {device_name, device_type, platform, firmware_version, core_version, device_id, server, status, ip_address, mac_address, uptime, channels} |
| 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) |
| GET | /update |
— | — | HTML форма загрузки прошивки |
| POST | /update |
— | multipart/form-data с полем firmware |
Update OK! Rebooting... |
| POST | /simulate-event |
— | {event_name, channel} |
{status, message} |
Настройка кнопки, которая переключает реле через automation-скрипт.
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
python cli.py start --alias lamp_relay python cli.py start --alias wall_btn
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
В automation/Scopes/MyTestScope.php:
<?php
namespace ControlScripts\Scopes;
class MyTestScope extends \SHServ\Middleware\ControlScripts {
public function register_actions_scripts() {
$this->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:
{
"scopes": ["LightHubScope", "OfficeRoomScope", "SpotlightsScope", "TestScriptsScope", "MyTestScope"]
}
python cli.py click --alias wall_btn --channel 0
curl -s http://127.0.0.1:9001/status -H "Authorization: Bearer ..." | python3 -m json.tool
Канал 0 должен переключиться в on.
/setup — заглушка/reboot возвращает ok, процесс не перезапускаетсяrequests.post с timeout=3, ошибки игнорируются