# REST API сервера — справочник

Веб-клиент общается с сервером по HTTP+JSON. Все API-запросы идут через `proxy.php` (CORS).

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

Клиент **не управляет устройствами напрямую**. Вся логика — только через скрипты (`ControlScripts`):
- управление устройствами — через **action-скрипты** (`POST /api/v1/scripts/actions/run`)
- реакция на физические события — через **event-хендлеры** в скриптах
- периодические задачи — через **regular-скрипты** (cron)

Прямые вызовы устройств (`/api/v1/devices/action` и т.п.) — служебные endpoint'ы сервера для внутреннего использования, не предназначены для вызова из клиентского кода.

---

## Формат ответов

Все endpoint'ы используют единый формат из `Utils::response_success` / `Utils::response_error`.

**Успех:**
```json
{
  "status": true,
  "data": { ... }
}
```

**Ошибка:**
```json
{
  "status": false,
  "error_alias": "device_not_found",
  "failed_fields": ["device_id"],
  "msg": "Устройство не найдено"
}
```

> **Примечание.** Все endpoint'ы требуют авторизации через `Authorization: Bearer <token>` (JWT access token). Проверка прав реализована через `AuthControllerTrait::require_permission()`. Для публичных endpoint'ов (например, OAuth callback) используется отдельная middleware.

---

## Устройства `/api/v1/devices`

### `GET /api/v1/devices/list`
Список всех активных устройств.

**Ответ:**
```json
{
  "status": true,
  "data": {
    "devices": [
      {
        "id": 12,
        "alias": "kitchen_relay",
        "device_type": "relay",
        "device_hard_id": "ecf0a1b5c9d7...",
        "device_mac": "A4:CF:12:9B:3F:D2",
        "device_ip": "192.168.2.42",
        "firmware_version": "1.22 dev",
        "name": "Реле кухни",
        "status": "active",
        "connection_status": "active",
        "last_contact": "2026-04-22 18:35",
        "create_at": "2025-12-01 10:00"
      }
    ],
    "total": 1
  }
}
```

---

### `GET /api/v1/devices/id/{id}`
Данные одного устройства из БД (без запроса к самому устройству).

**Ответ:** `data.device` — поля устройства (те же что в списке выше).

---

### `GET /api/v1/devices/id/{id}/info`
Данные устройства + живой ответ `/about` с самого устройства.

**Ответ:**
```json
{
  "status": true,
  "data": {
    "device": {
      "id": 12,
      "alias": "...",
      "name": "...",
      "description": "...",
      "status": "active",
      "connection_status": "active",
      "last_contact": "...",
      "create_at": "...",
      "device": { /* ответ GET /about с устройства */ }
    }
  }
}
```

---

### `GET /api/v1/devices/id/{id}/status`
Живое состояние устройства — проксирует `GET /status` на само устройство.

**Ответ:**
```json
{
  "status": true,
  "data": {
    "device": {
      "id": 12,
      "alias": "...",
      "device_response": { /* ответ GET /status с устройства */ }
    }
  }
}
```

---

### `GET /api/v1/devices/scanning/setup`
Сканировать сеть и вернуть только устройства в режиме `setup` (ещё не добавленные).  
Параллельный curl_multi по диапазону IP из конфига (`192.168.2.2–192.168.2.254`).

**Ответ:** `data.devices` — массив ответов `/about` найденных устройств.

---

### `GET /api/v1/devices/scanning/all`
Сканировать сеть и вернуть все найденные устройства (любой статус).

---

### `POST /api/v1/devices/setup/new-device`
Добавить устройство. Сервер запрашивает `/about`, сохраняет в БД, генерирует токен, отправляет его устройству через `/set_token`, задаёт имя через `/set_device_name`.

**Тело:**
```json
{
  "device_ip": "192.168.2.42",
  "alias": "kitchen_relay",
  "name": "Реле кухни",
  "description": "..."
}
```

**Ответ (успех):** `data.device` — объект нового устройства.

**Ответ (ошибка):** `error_alias` — `invalid_ip` | `empty_field` | `alias_already_exists` | `device_not_found` | `device_mode_error` | `db_error`

---

### `POST /api/v1/devices/action`
Выполнить действие на устройстве — проксирует `POST /action` на само устройство.

**Тело:**
```json
{
  "device_id": 12,
  "action": "toggle_channel",
  "params": { "channel": 0 }
}
```

**Ответ (успех):**
```json
{
  "status": true,
  "data": {
    "device": {
      "id": 12,
      "alias": "...",
      "device_response": { /* ответ устройства */ }
    }
  }
}
```

---

### `POST /api/v1/devices/update-name`
Обновить имя устройства (в БД + на самом устройстве через `/set_device_name`).

