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

Этот документ описывает систему событий от устройств — основной механизм реакции автоматизации на физические действия (нажатия кнопок, срабатывание датчиков и т.д.).

---

## 1. Как устройство отправляет событие

Устройство отправляет событие на сервер через `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 устройства)

---

## 2. Жизненный цикл события на сервере

```
[Устройство] 
    POST /events/new (device_id, event_name, data)
        ↓
[EventsController::new_event()]
    1. Находит устройство по device_hard_id
    2. Проверяет, что auth активен
    3. Логирует событие
    4. Обновляет last_contact и connection_status = "active"
    5. Отвечает 200 OK немедленно
    6. fastcgi_finish_request() — ответ отправлен, но скрипт продолжается
        ↓
[EventsModel] — триггерит 5 вариантов событий через Fury Events
```

**Почему 5 вариантов?**  
Одно физическое событие (например, нажатие кнопки) триггерит 5 разных имён событий — это позволяет подписываться на разном уровне детализации.

---

## 3. Пять паттернов имён событий

Для устройства типа `button` с alias `kitchen_btns`, канал 2, событие `press`:

| № | Паттерн | Имя события | Когда использовать |
|---|---------|-------------|-------------------|
| 1 | `{event_name}` | `press` | **Глобально** — любое устройство, любое нажатие |
| 2 | `{device_type}.{event_name}` | `button.press` | **По типу** — все кнопки системы |
| 3 | `{device_type}@{alias}.{event_name}` | `button@kitchen_btns.press` | **По alias** — конкретная кнопка (все каналы) |
| 4 | `{device_type}({ch}).{event_name}` | `button(2).press` | **По каналу** — все кнопки, канал 2 |
| 5 | `{device_type}@{alias}({ch}).{event_name}` | `button@kitchen_btns(2).press` | **Точно** — конкретная кнопка, конкретный канал |

---

## 4. Как подписаться на событие (Control Scripts)

В `register_events_handlers()` Scope-класса:

```php
<?php

namespace ControlScripts\Scopes;

class KitchenScope extends \SHServ\Middleware\ControlScripts 
                     implements \SHServ\Implements\ControlScriptsInterface {

    public function register_events_handlers(): void {
        
        // 1. Глобально: любая кнопка системы
        $this->add_event_handler("press", function(Device $device, array $data) {
            logging()->info('KitchenScope', 'Any button pressed', [
                'device' => $device->alias,
                'channel' => $data['channel'] ?? null
            ]);
        });

        // 2. Все кнопки типа button
        $this->add_event_handler("button.press", function(Device $device, array $data) {
            logging()->info('KitchenScope', 'Button pressed', ['device' => $device->alias]);
        });

        // 3. Конкретная кнопка (все каналы)
        $this->add_event_handler("button@kitchen_btns.press", function(Device $device, array $data) {
            logging()->info('KitchenScope', 'Kitchen button pressed');
        });

        // 4. Все кнопки, канал 2
        $this->add_event_handler("button(2).press", function(Device $device, array $data) {
            logging()->info('KitchenScope', 'Channel 2 pressed on some button');
        });

        // 5. Точно: кухня, канал 2
        $this->add_event_handler("button@kitchen_btns(2).press", function(Device $device, array $data) {
            // Самая частая форма — реакция на конкретную физическую кнопку
            $relay = $this->devices()->by_alias("kitchen_relay");
            $relay->device_api()->toggle_channel(0);
        });

        // 6. Устройство онлайн (например, после перезагрузки)
        $this->add_event_handler("button@kitchen_btns.online", function(Device $device, array $data) {
            // Синхронизировать индикаторы после появления кнопки в сети
            $this->helper()->sync_btn_channels($this->sync_map(), $device->alias);
        });
    }
}
```

**Параметры хендлера:**
- `Device $device` — объект устройства, отправившего событие (методы: `id()`, `alias`, `device_type`, `device_api()`, ...)
- `array $data` — данные от устройства (обычно `channel`, иногда дополнительные поля)

---

## 5. Известные имена событий (event_name)

### button
| event_name | Описание | data |
|------------|---------|------|
| `press` | Нажатие кнопки | `{ channel: 0 }` |
| `online` | Устройство вышло в сеть | `{}` |

### relay
| event_name | Описание | data |
|------------|---------|------|
| `limit_switch_activated` | Сработал концевик (опционально, если прошивка поддерживает) | `{ channel: 0, state: "open" }` |

### hatch
| event_name | Описание | data |
|------------|---------|------|
| `limit_switch_activated` | Сработал концевик закрытия | `{ channel: 0 }` |
| `calibration_failed` | Не удалась калибровка | `{ reason: "timeout" }` |

### sensor
| event_name | Описание | data |
|------------|---------|------|
| `presence_changed` | Изменение присутствия в помещении | `{ present: true }` |
| `online` | Датчик вышел в сеть | `{}` |

> **Примечание:** Конкретные `event_name` зависят от прошивки устройства. Смотрите документацию конкретного типа устройства в `docs/devices/`.

---

## 6. Как EventsModel триггерит события

Файл: `server/SHServ/Models/EventsModel.php`

```php
// 1. Если есть канал — триггерит alias+channel и type+channel
if(isset($data["channel"])) {
    $events_model->channel_alias_device_event_call($device, $event_name, $channel, $data);
    // → button@kitchen_btns(2).press
    $events_model->channel_device_event_call($device, $event_name, $channel, $data);
    // → button(2).press
}

