project overhaul: introduce kegboard v4, an esp32 + esphome based device - #20
Open
mik3y wants to merge 37 commits into
Open
project overhaul: introduce kegboard v4, an esp32 + esphome based device#20mik3y wants to merge 37 commits into
mik3y wants to merge 37 commits into
Conversation
Pours are now assembled on-device. Kegbot Server's API already accepts a finished pour (POST /api/taps/<meter> with ticks/duration/pour_time/now), so the board detects the pour, applies calibration, and posts a drink directly: no kegbot-pycore, no usb tether, and a server outage costs a retry rather than a pour. - legacy avr firmware, the python kbsp library, eagle files, and the old sphinx docs leave main; all preserved on the `arduino` branch - adds the framework-agnostic core -- pour_session, tick_series (bounded <offset_ms>:<ticks> series), kegbot_request, ring_queue -- carrying no esphome, arduino, or esp-idf headers; 471 assertions run on a plain host compiler in ~1s, and check-core-purity.py keeps the boundary honest - drink posts omit volume_ml by default: the server already stores ml_per_tick per meter behind a calibration ui, and two sources of truth for volume reliably produces confusing data. opt in via send_volume - pour timestamps fall back to a monotonic uptime pair when the clock has never synced; the server only uses (now - pour_time), so a board that has never seen ntp still reports accurate pour times - queue overflow evicts oldest and counts the loss, bounding a long outage and making the data loss visible rather than silent - test output is scoped by os and arch, since the repo is often shared between a host and a container over a bind mount; -Werror requires STRICT=1, which ci passes - tooling mirrors esphome's, since these components compile into their tree: clang-format v13 with their .clang-format verbatim, ruff, yamllint, via pre-commit - relicensed mit to match the rest of kegbot, clean because every gpl-licensed file left main. built images stay gplv3 (esphome's runtime); the `arduino` branch keeps gplv2-or-later - reporting is http only for now; mqtt, ble, and websocket are planned
First half of the ESPHome adapter layer. The meter counts pulses in an ISR and hands them to the kbcore pour state machine; all the pour detection logic stays in the unit-tested core. - core moves from namespace `kegboard` to `kbcore`. ESPHome components live in `esphome::<name>`, so a global `kegboard` namespace would be shadowed from inside the component and every reference would need a leading `::` - hub derives a serial number from the last three mac bytes, giving an unconfigured board a stable identity that survives reflashing. meters default to `<serial>.flow<index>`, which is the name kegbot server keys a tap by - hub takes its wall clock as a callback rather than a time::RealTimeClock*: esphome only copies headers for loaded components, so including the time header would break every config without `time:` - meter exposes total/volume/flow_rate/pouring entities, on_pour_start and on_pour_end triggers, and reset_total/end_pour/set_calibration actions - sensor updates are throttled to report_interval during a pour but always published on a pour boundary - ruff ignores E501; the formatter owns line length, and it only fires on lines it cannot split
Posts finished pours and temperature readings to a kegbot server. This is what replaces kegbot-pycore. Delivery is queue-first: a completed pour is enqueued, never posted inline. The meter's loop stays responsive, and a server that is down, slow, or unreachable costs a retry rather than a pour. - meters register through add_meter() rather than yaml automations, so a pour cannot be lost to a forgotten `on_pour_end:` block - pours are sent before temperatures, and only one item per loop iteration: each send blocks, so draining a backlog at once would stall meter polling and lose ticks - 4xx responses are discarded rather than retried. a bad key, an unknown meter, or a tap with no keg cannot be fixed by trying again, and an unknown meter is the normal state before a tap is configured, so retrying would block the queue forever - failures back off exponentially from retry_interval to a 5m cap; a new pour clears the backoff and attempts immediately - installs the hub's clock source, since this is the component that already depends on `time` - optional queue_depth and dropped diagnostic sensors. a non-zero dropped count means pours were lost and is worth alerting on
- boards for esp32-s3-devkitc-1 (reference), esp32dev, and esp32-c6, each with a pin map. meter pins avoid strapping, flash, native-usb, and the input-only 34-39 range, which has no internal pull-ups and so cannot bias an open-collector meter - packages/base.yaml carries wifi with ap fallback, ota, sntp, and the http client - worked examples for kegbot server and for home assistant alone - ci runs core unit tests, pre-commit, config validation, and an esp-idf compile matrix; esphome is pinned so a release cannot land on a contributor mid-pr - a weekly job builds against esphome dev, so their breaking changes show up as a scheduled failure rather than a user bug report
M4 is yaml over stock components; no new c++. - each relay gets a watchdog: a relay left on is usually a solenoid valve held open, and if whatever turned it on stops talking, nobody is left to turn it off. scripts use mode: restart, so re-issuing the on command restarts the countdown rather than stacking timers, matching the legacy set_output behaviour - buzzer plays the avr firmware's boot/auth/ping melodies, transcribed from tones.h to rtttl - boards gain led, rfid rx, and auth-bus pin substitutions
One grant at a time: the person at the tap is whoever presented the most recent token. Covers both reader styles -- a momentary rfid scan expires on its own, a held ibutton is revoked on detach. - a different token replaces the grant and clears the resolved username, so a newcomer cannot inherit the previous holder's tab - detach only revokes if the token matches the active grant; a stale event from another reader cannot close someone else's tap - extend() pushes expiry out, used to keep the valve open under a pour that outlasts the grant window - expiry math survives the 32-bit millis rollover - 46 assertions; 517 total across the core suites
kegboard_auth turns token events from any reader (rdm6300, wiegand, pn532, kegboard_onewire) into a grant: resolves the user against /api/auth-tokens, opens the flow toggle, and attributes pours to the resolved username. kegboard_onewire adds the arrive/leave events that esphome's one_wire bus lacks, which is the entire point of an ibutton reader. - authorization is decided on-device: the tap works through a server outage and the valve opens at local speed. the trade is that a server-side revocation stays valid until the next lookup - require_known_token defaults false: an unregistered fob pours as guest rather than doing nothing. set true to lock the tap - an active pour extends the grant, and revocation ends any pour in flight before clearing the username, so the drink still lands on whoever poured it - ibutton detach requires 4 consecutive missed searches (the avr firmware's number): a held fob makes intermittent contact, and reporting on the first miss would flap several times a second - meter pour callbacks now carry the active username; the reporter passes it through to the drink post - action/condition overrides now match Action::play(const Ts &...) -- by-value signatures only collapse to the same overload when the template pack is empty, so the meter actions compiled until the first automation with arguments instantiated one - id-only actions (revoke, reset_total, end_pour, is_authorized) accept the bare-id shorthand via maybe_simple_id - examples/kegbot-full.yaml wires all of it: two gated taps, valve relay, buzzer, leds, rfid, and ibutton
esphome's host platform compiles the components with the system compiler in seconds -- a full c++ type-check with no esp toolchain download. ci runs it as the first gate ahead of the esp32 matrix. - the smoke config deliberately exercises actions invoked from automations with arguments, exactly where an override-signature mismatch hides from the plain examples - meter's isr drain guards InterruptLock with #ifndef USE_HOST; the host platform implements no interrupts to mask (and the "isr" never runs there, so the unlocked read is equally correct) - kegboard_onewire is absent from the smoke config: esphome's own gpio one_wire does not link on host for the same InterruptLock reason. the esp32 matrix covers it via kegbot-full - the weekly esphome-dev canary now builds kegbot-full
This was referenced Aug 3, 2026
Closed
Defines the reporting protocol that replaces both the legacy kbsp serial protocol and the legacy pykeg http api. One endpoint (POST /kegboard-event), json batches with normative json schemas for request and response, bearer auth. - device is authoritative for volume: volume_ml always present and computed from on-device calibration; ticks/ml_per_tick/tick_series ride along as diagnostics only - at-least-once delivery with idempotent processing: events dedup on (device, boot_id, id); a retried batch can never create a duplicate drink. pour_id is a separate globally unique opaque string, usable as-is server-side (event ids stay boot-scoped integers; rationale in the doc) - age_ms is the authoritative time signal, recomputed at each send, so queued events deliver late with correct timestamps and a device with no synced clock still reports accurately - pour_update events give a live pour view (tunable rate, default 1s); best-effort and exempt from delivery guarantees - status heartbeats (default 1m) carry a config object so operative device settings are server-discoverable, plus events_dropped and per-meter totals so data loss and missed pours are detectable - pairing: an unprovisioned device announces itself and appears on the server dashboard for allow/deny; the token is provisioned in-band and the user never transports a credential. any 401 re-enters pairing, so key rotation is revoke-then-reallow - commands ride the response to device-initiated requests (no inbound connections): authorize/deny/deauthorize are specified in the authenticated-pouring doc; grants are per meter, the server decides the meter set, and the device clamps grant duration (default 5m) as the final bound on valve-open time - offline_policy governs token presentment during an outage: deny (default) or guest, which attributes but never opens a valve
The kbcore side of the kegboard event protocol: json writer, payload builders for every event type, batch envelope serialization, and boot_id/uuid formatting. Schemas extracted from the doc appendices to schemas/ as the normative files. - payloads are rendered at event creation; only the envelope is rendered at send time, so age_ms is recomputed per (re)send and a queued event delivered late still lands at the right time - ci proves conformance end to end: check-events-schema.py drives the test binary's emit mode and validates every produced document against the schema file, and asserts the schema files still match the doc appendices, so doc, schema, and code cannot drift apart silently - json writer is write-only, deterministic, and escapes correctly (quotes, control chars as \uXXXX, utf-8 passthrough); nan/inf floats serialize as 0 rather than corrupting the document - pour ids format as uuidv4 from injected random bytes, keeping the core free of platform rng and the tests deterministic
mik3y
force-pushed
the
mikey/kegboard-esp32
branch
2 times, most recently
from
August 3, 2026 17:15
2b9fed5 to
c1db065
Compare
Replaces the legacy pykeg client stack (kegboard_kegbot, kegbot_request, auth_session) with the kegboard event protocol end to end: a reporter that batches events to the configured reporting url, per-meter server-decided authorization, and pairing. kegboard_reporter: - reporting_url is a full url, path included, used verbatim - queue-first delivery with backoff; pours and token events reset backoff for an immediate attempt, heartbeats and command acks wait their turn; non-401 4xx drops the batch and counts the loss - pairing is the only way a device gets a credential: unauthenticated batches until the server allows the device, provisioned token persisted in flash, and any 401 (revoked or rotated token) drops it and re-enters pairing - commands dispatched from every 2xx response with dedup on command id and command_result acks; the auth component registers the handler - pour_update events at a tunable cadence (default 1s), best-effort: sent only when healthy, never queued, never counted as dropped - status heartbeats (default 1m) carry config, rssi, dropped count, and per-meter totals kegboard_auth: - per-meter grants (kbcore::GrantTable) replace the single-grant session: alice on meter 0 and bob on meter 1 coexist, and the server decides each grant's meter set, user, and duration - server mode sends every presentment; authorize/deny ride back in the same round trip. offline_policy deny (default) or guest, which attributes but never opens a valve - duration clamp (max_grant_duration, default 5m) bounds valve-open time regardless of what the server asks for - local mode accepts every token as guest, serverlessly - reporter binding is automatic when a single kegboard_reporter exists, like every other *_id reference; server mode with none fails validation with a pointer to `mode: local` kegboard_meter: - pours carry a uuid pour_id generated at pour start, plus full attribution (user, auth_device, auth_token) captured at pour end - `index` is now `meter_number`, matching the protocol field: the yaml `id` is a config-internal reference and never on the wire, and the docs now say so. duplicate meter numbers fail config validation -- the default is 0, so a multi-meter config that forgot to set it would have reported every tap as meter 0 and the server would silently merge them - `meter_name` removed: it built the legacy `<serial>.flow<n>` kegbot naming, which died with the pykeg api The legacy pykeg api client and its core modules are gone; examples, host smoke test, and readme updated. Whole stack compiles natively on the host platform and every emitted document validates against the protocol schemas in ci.
mik3y
force-pushed
the
mikey/kegboard-esp32
branch
from
August 3, 2026 17:15
c1db065 to
6281e78
Compare
|
This is awesome. This exactly what I was thinking of doing if my current setup ever died. |
Member
Author
@scottresnik awesome, thanks for the quick gut check! (and even more awesome your system is still running!) I should have linked to the forum thread here, too. |
- pour.pour_id is required; the device always generates one, so the "SHOULD, MUST if updates" caveat described an impossible state - pour_update.rate_ml_per_min removed: derivable from volume/duration, and better from deltas between successive updates - response `commands` is optional; absent means none - status.meters[].ml_per_tick is required; ml_per_tick: 0 is now a config-time error
A device without a token sends no Authorization header, and a server that accepts unauthenticated batches never triggers pairing -- the minimum receiver needs no auth machinery. Only a 401 introduces auth, and later serves as revocation. - pairing section reframed to match: it begins when a server 401s - fixes healthy_() requiring is_paired(), which silently suppressed pour_update events against auth-less servers
age_ms already covers queued pours (envelope field, recomputed per send); now documented that it measures to the pour's end, with start = end - duration_ms.
mik3y
force-pushed
the
mikey/kegboard-esp32
branch
from
August 3, 2026 17:49
8682951 to
3187693
Compare
Silences the register_action synchronous= warnings. All six actions do their work inline in play() -- nothing defers to a callback or loop() -- so synchronous=True is correct and enables the StringRef optimization.
Member
Author
Success was silent while failures were loud, which made bring-up blind: no way to see what the device actually sent. - ESP_LOGD per POST: status, event count, byte size - ESP_LOGVV: full request and response bodies; compiled out at default log levels, so production builds pay nothing
Speaks the full event protocol so the server side can be built without hardware: pairing (pending/allowed/denied, token persisted per url+device), batching with recomputed age_ms, command dispatch with dedup and command_result acks, per-meter grants with pour attribution. Validates every outgoing batch against schemas/ so it cannot drift from the protocol. Single-file, runs via `uv run` with inline deps. Beyond the basics (pours with live updates and slight per-pour variance, temperature logging, preset tokens incl. an unknown fob and a presence ibutton), it can misbehave on purpose: - kill the heartbeat so server liveness detection can notice - go offline to build a backlog that delivers late with correct ages - replay the last batch verbatim to exercise server dedup - send an unknown event type (server must 2xx and ignore) - reboot: new boot_id, ids reset ui: a right-hand command palette lists every key with a short muted description (? minimizes it to the bottom bar; keys, footer labels, and help text share one table so they cannot drift). the traffic log shows each request body as sent and each json response; non-json bodies summarize to one line. ctrl-c quits. Verified headless end to end against an inline receiver (19 checks: pairing flow, attribution, dedup, offline aging) plus textual pilot smokes of the ui.
The esp32 rewrite is the project's 4th generation: v1 was pic16-based (written in jal), v2 arduino, v3 the kegboard pro mini, v4 this. Prose says v4, a history section at the bottom of the readme records the lineage with dates, and version strings follow to 4.0.0-dev (firmware, project metadata, sim, doc examples).
- new docs/ manual: overview, operating modes, installation, hardware, configuration, theory of operation, developer notes - protocol specs unchanged, pulled in as an appendix toctree - sphinx + myst + furo, matching docs.kegbot.org conventions; publishes as the kegboard subproject via .readthedocs.yaml - docs/ is a self-contained uv project; make -C docs html / livehtml (liveserver on port 8010) - README: docs pointer + build instructions - examples: kegbot-2tap on the classic devkit board WIP: copy edits pending before publish.
- meters and the new relays array are exhaustive hardware inventories, so a server can allocate and retire port records automatically; retirement guidance says warn, don't delete, for operator-configured ports - self-explanatory field names: meter_number and relay_number in objects, meter_numbers for the authorize/deauthorize arrays; the reserved set_output command becomes set_relay - "relay" replaces "toggle" throughout (matches the hardware docs, relay0/relay1 ports, and esphome entities) - schema and simulator updated to match - drop the DRAFT note
- authorize/deny/deauthorize are now §7.1–7.3 of the protocol doc, which is meant to be the exhaustive contract; authenticated-pouring keeps only a flow-level summary of their roles - reserved command types get their own §7.4 - response schema (Appendix B + in-repo) now types the three command payloads via per-type $defs with if/then dispatch, mirroring the request schema's event pattern
- authorize carries exactly one grant: a server-assigned grant_id, meter_numbers/relay_numbers sets (the meter↔relay association stays on the backend and travels in each command), and limits — max_volume_ml, max_duration_ms, max_idle_ms, 0 meaning unlimited, with the device clamp always bounding total lifetime; re-using a live grant_id updates the grant in place, counters intact - identity leaves the wire: no user field anywhere — pours and grant endings carry grant_id plus the token echo, and the server resolves the user itself - deauthorize revokes by grant_ids; absent means every active grant - new grant_end event reports every ending with a reason (max_volume, max_duration, max_idle, detach, command, replaced); totals are snapshots, and the final pour always precedes its grant_end - duplicate commands are re-acknowledged, not just deduplicated - authenticated-pouring restructured: background and summary up front, everything optional for monitoring-only boards, and a corner-case catalog (§9) for pours × grants — guest pours, mid-pour adoption, splitting on replacement, offline-guest scope - schemas match the appendices byte-for-byte; config docs speak "relay" uniformly and gates become local-mode-only
- GrantTable rewritten around grant objects: server ids, meter/relay sets, limits with the clamp spanning updates, per-meter takeover, and every ending returned with its reason - kegboard_auth validates grants against the device inventory (unknown meter/relay acks error, not applied), feeds live flow into the limits so a volume cap closes the valve mid-pour, ends pours before queueing grant_end, adopts in-flight pours at an explicit policy point (authenticated-pouring §9), and revokes by grant id — malformed grant_ids never read as the emergency stop - reporter gains the numbered relay registry (relays option + status inventory), grant_end emission, duplicate-command re-acks, and response reads that no longer trust Content-Length - identity is gone from the device: pours carry grant_id instead of user, token events and triggers drop user, the user entity is removed, on_authorized fires with (auth_device, token) - event payloads emit meter_number as the schema requires (the old "meter" key never validated); status self-describes relays - gates are local-mode-only and ignored in server mode; local mode requires at least one gate (relay optional); grant durations are bounded to keep the rollover math valid; detach matches the auth_device/token pair and ignores empty tokens - grant table and event builders covered by host tests; the host smoke config and examples exercise the new surface
- authorize/deauthorize by grant: sets, limits, update-in-place, and inventory validation, mirroring the firmware - limits enforced live: volume cutoff mid-pour, idle and duration watchers, with the pour event always preceding its grant_end - pour lifecycle matches the corner-case catalog: adoption mid-pour, splitting on replacement, zero-volume pours discarded as drips - duplicate commands re-acked, detach matches the device/token pair, grant endings flush promptly, reboot forgets applied command ids
Local mode let a serverless board accept every token as guest on config-wired gates. It was thin as access control, it kept the one remaining device-side copy of the meter↔relay association alive, and a serverless install can reproduce it with plain ESPHome automations on the reader triggers. We may bring a device-decided mode back some day — perhaps with a proper allow-list — but for now authorization simply means the server decides, and everything is smaller for it. - kegboard_auth loses mode, gates, and local_grant_duration; it now requires a kegboard_reporter, and its meter inventory is the reporter's - the device-decided grant machinery goes with it: no GrantSpec.local, no internal grant ids — every grant is server-issued, so grant_end now requires grant_id - offline_policy: guest simplifies to "stay silent": nothing opens, nothing is granted, pours proceed as ordinary guest pours, and the queued token event preserves the audit trail — the only difference from deny is the missing refusal signal - the token event loses its status field (local decisions were its only producer): an attached event is always a question for the server - docs: modes collapse to authenticated vs open, the local-mode section and the offline-guest corner case disappear, and the corner-case catalog renumbers to §8 - schemas, examples, smoke config, tests, and the simulator follow
…gure around it ESPHome deduplicates repeated switch publishes, so a redundant turn_on does not re-fire on_turn_on — the watchdog timer runs from the on-edge and cannot be refreshed while the relay is on. kegboard_auth energizes a grant's relays once, so a 10s watchdog was silently closing granted valves mid-grant while attribution carried on. - kegbot-full example raises relay_watchdog_timeout to 6min, above the 5min grant clamp: the clamp bounds grant-held relays, the watchdog covers manual/HA toggles - relays.yaml drops the false "re-issuing the on command restarts the timer" claim and documents the grant-driven guidance - operation, configuration, hardware, overview, and README stop claiming the two limits compose independently and state the rule: watchdog longer than max_grant_duration (or 0s) on granted relays
Protocol §9 grants immediate-attempt privileges to pours and tokens only, but the thermo callback used enqueue_'s default reset_backoff, so a device with a 60s sensor retried a down server every 60s forever instead of backing off to the 5min cap.
The option was renamed long ago; copy-pasting the snippet failed validation.
- overview, operating-modes, operation, and the offline-behavior heading stop naming a `server` mode / local decisions that no longer exist - meter and reporter header comments and the sim drop mentions of locally decided grants and the removed token status field - core comments (hub identity, serial-number validation, tick series) cite the v4 protocol -- (device, meter_number) tap identity and tick_series -- instead of retired pykeg-era behavior
When a grant ends or is replaced mid-glass, the firmware ends the pour and keeps metering — the sim's pour task just stopped, so receivers never saw split-pour traffic (authenticated-pouring §8, cases 3 and 5). The pour now runs in segments: a grant boundary finishes the current pour and the remaining flow opens a fresh pour_id under whatever covers the meter — the new grant, or nobody (a guest pour). - tick_series entries become per-interval deltas that sum to the pour's ticks (they were cumulative/steps, adding up to about half) - gitignore uv.lock (a tooling artifact)
- healthy-and-paired now flushes whenever the queue is non-empty, like the firmware, so command acks and grant endings no longer sit out the heartbeat interval - authorize limits are type-checked (numeric, non-negative) and acked `error` on garbage instead of crashing the dispatch task or poisoning grant state; meter/relay numbers reject booleans (JSON true == 1)
The reporter's spec surface (status-code handling, backoff, pairing cadence, command dedup/re-acks) and the grant/pour composition (adoption, splits, limit trips, pour-before-grant_end ordering) lived only in ESPHome-bound components, where the host suite could not reach them — the recent temperature-backoff bug is exactly the class that gap allows. Both state machines now live in kbcore. - kbcore::Delivery owns attempt timing: the status table, exponential backoff with cap and reset rules, pairing fast/slow polling, denied state, and the command ledger for dedup and re-acks - kbcore::AuthEngine owns grant semantics end to end — inventory validation, replacement/updates/adoption, live flow accounting, relay refcounting, endings — driving the device through callbacks - kegboard_reporter and kegboard_auth become thin adapters: HTTP, JSON parsing, entities, triggers, and logging - new host suites: test_delivery (backoff/pairing/ledger, rollover) and test_auth_engine (the corner-case catalog against a fake board, including ordering, no-double-count true-up, and the grant-killed- while-applied reentrancy case) - purity checker and CORE.md cover the new files
- packages/base.yaml http timeout drops to 5s: it bounds how long someone stands at the tap waiting for an offline decision, and the authenticated-pouring doc already suggested 5s - protocol doc: commands ride every 2xx exchange, authenticated or not - configuration doc: one_wire_id is auto-bound with a single bus, not required - heartbeat_interval gains a 1s floor to match the status schema - PLAN-kegboard-esp32.md is marked historical: several designs in it (local gating, token caches, API keys, a flash queue) were considered and rejected, and the docs and code supersede it - code comments largely stop citing doc section numbers: the code is canonical over time, so comments say what the rule is rather than where the doc states it; pointers to doc files remain at file level - drop the empty kegboard_kegbot remnant
Covers the authorize branch where an update sheds every one of the target grant's old meters: the grant must survive with counters and age intact, keep its relays (a partial ending releases nothing), and cover the new set.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

This branch introduces the fourth major redesign of Kegboard in ~22 years 😯. Major changes:
esphome, as a collection of custom components. While using this framework is not strictly necessary for kegbot purposes, esphome gives us a great framework with some useful adjacent components (like a wifi captive portal), and makes it easier to use a kegboard with an alternative backend such as a home assistant server.As with the previous generation of kegboards, all core features are supported, including:
More background and potentially more discussion in the community forum thread here.