Changelog

for the component ilia-ae/zignode v2.0.0
# Changelog

All notable changes to the `zignode` component are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] — 2026-07-28

Modular architecture: a shared core plus peripheral modules compiled in per-board, and **board
selection decoupled from the chip target**. Adds the Seeed XIAO ESP32-C6 Expansion Base. The
M5NanoH2 and bare XIAO-C6 behave exactly as before.

### Fixed
- **RTC century bit round-tripped 100 years off** (found on real hardware). The write path set the
  century bit for any year ≥ 2000 while the read path added 100 when it saw that bit, so a correct
  2026 came back as **2126**. The bit is now written as 0 and ignored on read — its polarity is
  documented inconsistently across PCF8563 datasheets and clones, and a watering controller only
  ever lives in 2000–2099, so `YY` alone is unambiguous.
- **A dead/absent RTC battery could set the clock decades into the future.** The driver logged the
  PCF8563's VL ("clock integrity lost") flag and then seeded the clock from the garbage anyway — a
  real board handed us `2081-12-32`. VL now aborts the read, and the decoded date is range-checked
  before use. Without a valid clock, time-of-day rules refuse to run (as designed) until Zigbee or
  SNTP provides the time, which then writes the RTC and clears VL.

### Added
- **The OLED is now a paged dashboard, not one fixed screen.** Five pages — Home (pump / soil /
  automation / usage), Zigbee (channel, PAN, parent + LQI/RSSI, "when the coordinator last saw us"),
  Wi-Fi (IP, mDNS, ping, window, antenna), System (uptime, heap, temperature, CPU, events) and Clock
  (date, source, sync age, drift, UTC offset) — each with an 8×8 icon and page-indicator pips. It
  cycles on its own every `ZIGNODE_OLED_PAGE_SECONDS` (default 5); any state change jumps back to
  the Pump page and restarts the dwell, so what just happened is what you see. **While the pump is
  running the carousel stops entirely** and the Pump page is pinned — a glance mid-run must land on
  the live timer and the moving bar, never on "heap free" while water is going somewhere. The single button is left
  alone deliberately — it is the pump/automation/factory-reset control, and paging is not worth
  putting a safety control behind a UI gesture.
- **Live clock with milliseconds**, pinned to the bottom row of every OLED page and shown in the web
  panel header. On the display this needs ~10 Hz, so only the bottom 128-byte band is rewritten
  (~3 ms) instead of the whole framebuffer (~25 ms) — the I²C bus is shared with the RTC. In the
  panel the device sends `epoch_ms` and the browser interpolates locally, rendering the **device's**
  local time (via `utc_offset`) rather than the browser's.
- **`dev` build channel** (`tools/build_board.sh <board>:dev`) — for running a second node beside
  your production one. Same firmware, distinct identity: mDNS becomes **`zignode-dev.local`** so the
  two boards don't race for the same name, and the web panel titles itself from that name. Zigbee
  needs no switch (Z2M keys devices by IEEE); rename it there once. See docs/BOARDS.md.
- **Firmware integrity checking on the Wi-Fi OTA path.** The panel hashes the `.bin` in the browser
  and declares it via an `X-SHA-256` header; the device hashes every byte it accepts and **aborts
  before `esp_ota_end()`/`esp_ota_set_boot_partition()`** on a mismatch, so a mismatched image can
  never become bootable. The digest is logged and echoed in the HTTP response regardless. Note that
  `crypto.subtle` needs a secure context, so over plain HTTP the browser cannot compute it — pass
  the header yourself (`curl -H "X-SHA-256: $(shasum -a 256 …)"`) for an enforced check.
