-
Notifications
You must be signed in to change notification settings - Fork 0
REST API
Base URL: http://<host>:8420 (or your HTTPS proxy URL). All secret
comparisons are constant-time. Responses are compressed with brotli
(gzip fallback) when the client sends Accept-Encoding — disable with
AC_COMPRESSION=0.
POST /api/auth/enroll/start X-API-Key → {session_id, user_code, expires_in}
body {label, auth_version?, public_key?} (public_key for v2)
POST /api/auth/enroll/poll X-API-Key → {status, token_id, label, auth_version,
expires_at[, device_token]} (token: v1 only)
POST /api/auth/enroll/approve X-API-Key + LAN → {token_id, label} (admin)
POST /api/auth/upgrade X-API-Key + cred → {token_id, auth_version:2}
body {public_key}; re-keys the calling device to v2 in place
GET /api/auth/whoami X-API-Key + cred → {token_id, label, auth_version,
created_at, expires_at, last_used}
(the *calling* device; not LAN-gated)
GET /api/auth/devices X-API-Key + LAN → [{token_id, label, auth_version, created_at, ...}] (admin)
DELETE /api/auth/devices/{id} X-API-Key + LAN → 204 (admin)
Health is public; version needs the API key.
GET /api/health → {status:"ok"} (public liveness probe)
GET /api/version X-API-Key → {name, version, commit, features[],
auth_versions:[1,2], min_auth_version, units} (feature-detect)
GET /metrics X-API-Key → Prometheus text: unit online/temps (last known,
no live fetch) + scheduler counters + build info
Everything this deployment knows about itself, in one response — it backs the
Breeze app's Nerd screen. Requires the API key and a device credential:
it reports host details and the enrolled-device list, so it sits at the same
bar as controlling a unit. Added in 3.0.5; feature flag system_info.
GET /api/system X-API-Key + cred → {
server: {name, version, commit, python{}, started_at, uptime_seconds,
installed_at, timezone, utc_offset_seconds, server_time,
features[], auth_versions[]},
os: {system, pretty_name, distro_id, distro_version, kernel,
kernel_version, hostname, libc},
init: {name, detail} # systemd | openrc | runit | rc.d | launchd …
cpu: {arch, model, cores, endianness},
components: {msmart-ng, fastapi, uvicorn, starlette, pydantic, …},
network: {hostname, local_addresses[], bind_host, bind_port},
connection: {client_ip, client_is_private, request_url, host_header,
scheme, http_version, forwarded_for, behind_proxy_enabled},
paths: {config, devices, programs, package},
settings: {…the effective runtime settings…},
units: [{id, name, ip, port, has_v3_credentials, connected,
capabilities, samples}],
devices: [{token_id, label, auth_version, created_at, last_used,
expires_at, expires_in_seconds, expired}],
programs: {total, by_kind}, scheduler: {…}, storage: {…}, process: {…},
machine_uptime_seconds }
Two design rules worth knowing as a client author. Every field can be
null — each fact is probed defensively and an undeterminable one degrades
rather than failing the request, so render missing values, don't assume them.
And units[].capabilities is only populated for units the server already
has a live connection to: filling it for the rest would cost a LAN
round-trip each (~0.7 s on real hardware). Ask
/api/units/{id}/capabilities per unit if you need the others.
It contains no secrets — not the API key, not device public keys, not the
per-unit V3 token/key (has_v3_credentials is a boolean).
All of these require the API key and a device token.
GET /api/units → [{id, name, ip}]
GET /api/units/state → {states:[…], errors:[{id,name,ip,detail}]} (all units,
fanned out concurrently; unreachable units → online:false
in states, or in errors — never 503s the whole batch)
GET /api/units/{id}/state → full state (connects + refreshes the unit)
GET /api/units/{id}/capabilities → what the unit supports (see below) — lets
clients hide controls the hardware lacks
GET /api/units/{id}/history → {id, samples:[…]} recent in-memory readings
(indoor/outdoor/target/mode/power) for a graph;
best-effort, non-persistent, empty until polled
GET /api/units/stream → text/event-stream: live per-unit state pushed
as it changes (SSE). `event: state` frames +
`: keepalive` comments. See "Live updates" below.
POST /api/units/{id}/control → full state (applies only the fields present)
PATCH /api/units/{id} → rename a unit (body {name}) → sanitized unit view
GET /api/units/scan → {subnet, candidates:[{ip, port, known}]} (LAN TCP scan
of ports 6440–6449; ?subnet=CIDR override, ?timeout=;
read-only — adding still goes through POST /api/units)
POST /api/units → add a unit by LAN IP (body {ip, name?}); discovers
it and writes config.json → 201 sanitized unit view
DELETE /api/units/{id} → remove a unit from config → 204
GET /api/config → sanitized config: [{id,name,ip,port,has_v3_credentials}]
(never returns the api_key or V3 token/key secrets)
State object:
{ "id":"…","name":"…","ip":"…","online":true,"power_state":true,
"operational_mode":"COOL","target_temperature":22.0,
"indoor_temperature":26.3,"outdoor_temperature":31.0,
"fan_speed":102,"swing_mode":"BOTH","eco":false,"turbo":false }Errors: 401 bad key/credential · 404 unknown unit · 422 out-of-range value ·
426 device auth-version below min_auth_version (upgrade the client) ·
503 unreachable/apply-failed.
Capabilities object (GET /api/units/{id}/capabilities) — from msmart's
get_capabilities(), read defensively (a field is null when the
firmware/msmart doesn't report it, so clients "show it" rather than wrongly
hide it):
{ "id":"…","operational_modes":["AUTO","COOL","DRY","HEAT","FAN_ONLY"],
"swing_modes":["OFF","VERTICAL","BOTH"],
"supports_vertical_swing":true,"supports_horizontal_swing":false,
"fan_speeds":["LOW","MEDIUM","HIGH","AUTO"],"supports_custom_fan_speed":true,
"min_target_temperature":16.0,"max_target_temperature":30.0,
"supports_eco":true,"supports_turbo":true,"supports_display_control":true,
"supports_freeze_protection":false,"supports_humidity":false }GET /api/units/stream is a Server-Sent Events stream (full auth, same as
the other unit routes). Instead of each client polling, the server polls the
units once (only while ≥1 client is connected) and fans changes out to all
streams — the app can drop its own poll loop (battery), and a change made by a
schedule or another client shows up on the next tick.
: connected ← sent immediately on open
event: state ← one frame per unit, whenever it changes
data: {"id":"…","online":true, … same shape as GET /state}
: keepalive ← comment every ~15 s to hold the connection
Notes:
- It pushes the same state object as
/state; a client applies each frame to its cached unit. Cadence while connected isAC_STREAM_TICK(default 5 s) — Midea units have no push, so the server still polls them; SSE only centralises that polling. -
Compression is disabled for this route (
Content-Encoding: identity), so a client that can't decode brotli still reads it; the response also setsX-Accel-Buffering: noso nginx doesn't buffer it (see REVERSE-PROXY.md).
Require API key + token; stored and executed server-side by the background scheduler.
GET /api/programs → [Program]
POST /api/programs → 201 Program
GET/PUT/DELETE /api/programs/{id}
POST /api/programs/{id}/apply → [UnitState] (favourite→scene; curve→now; schedule→400)
GET /api/programs/status → {running, tick_seconds, runs, errors, last_run}
A program targets units (unit_ids, empty = all) and is one kind:
- favourite — a saved scene, applied on demand.
-
schedule —
{days:[0–6, empty=daily], time:"HH:MM", settings}triggers, fired when the clock crosses the minute. -
curve —
{operational_mode, fan_speed, points:[{time,temperature}]}; the scheduler sets the interpolated setpoint (cyclic over the day, snapped to 0.5°). Times are server-local.
One-shot "do this in N minutes". Full auth, like control. See Timers for the reasoning.
| Method | Path | Notes |
|---|---|---|
GET |
/api/timers |
pending timers, each with seconds_remaining computed server-side |
POST |
/api/timers |
{unit_ids, minutes, settings?, label?} → 201. minutes is 1–1440; settings defaults to power off. Creating one for a unit replaces that unit's existing timer. 404 for an unknown unit id |
DELETE |
/api/timers/{id} |
204, or 404 if it has already fired |
GET |
/api/timers/status |
runner health: pending count, next fire time, tick, counters |
minutes, not a wall-clock time: the server computes the moment from its own
clock, so no client needs to know the server's timezone.
Breeze Core · Breeze for Android · Packages · AGPL-3.0
Start here
Install it
Use it
Reference
Run it safely
Develop and port