# 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/<alias>.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, 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}` |

## 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
<?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", []);
        });
    }
}
```

Поместить файл Scope в `automation/Scopes/` — он будет автоматически обнаружен при старте сервера (autodiscover по `glob(automation/Scopes/*.php)` + проверка `implements ControlScriptsInterface`).

> Ранее требовался `automation/scopes-manifest.json` — он удалён, autodiscover реализован.

### 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
- **Нет сетевых задержек** — ping ≈ 0 ms
- **Один процесс = одно устройство** — для множества устройств нужно несколько портов
