246 lines
9.0 KiB
Markdown
246 lines
9.0 KiB
Markdown
# Ignis Core
|
||
|
||
Локальный FastAPI-сервер для WiZ-ламп: discovery, управление, группы, расписания, API-ключи, аудит и встроенная веб-морда.
|
||
|
||
## Что умеет
|
||
|
||
- искать лампы в локальной сети при старте, вручную и в фоне;
|
||
- управлять лампой или группой;
|
||
- мигать лампой (`blink`) и читать живой статус;
|
||
- хранить группы, ключи, события и расписания в SQLite;
|
||
- выполнять `one-shot` и `cron`-задачи через APScheduler;
|
||
- разделять доступ на `master`, `admin`, `guest`;
|
||
- отдавать встроенный web UI из `static/`;
|
||
- публиковать OpenAPI.
|
||
|
||
## Структура
|
||
|
||
- `main.py` — FastAPI, middleware, lifecycle, статика.
|
||
- `app/api/routes/` — HTTP API.
|
||
- `app/core/discovery.py` — discovery и выбор подсетей.
|
||
- `app/core/scheduler.py` — расписания и reconciliation.
|
||
- `app/core/state.py` — runtime-state устройств и групп.
|
||
- `app/models/` — SQLAlchemy и схемы данных.
|
||
- `static/` — встроенный UI.
|
||
- `deploy/` — `systemd`-деплой и пример env.
|
||
|
||
## Быстрый старт
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
source .venv/bin/activate
|
||
pip install -r requirements.txt
|
||
cp deploy/ignis-core.env.example .env
|
||
.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8000
|
||
```
|
||
|
||
- UI: `http://127.0.0.1:8000/`
|
||
- OpenAPI runtime: `http://127.0.0.1:8000/openapi.json`
|
||
- Коммитнутый экспорт: [openapi.json](openapi.json)
|
||
|
||
`python-dotenv` подхватывает `.env` автоматически.
|
||
|
||
## Конфиг
|
||
|
||
Минимум:
|
||
|
||
```env
|
||
IGNIS_API_KEY=change-me
|
||
IGNIS_INSTANCE_NAME=Home
|
||
APP_TIMEZONE=Asia/Novosibirsk
|
||
SCAN_NETWORK=192.168.0.0/24
|
||
IGNIS_DATABASE_URL=sqlite+aiosqlite:///./ignis.db
|
||
IGNIS_SYNC_DATABASE_URL=sqlite:///./ignis.db
|
||
```
|
||
|
||
Основные переменные:
|
||
|
||
- `IGNIS_API_KEY` — мастер-ключ. Без него сервер работает в `fail-closed` и защищённые маршруты отвечают `503`.
|
||
- `IGNIS_INSTANCE_NAME` — имя инстанса в UI и `GET /system/info`.
|
||
- `APP_TIMEZONE` — таймзона расписаний.
|
||
- `SCAN_NETWORK` — подсеть или список подсетей для discovery. На хостах с VPN или несколькими NIC лучше задавать явно.
|
||
- `DISCOVERY_INTERVAL_SECONDS` — период фонового refresh.
|
||
- `DISCOVERY_BACKGROUND_MISSING_THRESHOLD` — сколько циклов подряд лампа может не отвечать до удаления.
|
||
- `DISCOVERY_ENV_MIN_PREFIX_LEN` — минимально допустимая маска для `SCAN_NETWORK`.
|
||
- `DISCOVERY_AUTO_MIN_PREFIX_LEN` — минимально допустимая маска для auto-discovery.
|
||
- `EVENT_LOG_RETENTION_DAYS` — срок хранения event log.
|
||
- `IGNIS_PUBLIC_BASE_URL` — внешний URL, если сервер стоит за reverse proxy.
|
||
- `IGNIS_BUILD_VERSION`, `IGNIS_BUILD_DATE`, `IGNIS_GIT_SHA` — build metadata для диагностики.
|
||
|
||
Полный пример: [deploy/ignis-core.env.example](deploy/ignis-core.env.example)
|
||
|
||
## Discovery
|
||
|
||
- при старте выполняется `startup_refresh()`;
|
||
- `POST /devices/rescan` делает ручной refresh и сразу убирает оффлайн-устройства;
|
||
- фоновый refresh удаляет устройство только после `DISCOVERY_BACKGROUND_MISSING_THRESHOLD` подряд промахов;
|
||
- если `SCAN_NETWORK` пуст, сервер сам выбирает private IPv4 подсети и старается не лезть в `docker`, `tun`, `wg`, `tailscale` и похожие интерфейсы.
|
||
|
||
## Роли
|
||
|
||
Заголовок авторизации: `X-API-Key`
|
||
|
||
- `master` — значение `IGNIS_API_KEY`, полный доступ.
|
||
- `admin` — ключ из БД с `is_admin=true`, доступ к группам, рескану, расписаниям, stats и расширенному `system/info`.
|
||
- `guest` — управление светом и чтение безопасной части API.
|
||
|
||
`GET /auth/me` возвращает текущую роль и имя ключа.
|
||
|
||
## HTTP API
|
||
|
||
Основные группы маршрутов:
|
||
|
||
- `GET /devices`
|
||
- `GET /devices/groups`
|
||
- `GET /devices/scenes`
|
||
- `POST /devices/groups`
|
||
- `DELETE /devices/groups/{group_id}`
|
||
- `POST /devices/rescan`
|
||
- `POST /control/device/{device_id}`
|
||
- `POST /control/group/{group_id}`
|
||
- `POST /control/device/{device_id}/blink`
|
||
- `GET /control/device/{device_id}/status`
|
||
- `GET /control/group/{group_id}/status`
|
||
- `POST /schedules/once`
|
||
- `POST /schedules/cron`
|
||
- `GET /schedules/tasks`
|
||
- `DELETE /schedules/{job_id}`
|
||
- `GET /api-keys`
|
||
- `POST /api-keys`
|
||
- `POST /api-keys/revoke`
|
||
- `POST /api-keys/activate`
|
||
- `GET /stats/summary`
|
||
- `GET /stats/log`
|
||
- `GET /system/info`
|
||
|
||
Ролевые ограничения:
|
||
|
||
- `guest`: чтение устройств/групп/сцен, управление светом, `status`, `blink`, безопасный `system/info`.
|
||
- `admin`: всё выше плюс создание/удаление групп, `rescan`, расписания, stats/log, расширенный `system/info`.
|
||
- `master`: всё выше плюс управление API-ключами.
|
||
|
||
Формат основных тел:
|
||
|
||
```json
|
||
{"state": true}
|
||
```
|
||
|
||
```json
|
||
{"brightness": 60}
|
||
```
|
||
|
||
```json
|
||
{"temp": 3200}
|
||
```
|
||
|
||
```json
|
||
{"r": 255, "g": 180, "b": 120}
|
||
```
|
||
|
||
```json
|
||
{"scene": "Cozy"}
|
||
```
|
||
|
||
Валидация:
|
||
|
||
- `brightness`: `10..100`
|
||
- `temp`: `2200..6500`
|
||
- `r/g/b`: `0..255`
|
||
- можно передать только один режим из `scene`, `temp` или `rgb`
|
||
- `r/g/b` нужно передавать полной тройкой
|
||
- для `POST /schedules/once` нужно передать ровно одно из `run_at` или `hours_from_now`
|
||
|
||
Примеры:
|
||
|
||
```bash
|
||
curl -sS http://127.0.0.1:8000/control/group/bedroom \
|
||
-H 'X-API-Key: change-me' \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"state":true,"brightness":60}'
|
||
```
|
||
|
||
```bash
|
||
curl -sS http://127.0.0.1:8000/api-keys \
|
||
-H 'X-API-Key: change-me'
|
||
```
|
||
|
||
Замечания по ключам:
|
||
|
||
- `GET /api-keys` возвращает публичные `key_id`, а не полный секрет;
|
||
- `POST /api-keys` показывает полный секрет только один раз;
|
||
- `POST /api-keys/revoke` и `POST /api-keys/activate` принимают JSON вида `{"key":"<secret-or-public-id>"}`.
|
||
|
||
## Встроенный UI
|
||
|
||
UI лежит в `static/` и не требует отдельной сборки.
|
||
|
||
Что есть сейчас:
|
||
|
||
- вход по API-ключу;
|
||
- роль-зависимые вкладки;
|
||
- пульт групп со сценами, яркостью, температурой, цветом и таймером на 4 часа;
|
||
- список устройств и сборка групп;
|
||
- one-shot и cron;
|
||
- серверная вкладка с метаданными инстанса;
|
||
- аудит, stats и API-ключи для нужных ролей.
|
||
|
||
Свойства UI:
|
||
|
||
- использует только локальные ассеты;
|
||
- не использует `localStorage`;
|
||
- может хранить ключ только в `sessionStorage` текущей вкладки;
|
||
- гость не видит чувствительные поля `system/info`.
|
||
|
||
## Хранилище
|
||
|
||
SQLite-таблицы:
|
||
|
||
- `groups`
|
||
- `api_keys`
|
||
- `event_log`
|
||
- `schedules`
|
||
- `apscheduler_jobs`
|
||
- `devices`
|
||
|
||
Важно:
|
||
|
||
- онлайн-устройства и статусы живут в runtime-памяти процесса;
|
||
- таблица `devices` пока не является полноценным source of truth;
|
||
- миграций схемы нет, используется `Base.metadata.create_all()`.
|
||
|
||
## OpenAPI
|
||
|
||
Актуальная схема закоммичена в `openapi.json`.
|
||
|
||
Перегенерация:
|
||
|
||
```bash
|
||
.venv/bin/python -c 'import json, os, pathlib; os.environ.setdefault("IGNIS_API_KEY", "openapi-export"); from main import app; pathlib.Path("openapi.json").write_text(json.dumps(app.openapi(), ensure_ascii=False, indent=2) + "\n", encoding="utf-8")'
|
||
```
|
||
|
||
## Тесты
|
||
|
||
Основная команда:
|
||
|
||
```bash
|
||
timeout 120s .venv/bin/python -m unittest discover -s tests -v
|
||
```
|
||
|
||
Дополнительно:
|
||
|
||
```bash
|
||
.venv/bin/python -m compileall app tests main.py
|
||
node --check static/app.js
|
||
```
|
||
|
||
## Ограничения
|
||
|
||
- discovery всё ещё сетевой перебор внутри выбранных подсетей;
|
||
- runtime-state устройств живёт в памяти процесса;
|
||
- UI монолитный, без отдельного frontend-build step;
|
||
- stats — это аудит и summary, а не полноценная аналитика.
|
||
|
||
## License
|
||
|
||
[MIT](LICENSE)
|