Releases: monikapurpl3/breeze-core
Release list
Breeze Core v3.2.0
Timers, and a web panel that signs its own requests
One-shot timers
"Turn this unit off in 45 minutes" — decided by the server, so it happens
whether or not the phone is still in the house, charged, or in the country. In the
Android app it is an hourglass beside the power switch; over HTTP it is
POST /api/timers.
It is a new subsystem, not a new kind of program. Programs are standing
intentions that keep applying; a timer exists to happen once and then stop
existing. Making it a schedule entry would have taught every client that reads
programs about a schedule that is not really a schedule, and left spent ones
sitting in the list looking like they will fire again. So: timers.json, its own
runner, its own endpoints — and it fires through the same
devices/control.py:apply_to_unit path as a button press.
- You ask for minutes, never a time of day. The server computes the moment
from its own clock, and every response carries a server-computed
seconds_remaining. A phone in another timezone, or with a wrong clock, still
gets the right result — this project has been bitten by clock skew before. - A new timer replaces that unit's existing one; a fired timer deletes itself;
an overdue one fires late rather than being skipped, because "off, late" beats
"on all night". settingsis a full control payload, defaulting to power-off, so "switch to
eco in an hour" needs no second feature.- The runner says dispatched, not applied, and warns when the unit reports
itself offline — msmart'sapply()does not raise for an unreachable unit, so a
success is not proof it was heard. True of every command; it just matters more
unattended.
New settings: AC_TIMERS (default <config dir>/timers.json) and
AC_TIMER_TICK (default 15s — finer than the scheduler's 30s, because a
schedule only has to hit the right minute while a timer is a promise about a
moment). Separate asyncio task, so a disk error in one cannot stop the other.
The web panel signs its requests (auth v2)
The panel was the last client still using the legacy bearer token, which is why a
server set to AC_MIN_AUTH_VERSION=2 used to lock out its own UI. It now
signs every request with Ed25519, like the app.
The key pair is generated in the browser with its private half
non-extractable: WebCrypto signs with it and will not hand the bytes back to
the page that created it. It lives in IndexedDB; enrolment registers only the
public key, so nothing secret crosses the wire in either direction.
Signatures bind method + path + timestamp + nonce + SHA3-512 of the body. WebCrypto
has no SHA-3 at all, so the panel ships a small Keccak implementation, checked
against 254 vectors. A clock_skew rejection is retried once with the server's
offset learned, and a replay rejection with a fresh nonce, so a drifted clock
self-heals instead of prompting for re-pairing. Browsers without Ed25519 fall back
to the bearer token, which still works unless the server requires v2.
OPNsense is a tier-1 port
os-breeze-core — a page under Services, service control through configd, an rc
script — with the entire Python runtime vendored, because OPNsense is
FreeBSD:14:amd64 with python311 and neither rust nor pip.
The container images were rebuilt from scratch
Five, each named for what it actually is instead of by tag archaeology: 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. Every image
has a first-run setup script; the nginx one sets up TLS too.
That rebuild also found a real bug: no image had a timezone database, so TZ
was silently ignored and every schedule and curve fired on UTC.
The documentation moved to the wiki
The README was ~2,000 words and docs/ another ~25,000. Both are replaced by
the wiki, including a
version history
going back to 1.0.0.
Packaging
- A build now records the commit it came from, and the packagers refuse to
label a stale bundle with a new version. This release is the reason: the
riscv64 bundle silently carried a 3.0.5 binary into 3.2.0-labelled packages.
They were deleted, and the guard exists so it cannot happen quietly again. mkpkg-netbsd.shsays it needs root instead of failing on a barecp.
Notes
- No riscv64 in this release. Its bundle is a ~2-hour emulated build and the
attempt for 3.2.0 produced a stale binary, so nothing riscv64 is attached rather
than something mislabelled. The most recent riscv64 packages are on
v3.1.0. - The attached bundles are stamped commit
481714f; the commits between it and
this tag touchpackaging/only, so the server and panel they contain are the
tagged ones. - FreeBSD
.pkgand NetBSD.tgzare built from source on real BSD hosts;
OpenBSD installs from the source tarball. - Everything here is also on the signed repositories at
https://bolero.salataputarica.hr.eu.org, andwingethas a 3.2.0 manifest.
Breeze Core v3.1.0
Live web UI, and programs in the browser
The web panel was polling every 5 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 LAN traffic to the air conditioners at all. Polling remains as a
documented fallback for an older server or a proxy that buffers responses.
Three additions bring 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. Editing schedule
entries and curve points stays in the app. - Nerd — everything
/api/systemknows, rendered generically so new server
facts appear without a UI change. Copy-all included. - Beep — a per-browser toggle, off by default so a 2am schedule stays silent.
Notes
riscv64remains at 3.0.5: its bundle is a ~2 hour emulated build and it
is documented as landing last and lagging. Packages labelled 3.1.0 for riscv64
were built and deliberately discarded rather than shipped with a 3.0.5 binary
inside.- FreeBSD
.pkgand NetBSD.tgzare built from source on real BSD hosts;
OpenBSD installs from the source tarball. - Everything here is also on the signed repositories at
https://bolero.salataputarica.hr.eu.org.
Breeze Core 3.0.5
GET /api/system — everything this deployment knows about itself
One endpoint that answers the question "what am I actually running?", added to back the Breeze app's new Nerd screen (tap the version in Settings seven times). It reports:
- Host — distro/OS pretty name, kernel, init system (systemd · OpenRC · runit · rc.d · launchd · Windows SCM · or pid 1's name in a container), CPU model, architecture, cores, byte order, libc, machine uptime
- Runtime — Python version and implementation, whether this is a frozen bundle, and the version of every dependency (
msmart-ng, fastapi, uvicorn, starlette, pydantic, …) - This deployment — build commit, process uptime, install date, time zone and UTC offset, pid and RSS, the paths and file modes of
config.json/devices.json/programs.json, and the effective settings - Units — each one's address, whether it has V3 credentials, whether a connection is cached, its history sample count, and its capabilities where they're already known
- Devices — every enrolled client with its auth version, enrolment date, last use and expiry
- This connection — the IP the server sees you as, whether that reads as private, the Host header, scheme, HTTP version,
X-Forwarded-Forand proxy state — which is the fastest way to diagnose a proxy that isn't forwarding the real client address
Requires the API key and a device credential — the same bar as controlling a unit. It contains no secrets: not the API key, not device public keys, not per-unit V3 token/keys (has_v3_credentials is a boolean), and a test greps the raw response to keep it that way. Every field can be null: each fact is probed defensively so an undeterminable one degrades instead of failing the request. Feature flag system_info; full reference in docs/API.md.
Also
- OpenBSD uptime — it reports
kern.boottimeas a bare epoch where FreeBSD/NetBSD/macOS print{ sec = … }. Found by running the probes on the actual VMs rather than assuming "the BSDs" agree. Verified on FreeBSD 15.1, NetBSD 11.0 and OpenBSD 7.9. - Packaging — when Docker Desktop silently fails to share a bind mount (the container starts,
/workis just empty), the packaging step now streams inputs and artifacts over stdio instead. build-installer.ps1is saved with a BOM so Windows PowerShell 5.1 can parse it.
Packages for deb · rpm · pacman · apk · tarballs (glibc/musl × amd64/arm64), FreeBSD .pkg, NetBSD .tgz, and the Windows installer are attached. musl/riscv64 follows shortly — it compiles every dependency from source under emulation. The signed repo at https://bolero.salataputarica.hr.eu.org is updated with the same build.
Breeze Core v3.0.2
Breeze Core 3.0.2 — a reliability release, from the postmortem of a real lockout: several users lost access for days and nothing in the log said why.
Fixed
- Authentication failures are now logged. There was previously no auth logging at all — only uvicorn's bare
401 Unauthorizedaccess lines. Every failure now records a reason code, the client IP and an 8-character key-id hint (never a secret), and enrolments/revocations are logged too, so "it logged me out yesterday" is answerable:journalctl -u breeze-core | grep "auth failed" - 401s now say why, so clients 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 and deleted its Ed25519 private key, which with LAN-only enrolment strands anyone away from home. Failures now carry a stable
errorcode and aretryableflag:clock_skew,replay,incomplete_signatureare retryable;unknown_key,expired,bad_signature,no_credential,bad_api_keyare definitive. Aclock_skewrejection also returnsserver_time, so a client can measure its offset and re-sign instead of re-pairing.unknown_keyandexpiredare now distinguished. - The shipped fail2ban configs banned legitimate users. The filter counted
400|401|403|404|405|422|429at 5 hits per 10 minutes, escalating to a five-week ban — but404is a client feature-detecting endpoints,422is input validation, and429is our own rate limiter. The tripwire banned for a week on a single 403. And since clients behind NAT share one address, a ban took everyone on that connection offline at once. Now:401/403only, 20 per 10 min, 1 h capped at a day, tripwire needs 3 hits, andignoreipdocuments that your WAN address belongs there. See HARDENING §3 — including a new §3.1 on reading the auth log.
Added
AC_AUTH_SKEW_SECONDS(default 60) to widen the accepted clock drift for devices with sloppy clocks.
Upgrading is safe from 3.0.1 (the packaging scriptlet fix in that version means dnf/apt upgrade no longer stops the service). 28/28 tests pass.
RISC-V (added after the initial release)
riscv64 now ships as a musl build — breeze-core_3.0.2_riscv64.apk and the matching tarball, also in the Alpine repo on bolero.
It is the first architecture that compiles rather than unpacks: PyPI has no riscv64 wheels, so every dependency is built from source under QEMU emulation — pydantic-core's Rust and PyInstaller's own bootloader included. That build takes hours, needs Alpine edge (rustc >= 1.88, which no stable Alpine ships yet), and is why riscv64 will land last on future releases and may lag the current version.
There is no OpenWrt riscv64 .ipk: that bundle is built on a newer Alpine than OpenWrt 23.05's musl 1.2.4 floor permits. There is no glibc riscv64 build either — see the note on bolero for why.
IT TOOK TWO FUCKING HOURS TO MAKE THAT RISCV BUILD
Breeze Core v3.0.1
Breeze Core 3.0.1 — the first stable of the 3.0 line. Self-hosted, LAN-first REST API + web control panel + diagnostic CLI for Midea air conditioners. No cloud after pairing.
Highlights of the 3.0 line
- 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. Existing bearer-token (v1) devices keep working and upgrade themselves in place; set
AC_MIN_AUTH_VERSION=2to require v2. - Network scan-to-add.
GET /api/units/scanfinds units by their open ports across the server's private subnet, so the app and web UI can add units without typing IPs. - Per-command beep.
beepis an optional control field now (silent by default — schedules and older clients stay quiet). - New read endpoints: per-unit
capabilities(clients hide controls the hardware lacks),whoami(the calling device's own credential + expiry), statehistory, and a Prometheus/metricsendpoint served from last-known state (it never triggers a live LAN fetch, so scrapes can't hammer the units). - Live updates over SSE.
GET /api/units/streampushes state changes — including ones made by a schedule or another client — so clients can stop polling. - Fuller
diag. Adds a host-local background-service check (is it running? enabled at boot?) across systemd / OpenRC / runit / procd / Windows.
New in 3.0.1
- BSD packages. FreeBSD and NetBSD install from a private virtualenv (the Linux self-contained bundles can't 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. FreeBSD ships a native
.pkgand a signedpkgrepo; NetBSD a binary package +pkginrepo. Fixed anrc.netbsdbug (pidfile/log written as the unprivileged user → permission denied) caught during live testing.
Install
Add the signed repository once — copy-paste setup per distro at https://bolero.salataputarica.hr.eu.org (apt · dnf/zypper · pacman · apk · opkg · FreeBSD pkg · NetBSD pkgin). One-off downloads are the assets below; Docker is ghcr.io/monikapurpl3/breeze-core:3.0.1. Full guide: docs/PACKAGES.md.
Every Linux artifact here is built and install-tested across 15 distro userlands in CI before release.
Breeze Core v3.0.0 (prerelease 2)
Prerelease 2 of the v3.0.0 overhaul — from the overhaul branch, for testing. Not on the bolero repo (stable only).
Everything in pre1 (Ed25519 request signing, scan-to-add, opt-in beep) plus:
- Live push over SSE —
GET /api/units/stream: the server polls the units centrally (only while a client is connected) and pushes state changes, so the app can drop its own polling. Emitsevent: stateframes + keepalives; compression forced off andX-Accel-Buffering: noso it streams cleanly through proxies. - Per-unit capabilities —
GET /api/units/{id}/capabilities: what the hardware supports (modes, flap axes, eco/turbo, temp range, …) so clients hide controls a unit lacks. - whoami —
GET /api/auth/whoami: the calling device's own record (label, auth version, expiry). - History + Prometheus —
GET /api/units/{id}/history(in-memory graph data) andGET /metrics(last-known values + scheduler counters, API-key gated, no live fetch). - diag now checks the background service (running? enabled at boot? systemd/OpenRC/runit/procd/Windows).
- The web UI got scan-to-add too; the min-auth clamp now also refuses new v1 enrollment.
This asset: breeze-core-3.0.0.pre2-1.x86_64.rpm (self-contained, no Python). Smoke-tested: installs, runs, streams SSE, reports 3.0.0.pre2. Pairs with the Breeze app v2.0.0 prerelease 2 (SSE baked in). Built from overhaul @ 987911d. (CI attaches the full deb/rpm/apk/pacman/tarball matrix shortly.)
Breeze Core v3.0.0 (prerelease)
Prerelease of the v3.0.0 overhaul — for testing, from the overhaul branch. Not on the bolero package repo (which stays on stable releases).
This bundles two majors of work:
Ed25519 request signing (auth v2)
Per-request Ed25519 signatures — the device's private key never leaves it, the server stores only the public key, and each request binds method+path+timestamp+nonce+SHA3-512(body). Replay/tamper protected. Legacy bearer (v1) still works; AC_MIN_AUTH_VERSION clamps the floor; devices upgrade in place via POST /api/auth/upgrade.
LAN scan-to-add + opt-in beep
GET /api/units/scanTCP-scans ports 6440–6449 across the server's private /24 so clients can offer "pick a found unit" next to manual IP entry.POST /controlnow honours an optionalbeep(silent by default).
This asset
breeze-core-3.0.0.pre-1.x86_64.rpm — self-contained (no Python needed), for RHEL/Fedora/Alma/Rocky/openSUSE (x86_64). Installs the hardened breeze-core systemd service. Smoke-tested: installs and runs, reports Breeze Core 3.0.0.pre.
Pairs with the Breeze app v2.0.0 prerelease (APK) for the redesigned UI. Built from overhaul @ 6d477ab.
Breeze Core v2.6.1
Patch release: fixes a silent SELinux failure in the native packages on RHEL, Fedora, AlmaLinux, Rocky, and SELinux-enforcing SUSE.
The bug
The packaged binary lives in /usr/lib/breeze-core/, which SELinux labels lib_t — not a domain-transition entrypoint. The service therefore ran as init_t instead of unconfined_service_t, and init_t's writes to /etc/breeze-core are denied and dontaudit'd: the server starts and reads its config fine, but approving a device pairing (or saving programs) fails with a 500 and nothing in the audit log. Found live by breeze-core diag during the maintainer host's own migration to the rpm.
The fix
Package post-install (and the tarball install.sh) now label the executable bin_t (semanage fcontext + restorecon, chcon fallback) — guarded by selinuxenabled, a no-op everywhere else. Already-installed 2.6.0 on SELinux hosts: one-time manual fix in TROUBLESHOOTING.md.
Debian/Ubuntu/Arch/Alpine/OpenWrt users are unaffected (no behavior change). Repo users: this arrives as a normal upgrade. No API changes.
Linux package assets are attached by CI — give it a few minutes if the list below is short.
Breeze Core v2.6.0
The diagnostic and approval tools are now built into the binary — every native package ships them, no zsh/curl/jq needed.
New subcommands
breeze-core diag # the full health/security battery (self-pairs on the LAN)
breeze-core approve <CODE> # approve a device pairing (admin, LAN-only)
breeze-core devices # list enrolled device tokens
breeze-core revoke <id> # revoke one device
diag is a faithful port of tools/ac-diag.zsh: connectivity, server version/build/features, auth posture, self-enrolment with a cached token, paired-device expiry warnings, config secret-sanitisation, batch state, input validation, per-unit state/latency/enum checks (optional control round-trip), and scheduler status — same flags (--auto, --unit, --pair, --no-pair, --token, --forget-token, --with-control-test), same output shape, exit code 0 = healthy.
Details that matter:
- Pure HTTP clients, like the zsh originals — the CLI never touches server internals, so the API stays the only contract.
- Shared token cache with the zsh tools (
~/.config/ac-diag/token) — pairing once covers both. - On source and Windows installs the same commands are available as
python -m meow_ac.cli …; the zsh scripts remain for anyone who prefers them.
Downloads
Grab packages below, or better: use the package repository and this arrives as a normal apt/dnf/pacman/apk/opkg upgrade. Windows installer attached. No API or wire-contract changes — a drop-in update.
Linux package assets are attached by CI — if you're very early to this page, give it a few minutes.
Breeze Core v2.5.0 — all-in-one packages for (almost) everything
Breeze Core no longer requires Python on the target machine. Every release now ships self-contained builds — server, web UI, and a private runtime in one bundle — wrapped as native packages for 15+ distros, with the same ease of setup the Windows installer has had.
Install it your way
| You run | Grab | Install |
|---|---|---|
| Debian, Ubuntu, Mint, Pop!_OS, Raspberry Pi OS, Armbian, Devuan | .deb (amd64/arm64) |
sudo apt install ./breeze-core_*.deb |
| Fedora, RHEL, Alma, Rocky, openSUSE Leap/Tumbleweed, SLE | .rpm (x86_64/aarch64) |
sudo dnf install ./breeze-core-*.rpm |
| Arch, Manjaro, Artix | .pkg.tar.zst |
sudo pacman -U … |
| Alpine | .apk |
sudo apk add --allow-untrusted … |
| Void, Gentoo, anything else | .tar.gz (glibc/musl × amd64/arm64) |
unpack, sudo ./install.sh |
| NixOS / Nix | the repo is now a flake | services.breeze-core.enable = true; via the NixOS module |
| Windows | Breeze-Core-Setup.exe |
double-click |
Then: sudo breeze-core pair → set your LAN IP in /etc/breeze-core/breeze-core.env → sudo systemctl enable --now breeze-core. Full guide: docs/PACKAGES.md.
What's inside
- A
breeze-corebinary withserve/pair/versionsubcommands; glibc builds made on AlmaLinux 8 (run on anything ≥ RHEL 8 / Debian 11 / Ubuntu 20.04), musl builds for Alpine/Void. - Hardened service out of the box: unprivileged
breezeuser, locked-down/etc/breeze-core, the same systemd sandbox as the source install (verified exposure score 3.0), OpenRC/runit variants included. - Every package is install-tested in CI on 15 real distro userlands (incl. Mint, Devuan, SLE, Artix, Void-musl, Gentoo and an arm64 pass) before release.
- Hybrid extras for packagers: AUR-style source
PKGBUILD, Gentoo-binebuild, Voidxbps-srctemplate (packaging/source/). - FreeBSD/NetBSD/OpenWrt stay on the documented source install for now (native poudriere builds are planned).
The classic from-source install, Docker images (incl. x86-64-v2/v3), and everything from 2.4.x are unchanged. No API or wire-contract changes — a drop-in update.
Linux package assets are attached by CI (the Packages workflow) — if you're very early to this page, give it a few minutes.