Skip to content

Releases: monikapurpl3/breeze-core

Breeze Core v3.2.0

Choose a tag to compare

@monikapurpl3 monikapurpl3 released this 21 Aug 19:44

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".
  • settings is 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's apply() 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.sh says it needs root instead of failing on a bare cp.

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 touch packaging/ only, so the server and panel they contain are the
    tagged ones.
  • FreeBSD .pkg and NetBSD .tgz are 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, and winget has a 3.2.0 manifest.

Breeze Core v3.1.0

Choose a tag to compare

@monikapurpl3 monikapurpl3 released this 18 Aug 21:19

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/system knows, 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

  • riscv64 remains 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 .pkg and NetBSD .tgz are 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

Choose a tag to compare

@monikapurpl3 monikapurpl3 released this 16 Aug 10:11

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-For and 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.boottime as 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, /work is just empty), the packaging step now streams inputs and artifacts over stdio instead.
  • build-installer.ps1 is 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

Choose a tag to compare

@github-actions github-actions released this 13 Aug 16:43

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 Unauthorized access 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 error code and a retryable flag: clock_skew, replay, incomplete_signature are retryable; unknown_key, expired, bad_signature, no_credential, bad_api_key are definitive. A clock_skew rejection also returns server_time, so a client can measure its offset and re-sign instead of re-pairing. unknown_key and expired are now distinguished.
  • The shipped fail2ban configs banned legitimate users. The filter counted 400|401|403|404|405|422|429 at 5 hits per 10 minutes, escalating to a five-week ban — but 404 is a client feature-detecting endpoints, 422 is input validation, and 429 is 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/403 only, 20 per 10 min, 1 h capped at a day, tripwire needs 3 hits, and ignoreip documents 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

Choose a tag to compare

@github-actions github-actions released this 23 Jul 13:10

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=2 to require v2.
  • Network scan-to-add. GET /api/units/scan finds 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. beep is 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), state history, and a Prometheus /metrics endpoint 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/stream pushes 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 .pkg and a signed pkg repo; NetBSD a binary package + pkgin repo. Fixed an rc.netbsd bug (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)

Pre-release

Choose a tag to compare

@monikapurpl3 monikapurpl3 released this 22 Jul 14:23

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 SSEGET /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. Emits event: state frames + keepalives; compression forced off and X-Accel-Buffering: no so it streams cleanly through proxies.
  • Per-unit capabilitiesGET /api/units/{id}/capabilities: what the hardware supports (modes, flap axes, eco/turbo, temp range, …) so clients hide controls a unit lacks.
  • whoamiGET /api/auth/whoami: the calling device's own record (label, auth version, expiry).
  • History + PrometheusGET /api/units/{id}/history (in-memory graph data) and GET /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)

Pre-release

Choose a tag to compare

@monikapurpl3 monikapurpl3 released this 21 Jul 20:37

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/scan TCP-scans ports 6440–6449 across the server's private /24 so clients can offer "pick a found unit" next to manual IP entry.
  • POST /control now honours an optional beep (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

Choose a tag to compare

@monikapurpl3 monikapurpl3 released this 10 Jul 19:52

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

Choose a tag to compare

@monikapurpl3 monikapurpl3 released this 10 Jul 19:06

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

Choose a tag to compare

@monikapurpl3 monikapurpl3 released this 10 Jul 09:39

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.envsudo systemctl enable --now breeze-core. Full guide: docs/PACKAGES.md.

What's inside

  • A breeze-core binary with serve / pair / version subcommands; 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 breeze user, 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 -bin ebuild, Void xbps-src template (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.