Newer
Older
smart-home-server / docs / virtual-device-emulator.md

Virtual Device Emulator

Руководство по виртуальному эмулятору устройств умного дома. Предназначен для локальной разработки и тестирования без реальных ESP8266/ESP32.

Содержание


Назначение

Эмулятор позволяет:

  • Разрабатывать и тестировать 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/<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

Процесс регистрации:

  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

# Список устройств и их статус
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:
    {
      "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. Создать устройства

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. Запустить

python cli.py start --alias lamp_relay
python cli.py start --alias wall_btn

3. Зарегистрировать

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
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"]
}

5. Нажать кнопку

python cli.py click --alias wall_btn --channel 0

6. Проверить реле

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
  • Один процесс = одно устройство — для множества устройств нужно несколько портов