**Тело:** `{ "device_id": 12, "name": "Новое имя" }`

---

### `POST /api/v1/devices/update-description`
Обновить описание устройства (только в БД).

**Тело:** `{ "device_id": 12, "description": "..." }`

---

### `POST /api/v1/devices/update-alias`
Обновить alias устройства (только в БД). **Осторожно:** сломает скрипты, использующие старый alias.

**Тело:** `{ "device_id": 12, "new_alias": "new_alias_name" }`

---

### `POST /api/v1/devices/place-in-area`
Поместить устройство в область.

**Тело:** `{ "target_id": 12, "place_in_area_id": 3 }`

---

### `GET /api/v1/devices/id/{id}/unassign-from-area`
Отвязать устройство от области (сделать «без области»).

---

### `POST /api/v1/devices/resetup`
Переустановить токен устройства (перепривязка).

**Тело:** `{ "device_id": 12 }`

---

### `POST /api/v1/devices/reset`
Сбросить устройство к заводским настройкам (вызывает `POST /reset` на устройстве).

**Тело:** `{ "device_id": 12 }`

---

### `GET /api/v1/devices/id/{id}/reboot`
Перезагрузить устройство.

---

### `GET /api/v1/devices/id/{id}/remove`
Удалить устройство: сбрасывает устройство (`POST /reset`), деактивирует токен, помечает запись как `removed` в БД.

---

## Области `/api/v1/areas`

Области — иерархическая структура физических пространств (комнаты, этажи, здания). Устройства и скрипты можно размещать в областях.

### `GET /api/v1/areas/list`
Список всех областей.

**Ответ:** `data.areas` — массив объектов AREA, `data.total`.

```json
{
  "id": 2,
  "type": "room",
  "alias": "kitchen",
  "display_name": "Кухня",
  "parent_area_id": 0
}
```

---

### `GET /api/v1/areas/id/{area_id}/list`
Список дочерних областей указанной области.

---

### `POST /api/v1/areas/new-area`
Создать новую область.

**Тело:** `{ "type": "room", "alias": "kitchen", "display_name": "Кухня" }`

**Ответ:** `data.alias`, `data.area`.

**Ошибки:** `alias_already_exists` | `empty_field` (поля `type`, `display_name`)

---

### `GET /api/v1/areas/id/{area_id}/remove`
Удалить область. Все устройства и дочерние области внутри — отвязываются (но не удаляются). Нельзя удалить area_id ≤ 1.

---

### `POST /api/v1/areas/place-in-area`
Вложить одну область в другую.

**Тело:** `{ "target_id": 5, "place_in_area_id": 2 }`

---

### `GET /api/v1/areas/id/{area_id}/unassign-from-area`
Отвязать область от родительской (поднять на верхний уровень).

---

### `POST /api/v1/areas/update-display-name`
Переименовать область.

**Тело:** `{ "area_id": 2, "display_name": "Кухня (1 этаж)" }`

---

### `POST /api/v1/areas/update-alias`
Изменить alias области. **Осторожно:** сломает скрипты, использующие старый alias.

**Тело:** `{ "area_id": 2, "new_alias": "kitchen_floor_1" }`

---

### `GET /api/v1/areas/id/{area_id}/devices`
Список устройств в области (включая вложенные области).

**Ответ:** `data.devices` — массив устройств, `data.total`.

---

### `GET /api/v1/areas/id/{area_id}/scripts`
Список скриптов, размещённых в области (action + regular).

**Ответ:** `data.scripts`, `data.total`.

---

### `GET /api/v1/areas/id/{area_id}/reboot_devices`
### `GET /api/v1/areas/reboot_devices`
Перезагрузить все устройства в области (или все устройства системы).

**Ответ:**
```json
{
  "status": true,
  "data": {
    "results": {
      "kitchen:relay_1": { /* ответ от устройства */ }
    },
    "total": 1
  }
}
```

---

### `GET /api/v1/areas/types/list`
Список всех существующих типов областей в БД (для подсказок при создании новой).

**Ответ:** `data.types` — массив строк, например `["room", "floor", "building"]`.

---

## Скрипты `/api/v1/scripts`

Управление тремя типами скриптов: **action** (ручной запуск), **regular** (cron), **scope** (PHP-класс, контейнер для action + regular).

### `GET /api/v1/scripts/actions/list`
Список всех зарегистрированных action-скриптов с их состоянием (enabled/disabled).

