Skip to content

HiveTraffic night mode: schedule, traffic gate, config and UI - #167

Merged
MacNite merged 4 commits into
mainfrom
claude/night-mode-review-setryb
Aug 21, 2026
Merged

MacNite merged 4 commits into
mainfrom
claude/night-mode-review-setryb

Conversation

@MacNite

@MacNite MacNite commented Aug 19, 2026

Copy link
Copy Markdown
Owner

The HiveHub half of night mode. Pairs with MacNite/HiveTraffic#14, which adds the device-side suspension. Deploy order below.

Why

A HiveTraffic counter's 48 IR emitters are its power budget — 24 series pairs behind 22 Ω ballast, ~0.5–1.0 A peak, an order of magnitude above everything else on that board. European honey bees are diurnal (flight needs light, and stops below roughly 10 °C regardless), so overnight that current buys nothing.

The split

HiveHub owns the schedule because the counter cannot: it has no RTC, no NVS and no network, while this device has NTP and a DS3231 good to ±2 ppm. Once per cycle the existing GATT session carries one extra 5-byte write telling the counter how long to stop sensing — no extra scan, no extra connect. The counter forgets it on any reset and it expires on its own, so a HiveHub that dies mid-night cannot leave a counter blind.

Fails safe by construction

firmware/include/night_mode.h is a pure header with host tests, because this goes wrong quietly in both directions: suspend when you should not and a day's foraging becomes a row of zeros that reads as bad weather; fail to suspend and nothing happens except a flat battery three weeks later.

Every uncertain path returns keep counting — no valid local time, no traffic baseline, a window whose ends are equal, a relay in flight. The worst outcome of a bug here is a counter that counts, which is what it did before.

Two rules worth flagging as decisions rather than details:

  • start == end is an empty window, never a 24-hour one. The other reading would let one mis-set field stop a counter for good, an hour at a time, with nothing in the data saying why.
  • No traffic baseline postpones rather than proceeds. "I don't know how busy this hive is" is not "it is quiet". Costs one cycle after a reboot.

The traffic gate

The "not yet" rule that was asked for: if more than N bees crossed in the last cycle, night mode waits. It differences the previous cycle's totals, held in RTC memory across HiveHub's own deep sleep — without that there would never be a previous reading and the gate would postpone forever. Reboots are attributed the same way server/measurements.py does, so the gate and the charted interval never disagree.

Timezone is load-bearing, not cosmetic

The device clock is UTC (configTime(0, 0, …)). Without a TZ a window entered as 20:00 fires at 21:00 local in summer, discarding an hour of foraging on exactly the long evenings that have most of it. Stored as a POSIX TZ string because the ESP32 carries no tz database; the dashboard offers presets for the common zones plus free text.

Scope

  • Ships OFF. An existing device is unaffected until it is turned on.
  • Settings live in device_configs (migration 025), edited under Device & admin.
  • Applied on every config fetch, not behind the config_version gate — that gate exists to protect a portal-side tare, and night mode has no portal counterpart. Turning the window off has to take effect next cycle, not after an unrelated edit bumps the version.
  • Persisted in NVS, so a hub that boots without WiFi still honours the last known window — which is precisely the deployment this is for.
  • Mirrored into the public demo dashboard so it stays representative.

Wire revision

The parser learns fw:4 (idle_s, status bit 0x80) and keeps reading fw:2/fw:3. The addition is purely additive, so it already read v4 documents correctly — what it could not do is tell a suspended counter from a broken one, which is the point of reading it.

The "relay at night" interaction

docs/hivetraffic-bee-counter.md recommends relaying firmware at night because a transfer costs counted bees. Deep sleep would have made that the one time of day a counter is unreachable. A suspended counter stays advertising, so relays run exactly as in daylight; HiveHub additionally declines to suspend a counter whose relay is unfinished.

Verification

Run locally, all green:

  • firmware/host_test/run_tests.sh — 111 + 35 checks
  • test_ruuvi_decode, test_beehive_decode, test_bee_counter_wire, test_night_mode (new)
  • test_counter_rules, test_accel_rules, test_ble_sensor_rules, test_sd_import, test_data_export
  • 38 pytest cases across test_tempcomp / test_hive_tempcomp / test_hiveheart_fft / test_relay_ota_gate / test_publish
  • test_pairing_lifecycle --require-db against a real PostgreSQL 16
  • node --check on every dashboard/website script

Also verified against a live PostgreSQL, beyond what CI covers: the config round-trip (defaults off, partial patch, config_version bump, pre-existing fields not shifted by the new columns), the CHECK constraints, and migration 025 applied to a pre-025 device_configs — twice, to prove idempotency, with the pre-existing row landing on night mode off.

Not verified here: the ESP32 firmware build. The sandbox blocks the PlatformIO registry, so firmware-build in CI is the first compile of the firmware changes.

Deploy order

This PR must ship before any counter running HiveTraffic#14, since HiveHub is what decides when to suspend and needs to understand idle_s first.


Generated by Claude Code

claude added 4 commits August 19, 2026 09:29
A HiveTraffic counter's 48 IR emitters are its power budget — 24 series
pairs behind 22 ohm ballast, all lit for the settle+read window of every
5 ms poll, roughly 0.5-1.0 A peak and an order of magnitude above the
~18 mA everything else on that board draws. European honey bees are
diurnal: flight needs light, and stops below roughly 10 C regardless. So
every night that current buys nothing.

