Platform driver architecture (easy-aso supervisor)
This document describes where the VOLTTRON-inspired platform driver (dynamic devices/points, polling, config store) fits into easy-aso, and how components interact. It is intentionally smaller than a full BAS stack: asyncio-first, SQLite for configuration, in-process polling tasks, and pluggable drivers (BACnet first).
Current codebase (anchors)
| Area | Location | Role today |
|---|---|---|
| BACnet abstraction | easy_aso/bacnet_client/ (BacnetClient, JsonRpcBacnetClient) | Read/write/RPM against diy-bacnet-server or direct bacpypes |
| HTTP gateway (BACnet socket owner) | easy_aso/gateway/app.py | Legacy REST shim around BacpypesClient |
| Agent loops | easy_aso/agents/*.py | Example long-running consumers using env + factory |
| RPC-docked agents (sidecars) | easy_aso/runtime/ (RpcDockedEasyASO, easy-aso-agent CLI) | Many EasyASO processes sharing one JSON-RPC BACnet gateway |
| Core ASO lifecycle | easy_aso/easy_aso.py | User subclasses EasyASO for algorithms |
The supervisor does not replace EasyASO; it feeds future algorithms and MQTT publishers with a normalized point cache and CRUD configuration.
Target package layout
New top-level package: easy_aso/supervisor/ (keeps gateway and agents unchanged).
easy_aso/supervisor/
app.py # FastAPI app + lifespan (starts/stops runtime)
api/schemas.py # Pydantic request/response models
api/routes.py # CRUD + health + latest values
store/
schema.py # DDL + PRAGMA user_version (simple migrations)
database.py # aiosqlite connection helper
repository.py # Async CRUD for devices + points + reading snapshots
seed.py # Example rows (disabled by default where appropriate)
runtime/
registry.py # SupervisorRuntime: task map, start/stop/reload
poller.py # Per-device asyncio loop: sleep → driver.read_points → persist
drivers/
base.py # BaseDriver protocol/ABC
bacnet_jsonrpc.py # Uses JsonRpcBacnetClient + RPM batching
Insertion points (by concern)
Persistent config storage
- SQLite via
aiosqliteundersupervisor/store/. - Single schema file (
schema.py) applied idempotently on open (CREATE TABLE IF NOT EXISTS, bumpPRAGMA user_version). - Repository is the only layer that runs SQL (keeps FastAPI routes thin).
Runtime device registry
SupervisorRuntimeinsupervisor/runtime/registry.pyholds:dict[device_id, asyncio.Task]for active poll loopsasyncio.Lockfor structural changes (add/cancel/replace task)- In-memory health (
last_poll_at,last_error,status) keyed by device
Polling task manager
poller.pyimplements one coroutine per enabled device: cancel-aware sleep, call driver, write results + timestamps through repository, update health.- Per-device scrape interval stored on the device row (
scrape_interval_seconds).
BACnet driver abstraction
drivers/base.py:BaseDriverwithDRIVER_TYPEandread_points(...).drivers/bacnet_jsonrpc.py: constructsJsonRpcBacnetClientfrom device/env; batches reads withrpm(object id + property pairs) when multiple points exist.
FastAPI / future web UI
supervisor/app.py: dedicated app (uvicorn easy_aso.supervisor.app:app) with lifespan (not legacy startup events) to own the runtime lifecycle.- Routers in
api/routes.pydepend onapp.state.runtimeandapp.state.repository. - A future static UI can call the same JSON API; no UI in initial phases.
Data flow
- Lifespan startup: open DB → migrate schema → seed if empty →
SupervisorRuntime.start()loads enabled devices and spawns poll tasks. - Poll loop: for each device → driver reads enabled points → repository updates
last_value,last_polled_at,last_errorper point → runtime updates device health. - Config change (API): repository mutates SQLite →
runtime.reload_device(device_id)cancels prior task and starts a fresh loop from DB (hot reload, not full process restart).
MQTT (future)
Publishers subscribe to an internal async callback queue or poll latest values from SQLite on an interval. The repository already holds last_value snapshots suitable for fan-out without coupling drivers to MQTT.
Docker / Raspberry Pi
- Supervisor is optional: default images keep working without the new app.
- Python 3.14 base images for easy-aso Dockerfiles; SQLite file on a volume for persistence on Pi.
Testing strategy
- In-memory SQLite (
:memory:) + mock driver for lifecycle and CRUD/reload tests (no BACnet network). - Optional integration later: compose stack + real RPC (out of scope for default CI).
Implementation status (phases 2–7)
| Phase | Delivered |
|---|---|
| 2 | easy_aso/supervisor/store/ — SQLite schema v1, SupervisorRepository, ensure_seed_data |
| 3 | easy_aso/supervisor/runtime/ — SupervisorRuntime, per-device tasks, health, graceful cancel |
| 4 | easy_aso/supervisor/drivers/ — BaseDriver, StubDriver, BacnetJsonRpcDriver (JSON-RPC RPM) |
| 5 | easy_aso/supervisor/coordinator.py — CRUD hooks calling reload_device with structured logging |
| 6 | easy_aso/supervisor/api/ + app.py — FastAPI CRUD, latest values, health (/api/v1/...) |
| 7 | tests/test_supervisor.py, docs/SUPERVISOR_WORKFLOWS.md, pytest-asyncio config |
Entry point: uvicorn easy_aso.supervisor.app:app