// 2. Триггерит alias (все каналы)
$events_model->alias_device_event_call($device, $event_name, $data);
// → button@kitchen_btns.press

// 3. Триггерит по типу устройства
$events_model->global_device_event_call($device, $event_name, $data);
// → button.press

// 4. Триггерит глобально (любое устройство)
$events_model->global_any_device_event_call($device, $event_name, $data);
// → press
```

**Порядок важен:**  
События триггерятся от наиболее специфичного к наиболее общему. Если вы подписаны на `button@kitchen_btns(2).press` — сработает только этот хендлер. Если на `press` — сработает на любое нажатие в системе.

---

## 7. Асинхронность и fastcgi_finish_request

`EventsController` отвечает устройству **немедленно** (строки 48–62):

```php
$response = json_encode(['status' => 'ok']);
http_response_code(200);
header("Content-Type: application/json; charset=utf-8");
header("Content-Length: " . strlen($response));
header("Connection: close");
echo $response;

if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();  // ← ответ отправлен, устройство ушло
} else {
    ob_flush();
    flush();
}

// ← С этого момента обработчики работают асинхронно
$events_model->channel_alias_device_event_call(...);
```

**Что это значит для Control Scripts:**
- Обработчики событий **не блокируют** ответ устройству
- Обработчики могут занимать время (HTTP-запросы к другим устройствам, сложная логика)
- Устройство уже получило `200 OK` и считает событие доставленным
- Если обработчик упадёт с ошибкой — устройство не узнает (ошибка логируется на сервере)

---

## 8. Синхронизация индикаторов кнопок (Sync Map)

Sync Map — декларативное описание связей «реле ↔ кнопки» для автоматической синхронизации индикаторов.

### Регистрация связи

```php
public function register_sync_map(): void {
    $this->add_sync_connection([
        // Первый элемент — источник состояния (реле)
        ["type" => "relay",  "alias" => "kitchen_relay", "channel" => 0],
        // Остальные — кнопки, чьи индикаторы синхронизируются
        ["type" => "button", "alias" => "kitchen_btns",  "channel" => 1],
        ["type" => "button", "alias" => "hall_btns",     "channel" => 0],
    ]);
}
```

### Хелперы синхронизации

```php
// После переключения реле — синхронизировать все кнопки из sync_map
$relay_api->toggle_channel(0);
$this->helper()->sync_relay_to_btns($this->sync_map(), "kitchen_relay");

