Skip to content

Architecture

monikapurpl3 edited this page Aug 21, 2026 · 1 revision

Architecture

   Browser / Breeze app ─┐
                         │  HTTPS (via reverse proxy)  or  HTTP on the LAN
   ac-diag.zsh ──────────┤
                         ▼
              ┌──────────────────────────────┐
              │  uvicorn → meow_ac.app:app   │  (one worker + in-process scheduler)
              │   ├── /api/auth/*   pairing  │
              │   ├── /api/units*   control  │
              │   ├── /api/programs favourites / schedules / curves
              │   └── /             static web panel
              │            │ msmart-ng       │
              │            ▼                 │
              │   AC device objects (cached) │
              └────────────┬─────────────────┘
                           │ TCP :6444 per unit
                    Midea AC units on the LAN

Three components share exactly one contract — the /api/* endpoints — and are otherwise fully decoupled. Deleting either client leaves the API and the other client working.

Inside the package

Small layers, assembled by an app factory. create_app() is the one place everything is wired: it builds a config store, hands it to the device manager and the authenticator, builds the routers from those, and mounts the static panel. Nothing reaches for global state.

Layer Responsibility
Settings Env-driven runtime config, read once. Feature flags for public exposure: docs, security headers, trusted hosts, behind-proxy, LAN-only enrolment, credential lifetime.
Config config.json as a typed store — the shared API key and the unit list, including per-unit V3 token/key. Admin-managed, shared with the pairing tool.
Security Device-pairing auth. An Authenticator protocol with an API-key implementation and a device-credential implementation, composed so a request must satisfy both; plus the enrolment handshake, the credential store, and rate limiting.
Devices Connection lifecycle and cache, LAN discovery, the wire schema with bounds, and one shared apply_to_unit helper.
Programs Favourites, schedules and curves, their store, and the background scheduler that fires them.
API Router factories, each taking its collaborators as arguments.

Two design decisions worth knowing

Connections are lazy and cached per unit. The first request for a unit after a restart pays connect + authenticate + capability probe + refresh; later ones reuse the object. Every state read and every control write is still a live LAN round-trip — there is no push or subscribe in the protocol — so per-call latency (~0.7 s on the maintainer's units) is inherent, not a bug. Each unit has its own lock, so concurrent requests to the same unit serialise instead of racing.

The scheduler shares the control path. Schedules and curves call the very same apply_to_unit as POST /control, which is why a scheduled change is indistinguishable from a manual one — including in the SSE stream. One uvicorn worker means exactly one scheduler, so nothing fires twice.

Extending it

The seams are deliberate: add work through them rather than widening a module.

  • A new endpoint → write a build_* router factory that takes its collaborators as arguments, and include it in create_app().
  • A new auth factor → implement the Authenticator protocol and pass it to create_app(). Endpoints depend on "the authenticator" and do not change.
  • A new client → speak the three /api/* endpoint groups. The panel and the Android app are both nothing more than HTTP clients; the Flutter client plan is a third.

Read REST API for the wire contract and Control schema for the field values every component shares.

Source layout

The repository holds the server package, the static panel, the CLIs, the packaging for every platform, and the container definitions. Build instructions: Building and releasing.

Clone this wiki locally