-
Notifications
You must be signed in to change notification settings - Fork 0
Version history
What each release actually gave you, newest first. Downloads live on the releases page and, for everything current, the signed repository.
The whole project is seven weeks old at the time of writing — it started on 4 July 2026 — which is why the version numbers move quickly.
| 3.2.0 | 21 Aug 2026 | one-shot timers, the panel signs its requests, OPNsense, containers rebuilt |
| 3.1.0 | 18 Aug 2026 | live web panel (SSE), programs and Nerd in the browser |
| 3.0.5 | 16 Aug 2026 |
GET /api/system — the whole deployment, introspected |
| 3.0.2 | 13 Aug 2026 | the lockout postmortem: auth logging, 401 reason codes, fail2ban fixes |
| 3.0.1 | 23 Jul 2026 | first stable 3.0; FreeBSD and NetBSD packages |
| 3.0.0 | 21–22 Jul 2026 | Ed25519 request signing, SSE push, scan-to-add, capabilities, metrics |
| 2.6.1 | 10 Jul 2026 | SELinux label fix — writes silently denied on RHEL-family |
| 2.6.0 | 10 Jul 2026 |
diag / approve / devices / revoke built into the binary |
| 2.5.0 | 10 Jul 2026 | no Python needed: self-contained native packages for 15+ distros |
| 2.4.3 | 7 Jul 2026 |
config.json 640, so the admin CLIs work without sudo
|
| 2.4.2 | 7 Jul 2026 | web panel refresh, six colour palettes, build-info footer |
| 2.4.1 | 6 Jul 2026 | web panel brought back to parity with the app |
| 2.4.0 | 6 Jul 2026 | batch state, /version, /health, delete-unit, brotli |
| 2.3.0 | 5 Jul 2026 | first-class Windows: installer, service, Caddy wizard |
| 2.2.1 | 5 Jul 2026 | non-systemd inits and musl hosts |
| 2.2.0 | 5 Jul 2026 | authenticated config API — rename and add units remotely |
| 1.0.0 | 4 Jul 2026 | the first working thing |
Five pieces of work:
-
One-shot timers. "Turn this off in 45 minutes", server-side, so it happens with the phone away or flat. Its own subsystem rather than a program kind, because a timer exists to happen once and then stop existing — Timers. The Android app drives it from an hourglass beside the power switch.
-
The web panel signs its requests with Ed25519 (auth v2), the last client that did not. The private key is non-extractable and lives in IndexedDB, so the page cannot read it back. A server set to
AC_MIN_AUTH_VERSION=2can now refuse unsigned clients without refusing its own panel. -
OPNsense is a tier-1 port. The
os-breeze-coreplugin — a page under Services, service control through configd, an rc script — with the whole Python runtime vendored, because OPNsense isFreeBSD:14:amd64withpython311and neither rust nor pip. Installing on OPNsense. -
The container images were rebuilt from scratch, five of them, each named for what it is: Alpine Edge (musl, x86_64 and arm64, self-updating), UBI 9 (glibc) at two x86-64 psABI levels, and one with nginx bundled so HTTPS works on first start. That work also fixed a real bug: no image had a timezone database, so
TZwas silently ignored and every schedule fired on UTC. Installing with containers. -
The documentation moved here. The README was 2,000 words and
docs/another 25,000; this wiki replaced both with something organised.
Live web UI, and programs in the browser.
The panel had been polling every five seconds. It now consumes the SSE stream the server has published since 3.0.0-pre2 — the same one the phone app uses — so state is live, and a backgrounded tab closes the stream entirely, which stops it causing any LAN traffic to the air conditioners. Polling stays as a documented fallback for an older server or a proxy that buffers.
Three additions brought the panel closer to the app: programs (favourites,
schedules and curves — view, apply, enable/disable, delete, and save a favourite
from a unit's current state), Nerd (everything /api/system knows, rendered
generically so new server facts appear without a UI change), and a per-browser
beep toggle, off by default so a 2 a.m. schedule stays silent.
riscv64 deliberately stayed at 3.0.5: its bundle is a ~2-hour emulated build, and
packages labelled 3.1.0 for riscv64 were built and discarded rather than shipped
with a 3.0.5 binary inside.
GET /api/system — everything this deployment knows about itself. One endpoint
answering "what am I actually running?", behind the app's Nerd screen: host
(distro, kernel, init system, CPU, byte order, libc, uptime), runtime (Python, and
the version of every dependency), the deployment itself (build commit, install
date, timezone, the paths and file modes of every store, the effective settings),
the units, the enrolled devices, and this connection — the IP the server sees
you as, whether that reads as private, and the proxy headers, which is the fastest
way to diagnose a proxy that is not forwarding the real client address.
It needs the API key and a device credential, the same bar as controlling a unit, and it contains no secrets — a test greps the raw response to keep it that way.
Also: OpenBSD reports kern.boottime as a bare epoch where the other BSDs print a
struct, found by running the probes on the actual VMs rather than assuming "the
BSDs" agree.
A reliability release written out of a real lockout, in which several users lost access for days and nothing in the log said why.
-
Authentication failures are now logged at all. Previously there was no auth
logging — only uvicorn's bare
401 Unauthorizedlines. Every failure now records a reason code, the client IP and an 8-character key-id hint, never a secret. -
401s say why, so a client can tell transient from fatal. A phone whose
clock had drifted past the ±60 s signature window produced a 401 indistinguishable
from a revoked credential — so the app concluded its credential was dead, deleted
its Ed25519 private key, and with LAN-only enrolment stranded anyone away from
home. Failures now carry a stable
errorcode and aretryableflag, and aclock_skewrejection returnsserver_timeso a client can measure its offset and re-sign instead of re-pairing. -
The shipped fail2ban configs were banning legitimate users. The filter counted
400|401|403|404|405|422|429— but 404 is a client feature-detecting endpoints, 422 is input validation, and 429 is our own rate limiter — at 5 hits per 10 minutes, escalating to a five-week ban, and the tripwire banned for a week on a single 403. Since clients behind NAT share one address, a ban took everyone on that connection offline at once.
riscv64 arrived here too, as the first architecture that compiles rather than
unpacks: no PyPI wheels exist for it, so every dependency builds from source
under emulation.
The first stable of the 3.0 line, and BSD packages: FreeBSD and NetBSD install
from a private virtualenv, since the Linux self-contained bundles cannot run on a
BSD kernel. Verified end-to-end on real FreeBSD 15.1 and NetBSD 10.1 — installer,
rc.d service and health check — which caught an rc.netbsd bug where the pidfile
and log were written as the unprivileged user.
Two majors of work, shipped stable as 3.0.1.
-
Ed25519 request signing (auth v2). Every request is signed with a key
generated on the device that never leaves it; the server stores only the public
key, so a server compromise leaks nothing forgeable. Each signature binds
method + path + timestamp + nonce + SHA3-512 of the body, so replay and tampering
both fail. Legacy bearer tokens keep working and upgrade in place;
AC_MIN_AUTH_VERSION=2refuses them. See Signed auth v2 migration. -
Live push over SSE —
GET /api/units/stream, polling the units centrally and only while a client is connected. -
Scan-to-add —
GET /api/units/scanfinds units by their open ports across the server's private subnet, so nobody has to type IP addresses. -
Per-unit capabilities, so clients hide controls the hardware lacks;
whoami; state history; and a Prometheus
/metricsendpoint served from last-known state so scrapes cannot hammer the units. - Opt-in beep — silent by default, which is what makes scheduled changes bearable at night.
A silent SELinux failure on RHEL, Fedora, AlmaLinux, Rocky and enforcing SUSE.
The packaged binary lives in /usr/lib/breeze-core/, which SELinux labels lib_t
— not a domain-transition entrypoint — so the service ran as init_t, whose writes
to /etc/breeze-core are denied and dontaudit'd. The server started and read
its config fine, but approving a pairing failed with a 500 and nothing in the
audit log. Found live by breeze-core diag during a migration to the rpm.
The diagnostic and approval tools moved into the binary: diag, approve,
devices, revoke, with no zsh, curl or jq needed. diag is a faithful port of
the zsh original and stays a pure HTTP client, so the API remains the only
contract. See Command-line tools.
Python stopped being a requirement. Every release began shipping self-contained builds — server, panel and a private runtime in one bundle — wrapped as native packages: deb, rpm, pacman, apk, tarballs for glibc and musl × amd64 and arm64, plus a Nix flake with a NixOS module and recipes for packagers. Each package is install-tested on 15 real distro userlands in CI before release.
-
2.4.3 —
config.jsonbecame mode 640. The CLIs told you to "run with sudo, or join the service group", but the file was mode 600, so joining the group did nothing — andsudo <alias>fails because sudo drops shell aliases. Now joining the group genuinely works, and the state directory stays 750. - 2.4.2 — a bigger, bolder panel, six Material-You-like palettes, and a footer showing the server's version and git commit.
-
2.4.1 — the panel had fallen behind the app ("oops forgot about web ui"):
manage units, °C/°F, one-request polling. Also closed two code-scanning alerts —
setup_device.pyno longer printed the API key or V3 secrets to the terminal. -
2.4.0 —
GET /api/units/state(every unit in one call, fanned out server-side, with unreachable units markedonline:falseinstead of 503-ing the batch),/api/version,/api/health,DELETE /api/units/{id}, and brotli compression with a gzip fallback.
First-class Windows. A guided NSIS installer that builds a private venv,
registers the BreezeCore service through NSSM as LOCAL SERVICE, locks the
firewall to the LAN, strips ACL inheritance on %ProgramData%\breeze-core, and
offers to pair your units on the finish page. The Caddy reverse proxy is a separate
optional component, chosen only for public exposure.
Beyond systemd and glibc. Install paths with copy-paste templates for OpenRC, runit, s6, supervisord and SysV, refined FreeBSD rc.d and macOS launchd, and a musl guide for Alpine, Void-musl and OpenWrt. The hardening runbook grew a section mapping every systemd sandbox directive to a non-systemd equivalent — most importantly a user-matched egress firewall that pins the service account's outbound traffic to the LAN.
The authenticated config API: a sanitized GET /api/config that never returns
the API key or V3 secrets, PATCH /api/units/{id} to rename, and POST /api/units
to add a unit by its LAN address — so units could be managed from any paired client
instead of only by editing a file on the server.
The first working thing, in a single day: the FastAPI app with pairing-based auth, the vanilla-JS web panel, the zsh diagnostic and approval CLIs, a UBI 9 container image, a reverse-proxy wizard, and CI.
- No 2.0 or 2.1. 1.0.0 went straight to 2.2.0; neither was ever released.
-
2.7.0 never shipped. Ed25519 request signing was developed under that number
on the
overhaulbranch and released as part of 3.0.0 instead — it was too large a change to be a minor. - No 3.0.3 or 3.0.4. 3.0.2 was followed by 3.0.5; neither number was ever published.
Breeze Core · Breeze for Android · Packages · AGPL-3.0
Start here
Install it
Use it
Reference
Run it safely
Develop and port