- **Runtime image attestation** — `GET /api/status` now reports `app_sha` (the running image's ELF
  SHA-256) and `idf_ver`, surfaced in the panel as *Running image*. A version string cannot tell two
  builds of "2.0.0" apart; the hash can. See [SECURITY.md](../../SECURITY.md#firmware-integrity).

### Changed
- **⚠️ Upgrading with an existing local `sdkconfig`? Migrate it first.** The Kconfig prefix changed
  (`CONFIG_PLANT_PUMP_*` → `CONFIG_ZIGNODE_*`), and Kconfig **silently discards values for symbols it
  no longer knows** — so the first v2 build in an old working tree drops your settings, including
  **`WIFI_SSID`/`WIFI_PASSWORD`**, and produces an image that can never join Wi-Fi. Rename them in
  place before building (`sdkconfig` is gitignored, so this is purely local):
  ```sh
  cp sdkconfig sdkconfig.bak
  perl -pi -e 's/\bCONFIG_PLANT_PUMP_/CONFIG_ZIGNODE_/g' sdkconfig
  ```
- **Project renamed `plant_pump` → `zignode`** to match its broader scope — a modular Zigbee/Wi-Fi
  *node*, not only a plant-watering pump (the pump is now its flagship module). *Breaking:* the
  ESP-IDF component is `ilia-ae/zignode`; the public API is `zignode_*` (was `plant_pump_*`); Kconfig
  is `CONFIG_ZIGNODE_*`; and the Zigbee modelIds are `ZigNode.H2` / `ZigNode.C6` / `ZigNode.C6Exp`
  (were `M5NanoH2.PlantPump` / `PlantPump.C6` / `PlantPump.C6Exp`) — re-interview the device or update
  the Z2M converter (`docs/zigbee2mqtt/zignode.js`) accordingly.
- **Board is a Kconfig `choice` (`ZIGNODE_BOARD`), not the chip `#if`** — one chip can host more
  than one board. Defaults track the target. *Breaking:* a build that relied on the target `#if`
  should now select a board (menuconfig or a `boards/*.defaults` profile).
- **Module registry + event broadcast** replaces the hand-wired fan-out in `zignode.c`: each
  module is a `zn_module_t` in one explicit, ordered, `#if`-guarded array, and a single event
  (`zn_broadcast`) reaches every subscriber. Adding a peripheral is a file + descriptor + one line,
  never a core edit. Init order (pump parked first, radio last) is preserved and host-tested.

### Added
- **XIAO C6 + Expansion Base** board (`ZigNode.C6Exp`, OTA image type `0x1013`), with:
  **OLED** status screen (in-tree SSD1306 + 5×7 font), **buzzer** cues, **PCF8563 RTC** as a third
  time source (seeds the clock at cold boot; SNTP/Zigbee written back to the coin cell), and
  **microSD logging** of pump runs — RAM-buffered, batched fsync, daily files rolled + pruned, so
  internal flash is never touched. SD is off by default (its SPI pins overlap the pump; enable with
  the `ZIGNODE_PIN_*_GPIO` overrides — see docs/BOARDS.md).
- `tools/build_board.sh` + `boards/*.defaults`, host unit tests (`test/host/`), and
  `docs/BOARDS.md` + `docs/MODULES.md`.
- Clickable mDNS link in the web Zigbee panel.

## [1.1.0] — 2026-07-27

Second board, a real automation engine, and a private update path. The H2
behaviour is unchanged; everything new is either shared or gated behind
`SOC_WIFI_SUPPORTED` so the H2 image never grows.

### Added
- **Seeed Studio XIAO ESP32-C6 support** alongside the M5Stack NanoH2. A board
  descriptor (`zn_board()`, weak-linked) replaces the old `#if` pin macros, so a
  third board is a new descriptor, not an edit to the component.
- **Watering-program engine** — up to 8 rules of *trigger + conditions + action*
  (time-of-day with a weekday mask / every-N-minutes / soil-moisture threshold),
  editable over Zigbee (cursor + staged commit on cluster `0xFC00`) and, on the
  C6, over HTTP. Everything is **disabled by default**.
- **Direct Zigbee sensor binding** — a client-role Soil Moisture cluster (`0x0408`)
  lets an external battery sensor report straight to the pump for true autonomy.
- **Time source** — ZCL Time from the coordinator (both boards) and SNTP on the
  C6; time-of-day rules refuse to run (fail-closed) when no clock is available.
- **C6 Wi-Fi + web panel** — separate Zigbee / Wi-Fi / clock telemetry panels,
  a dedicated pump block with a self-stopping timed dose, rules editor, event
  log, radio stats, a gateway-reachability ping, energy estimate, and CPU / RAM /
  chip-temperature. Wi-Fi runs the C6 as a Zigbee **End Device** with an
  adjustable poll rate for stable coexistence. The web page is optional, can be
  toggled and its autostart persisted from Zigbee, and its HTTP Basic auth is
  **off by default**.
- **Programmable antenna switch** — on boards with a u.FL connector the on-board
  ceramic / external antenna choice is settable and persisted in NVS.
- **Private OTA** — Zigbee OTA (H2 + C6) and HTTP upload (C6), with bootloader
  rollback and a health-gated image confirm (pump parked + Zigbee joined).

### Fixed
- **Fail-open auto-watering** — in `PUMP_MODE_BOTH` the dry check evaluated true
  regardless of moisture; the engine is now fail-closed (a moisture condition
  with no fresh reading does not water).

## [1.0.0] — 2026-07-15

### Added
- Initial release: Zigbee plant-watering pump controller for the M5Stack NanoH2
  (ESP32-H2) — safety-gated pump driver (max-runtime watchdog, min-off cooldown,
  dry-run gate, 3-way relay/MOSFET drive), soil-moisture auto-watering, status
  LED, button gestures, a wear-safe persistent event log, and Zigbee Router
  endpoints with an OTA client.

[1.1.0]: https://github.com/ilia-ae/zignode/releases/tag/v1.1.0
[1.0.0]: https://github.com/ilia-ae/zignode/releases/tag/v1.0.0