HiveHub now owns the schedule, because the counter cannot: it has no RTC,
no NVS and no network, while this device has NTP and a DS3231 good to
+/-2 ppm. Once per upload cycle the existing GATT session carries one extra
5-byte write telling the counter how long to stop sensing — no extra scan,
no extra connect. The counter forgets it on any reset and it expires on its
own, so a HiveHub that dies mid-night cannot leave a counter blind.

The decision (firmware/include/night_mode.h) is a pure header with host
tests, because it goes wrong quietly in both directions: suspend when you
should not and a day's foraging becomes a row of zeros that reads as bad
weather; fail to suspend and nothing happens except a flat battery three
weeks later. Every uncertain path returns "keep counting" — no valid local
time, no traffic baseline, a window whose ends are equal, a relay in
flight. The tests cover midnight wrap, an unsynced clock, the traffic gate
and a whole simulated night cycle by cycle.

The traffic gate is the "not yet" rule the feature was asked for: if more
than N bees crossed in the last cycle, night mode waits. It differences the
previous cycle's totals, held in RTC memory across HiveHub's own deep
sleep — without that there would never be a previous reading and the gate
would postpone forever. A hive with no baseline is postponed rather than
waved through: "I don't know how busy this is" is not "it is quiet".

Timezone handling is load-bearing rather than cosmetic. The device clock is
UTC (configTime(0, 0, ...)), so without a TZ a window entered as 20:00
would fire at 21:00 local in summer, discarding an hour of foraging on
exactly the long evenings that have most of it. Stored as a POSIX TZ string
because the ESP32 has no tz database; the dashboard offers presets.

Ships OFF. Settings live in device_configs (migration 025, applied
unconditionally on each config fetch rather than behind the config_version
gate — that gate exists to protect portal-side calibration, and night mode
has no portal counterpart), and are edited under Device & admin. Persisted
in NVS so a hub that boots without WiFi still honours the last known
window, which is the deployment the feature is for.

The wire parser learns fw:4 — idle_s and status bit 0x80 — and keeps
reading fw:2 and fw:3, since a counter reports the old revision until the
OTA relay reaches it and the relay must read this characteristic first. The
addition is purely additive, so the key-skipping parser already read v4
documents; what it could not do is tell a suspended counter from a broken
one, which is the whole point of reading it.

Also settles the interaction with the standing "relay firmware at night"
advice, which deep sleep would have broken: a suspended counter stays
advertising, so relays run exactly as in daylight, and HiveHub declines to
suspend a counter whose relay is unfinished.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gFpezSuXW3sHMd1xaSYnd
Both firmware builds failed to compile: Arduino's esp32-hal-gpio.h carries
`#define DISABLED 0x00`, and the preprocessor does not care that the
enumerator lives in an enum class inside a namespace. The macro expanded
inside the enum body, the whole nightmode namespace failed to parse, and
every later use cascaded into "'struct Decision' has no member named
'reason'".

Rename the enumerators to CamelCase — which is the house style already
(ClaimStatus) and, more to the point, is outside the all-caps namespace
Arduino and ESP-IDF share.

The host test could not have caught this: it compiles night_mode.h against
a bare host compiler, where none of those macros exist, so it passed while
the firmware build failed. Fix that too — the test now defines the Arduino
macros that actually bite, with their real values, before the include. A
future identifier that strays into the macro namespace fails here in
seconds instead of three minutes into a firmware build. Verified by
reintroducing DISABLED and confirming the test stops compiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gFpezSuXW3sHMd1xaSYnd
Both were missed when the feature landed.

FIRMWARE_VERSION matters functionally, not just for bookkeeping: HiveHub's
own OTA path compares it to decide whether a release is newer, so a device
carrying night mode while still reporting 0.24.16 would not be offered the
update, and nothing would confirm afterwards that it took. Every firmware
behaviour change in this repo's history bumps it; this one adds a feature.

SERVER_VERSION moves to 0.3.1 because /config gained fields
(beecounter_night_* and timezone) and /health is where an operator reads
whether their backend understands them — a device talking to an older
backend simply gets the defaults and stays in the pre-night-mode behaviour,
which is safe but silent. Kept to a patch bump: 0.2.0 -> 0.3.0 was a
documented release with docs/releases/v0.3.md behind it, and this is one
additive feature rather than a release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gFpezSuXW3sHMd1xaSYnd
Reported as "I can't see it", and that was accurate. The fields were real
and wired up, but they were a third column inside the Configuration form's
grid — and that panel is a <details> closed by default, so reaching them
meant expanding Configuration and scrolling past scale calibration and the
tempco fit tool. Nobody looking for a bee-counter setting would look there.

Move them into a top-level collapsible of their own, next to Configuration,
Publish data and Admin, with their own submit handler and Save button so
the panel is self-contained rather than depending on a button in a
different section.

No change to what is sent: the same PATCH fields, the same "start == end
would never apply" guard, the same only-changed-fields patch. Mirrored into
the demo dashboard, and verified by rendering it in Chromium — the summary
is visible while closed, the panel opens, and the controls read back the
configured window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gFpezSuXW3sHMd1xaSYnd
@MacNite
MacNite merged commit 3524930 into main Aug 21, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants