Skip to content

Breeze Core v3.2.0

Latest

Choose a tag to compare

@monikapurpl3 monikapurpl3 released this 21 Aug 19:44
· 3 commits to main since this release

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.