**Ответ:**
```json
{
  "status": true,
  "data": {
    "scripts": [
      {
        "alias": "kitchen_light_toggle",
        "name": "Свет на кухне",
        "icon": "<i class=\"ph ph-lightbulb\"></i>",
        "description": "...",
        "author": "Eugene Sukhodolskiy",
        "state": "enabled",
        "filename": "LightHubScope.php",
        "path": "/srv/http/smart-home-serv.local/automation/Scopes",
        "params_schema": {
          "level": {
            "type": "range",
            "label": "Яркость, %",
            "min": 0,
            "max": 100,
            "default": 50
          }
        }
      }
    ],
    "total": 3
  }
}
```

---

### `GET /api/v1/scripts/regular/list`
Список всех зарегистрированных regular-скриптов.

---

### `GET /api/v1/scripts/scopes/list`
Список всех Scope-классов с их состоянием.

**Ответ:** `data.scopes` — массив `{ name, filename, state, path }`, `data.total`.

---

### `GET /api/v1/scripts/scopes/name/{name}`
Получить исходный код PHP-файла Scope. Возвращает raw PHP-код (не JSON).

---

### `POST /api/v1/scripts/actions/run`
Запустить action-скрипт вручную. Если скрипт имеет `params_schema`, параметры валидируются перед вызовом closure. Отсутствующие опциональные поля заполняются `default` из schema.

**Тело (без параметров):**
```json
{
  "alias": "kitchen_light_toggle",
  "params": {}
}
```

**Тело (с параметрами):**
```json
{
  "alias": "dim_lights",
  "params": {
    "level": 80,
    "room": "kitchen",
    "instant": true
  }
}
```

**Ответ:**
```json
{
  "status": true,
  "data": {
    "return": {
      "result": { /* возвращаемое скриптом */ },
      "exec_time": "0.042 seconds"
    }
  }
}
```

**Ошибка:** `action_script_not_found` (если alias не существует или скрипт disabled)

---

### `GET /api/v1/scripts/actions/alias/{alias}/enable`
### `GET /api/v1/scripts/actions/alias/{alias}/disable`
Включить / выключить action-скрипт (записывает состояние в БД).

---

### `GET /api/v1/scripts/regular/alias/{alias}/enable`
### `GET /api/v1/scripts/regular/alias/{alias}/disable`
Включить / выключить regular-скрипт.

---

### `GET /api/v1/scripts/actions/scope/{name}/enable`
### `GET /api/v1/scripts/actions/scope/{name}/disable`
Включить / выключить весь Scope (все его скрипты перестают регистрироваться при следующем старте сервера).

---

### `POST /api/v1/scripts/scopes/update`
Перезаписать содержимое PHP-файла Scope. Перед записью выполняется `php -l` (проверка синтаксиса), валидация namespace и имени класса. Существующий файл бэкапится в `.bak.{timestamp}`.

**Тело:** `{ "name": "LightHubScope", "source": "<?php ..." }`

---

### `POST /api/v1/scripts/place-in-area`
Поместить скрипт в область.

**Тело:** `{ "target_id": 5, "place_in_area_id": 2 }`

---

### `GET /api/v1/scripts/id/{id}/unassign-from-area`
Отвязать скрипт от области.

---

## Общее

### `GET /api/v1/status`
Статус сервера.

```json
{
  "status": "active",
  "docs": "https://git.gnexus.space/root/smart-home-server/tree/master/docs"
}
```

---

## События от устройств

### `POST /events/new`

**Назначение:** Принимать события от устройств (не для клиентского кода).

**Тело запроса (от устройства):**
```json
{
  "device_id": "ecf0a1b5c9d74f9a8e294c1f67b0a8b9",
  "event_name": "press",
  "data": { "channel": 0 }
}
```

**Требования:**
- Заголовок `Authorization: Bearer <device_token>` (токен из `device_auth`)
- `device_id` — `device_hard_id` устройства (уникальный ID из БД)

**Ответ:**
```json
{
  "status": "ok"
}
```

**Жизненный цикл:**
1. `EventsController` находит устройство по `device_hard_id`
2. Проверяет, что `device_auth` активен
3. Логирует событие (`php:Events`)
4. Обновляет `last_contact` и `connection_status = "active"`
5. **Немедленно отвечает 200 OK**
6. `fastcgi_finish_request()` — ответ отправлен
7. Асинхронно: `EventsModel` триггерит 5 вариантов событий через Fury Events

**Пять паттернов событий (триггерятся одновременно):**

| Паттерн | Пример имени | Описание |
|---------|-------------|---------|
| `{event_name}` | `press` | Глобально — любое устройство |
| `{device_type}.{event_name}` | `button.press` | Все устройства типа |
| `{device_type}@{alias}.{event_name}` | `button@kitchen_btns.press` | Конкретное устройство |
| `{device_type}({channel}).{event_name}` | `button(2).press` | Все устройства, канал 2 |
| `{device_type}@{alias}({channel}).{event_name}` | `button@kitchen_btns(2).press` | Точно: устройство + канал |