// При появлении кнопки онлайн — синхронизировать её индикаторы с реле
$this->add_event_handler("button@kitchen_btns.online", function(Device $device, array $data) {
    $this->helper()->sync_btn_channels($this->sync_map(), $device->alias);
});
```

**Методы DeviceScriptsHelper:**
| Метод | Описание |
|-------|---------|
| `sync_relay_to_btn_channel($relay_api, $btn_api, $relay_ch, $btn_ch)` | Синхронизировать один канал реле с одним каналом кнопки |
| `sync_relay_to_btns($sync_map, $relay_alias)` | Синхронизировать все реле с кнопками по sync_map |
| `sync_btn_channels($sync_map, $btn_alias)` | Синхронизировать индикаторы кнопки с реле по sync_map |
| `get_sync_entries_by_type($sync_map, $type)` | Получить все записи указанного типа из sync_map |
| `prepare_sync_map_by_alias($sync_map, $alias)` | Подготовить sync_map для указанного устройства |

---

## 9. Отладка событий

### Логи сервера

События логируются в `server/SHServ/Logs/`:

```json
{"level":"info","timestamp":"2026-06-08T14:23:45+03:00","scope":"php:Events","message":"Event received","device_id":12,"alias":"kitchen_btns","event_name":"press","channel":2}
```

### Проверка подписок

```bash
# Убедиться, что Scope загружен
curl http://smart-home-serv.local/api/v1/scripts/scopes/list | jq '.data.scopes[] | select(.name=="KitchenScope")'

# Проверить, что хендлер зарегистрирован (в логе при старте сервера)
grep "handler.*app:button@kitchen_btns" server/SHServ/Logs/*.log
```

---

## 10. Полный пример Scope-класса

```php
<?php

namespace ControlScripts\Scopes;

class LightHubScope extends \SHServ\Middleware\ControlScripts 
                      implements \SHServ\Implements\ControlScriptsInterface {

    public function register_sync_map(): void {
        $this->add_sync_connection([
            ["type" => "relay",  "alias" => "kitchen_relay", "channel" => 0],
            ["type" => "button", "alias" => "kitchen_btns",  "channel" => 1],
        ]);
    }

    public function register_events_handlers(): void {
        // Нажатие кнопки → переключить реле
        $this->add_event_handler("button@kitchen_btns(1).press", function(Device $device, array $data) {
            $relay = $this->devices()->by_alias("kitchen_relay");
            $relay_api = $relay->device_api();
            $relay_api->toggle_channel(0);
            
            // Синхронизировать индикаторы всех кнопок
            $this->helper()->sync_relay_to_btns($this->sync_map(), "kitchen_relay");
        });

        // Кнопка онлайн → синхронизировать индикатор
        $this->add_event_handler("button@kitchen_btns.online", function(Device $device, array $data) {
            $this->helper()->sync_btn_channels($this->sync_map(), $device->alias);
        });
    }

    public function register_actions_scripts(): void {
        $this->add_action_script([
            "alias" => "kitchen_light_toggle",
            "name"  => "Свет на кухне",
            "icon"  => '<i class="ph ph-lightbulb"></i>',
        ], function($params) {
            $relay = $this->devices()->by_alias("kitchen_relay");
            $relay->device_api()->toggle_channel(0);
            $this->helper()->sync_relay_to_btns($this->sync_map(), "kitchen_relay");
            return ["result" => true];
        });
    }

    public function register_regular_scripts(): void {
        // Не используется
    }
}
```

---

## См. также

- `docs/control-scripts-guide.md` — полное руководство по Control Scripts
- `docs/architecture.md` — общая архитектура системы
- `docs/devices/button.md` — спецификация устройства button
- `server/SHServ/Models/EventsModel.php` — исходный код триггеринга событий
- `server/SHServ/Helpers/DeviceScriptsHelper.php` — хелперы синхронизации