**Обработчики событий:**  
Control Scripts подписываются через `$this->add_event_handler()` в `register_events_handlers()`. Обработчики вызываются после ответа устройству (асинхронно).

Полное описание: `docs/events-from-devices.md`.

---

### `GET /api/v1/scripts/timers/list`
Список таймеров (все статусы).

---

### `POST /api/v1/scripts/timers/cancel`
Отменить pending-таймер по `timer_alias`.

**Тело:** `{ "timer_alias": "kitchen_auto_off" }`

---

## Моды `/api/v1/modes`

Системные моды — boolean-теги текущего контекста (не взаимоисключающие).

### `GET /api/v1/modes/list`
Список всех модов с метаданными и состоянием.

**Ответ:**
```json
{
  "status": true,
  "data": {
    "modes": [
      {
        "tag": "home",
        "label": "Дома",
        "description": "Основной режим присутствия...",
        "is_active": true,
        "updated_at": "2026-06-08 14:30:00"
      },
      {
        "tag": "away",
        "label": "Не дома",
        "description": "Никого нет дома...",
        "is_active": false,
        "updated_at": "2026-06-08 14:30:00"
      }
    ],
    "total": 5
  }
}
```

---

### `GET /api/v1/modes/active`
Массив активных тегов.

**Ответ:**
```json
{
  "status": true,
  "data": {
    "active": ["away", "sleep"]
  }
}
```

---

### `POST /api/v1/modes/{tag}/enable`
### `POST /api/v1/modes/{tag}/disable`
Активировать / деактивировать мод.

**Ошибка:** `invalid_alias` (если тег не соответствует `^[a-z0-9_]+$`).

---

## Cron-маршруты

Не для клиента — вызываются планировщиком на сервере (только localhost).

| Endpoint | Действие |
|----------|---------|
| `GET /cron/regular-scripts` | Запустить все enabled regular-скрипты (inline, с flock) |
| `GET /cron/timers` | Выполнить pending-таймеры, у которых `execute_at` наступил |
| `GET /cron/status-update-scanning` | Сканировать сеть, обновить `connection_status` и `device_ip` в БД |

---

## Прошивки `/api/v1/firmwares`

### `GET /api/v1/firmwares`
Список всех прошивок в каталоге (in-memory, сканирует ZIP-файлы в `firmwares_dir`).

**Ответ:**
```json
{
  "status": true,
  "data": {
    "firmwares": [
      {
        "id": "relay-1.3.0-esp8266-x8",
        "device_type": "relay",
        "platform": "esp8266",
        "channels": 8,
        "version": "1.3.0",
        "core_version": "1.5.0",
        "bin_filename": "relay.ino.bin",
        "description": "...",
        "changelog": "..."
      }
    ],
    "total": 1
  }
}
```

---

### `GET /api/v1/firmwares/id/{id}`
Детали одной прошивки по ID манифеста.

---

### `GET /api/v1/firmwares/id/{id}/download`
Скачать бинарный файл прошивки (`Content-Type: application/octet-stream`).

---

### `POST /api/v1/firmwares/refresh`
Принудительно пересканировать директорию с прошивками и обновить in-memory кэш.

**Ответ:** `{status: true}`

---

### `GET /api/v1/devices/id/{id}/firmware-compatibility`
Проверить совместимость устройства с прошивками в каталоге. Сервер запрашивает устройство `GET /about` и фильтрует каталог по `device_type`, `platform`, `channels`, и версии (только более новые).

**Ответ:**
```json
{
  "status": true,
  "data": {
    "compatible": [
      { "id": "relay-1.3.0-esp8266-x8", "version": "1.3.0", "description": "...", "changelog": "..." }
    ],
    "current_version": "1.22.0",
    "current_platform": "esp8266",
    "current_channels": 8
  }
}
```

---

### `POST /api/v1/devices/update-firmware`
Запустить OTA-обновление устройства. Сервер распаковывает `.bin` из ZIP, проверяет совместимость, отправляет файл на устройство через `POST /update` (multipart/form-data).

**Тело:**
```json
{
  "device_id": 12,
  "firmware_id": "relay-1.3.0-esp8266-x8"
}
```

**Ответ (успех):** `{status: true, data: {device_msg: "Update OK! Rebooting..."}}`

**Ошибки:** `device_not_found` | `firmware_not_found` | `firmware_not_compatible` | `device_request_fail` | `ota_failed`

---

## Запланировано, не реализовано

Следующие разделы описаны в спецификациях (`docs/server-api-v1/`), но ещё не реализованы:

| Раздел | Файл спеки |
|--------|-----------|
| Логи | `logs.md` |
| Уведомления | `notifications.md` |
