Skip to content

Releases: marklynch/pool-controller-code

v1.9.0 - Service mode handling and reduced unknown messages

Choose a tag to compare

@github-actions github-actions released this 17 Jul 04:33
Immutable release. Only release title and notes can be modified.

[1.9.0] - 2026-07-17

Added

  • Decoded the CMD 0x2B controller heartbeat — a ~60 s unicast from the Connect 10 controller (0x0062) directly to the Touchscreen (0x0050), payload 02 00 in every capture, previously logged as an unhandled frame each cycle. Now handled log-only by handle_controller_heartbeat, which records any deviation from 02 00 as an "undocumented" entry on the Unknown Messages page. As one of only two message types the controller sends straight to the touchscreen, it is a prime candidate carrier for a controller-originated state signal such as service mode (documented in PROTOCOL.md)
  • Decoded Service Mode: byte 11 of the Connect 10 controller's CMD 0x12 status is a bitfield (bit 0 = heater on/off, bit 1 = service mode), confirmed by a service-mode capture — documented in PROTOCOL.md and surfaced as a new Home Assistant "Service Mode" binary sensor (published on first status frame) and a service_mode field in the /status JSON
  • Decoded the Genus Heater's (Active i25 Evo heat pump, 0x0070) CMD 0x12 device status, previously logged as unhandled: same 4-byte payload shape as the gas heaters with matching heater-on and water-flow bits, but a heat-pump-specific upper-bit set (0x13 observed immediately after a Gateway heater-on command, so bit 4 is read as actively heating rather than the gas heaters' lockout); heater on/off feeds the Heater 1 state in Home Assistant, and any status value outside the observed set — or a non-zero padding byte — is recorded as an "undocumented" entry on the Unknown Messages page — documented in PROTOCOL.md with the tentative bits flagged unconfirmed

Changed

  • Extended undocumented-payload flagging (record_undocumented()) across more decoders as protocol-research groundwork toward service-mode detection, so a novel byte value is surfaced on the Unknown Messages page instead of being silently absorbed: CMD 0x26 Configuration (reserved bits 5–7 set, or the documented always-1 bit 0 clear), the Connect 10 controller's own CMD 0x12 status (padding byte 10, state byte 11, and the unknown byte 12 — a likely service/interlock carrier), CMD 0x05 touchscreen ack, CMD 0x37 gateway comms status (codes absent from the known table), the VX 11S v3 chlorine-output byte 12 (to catch a LOW SALT / NO FLOW warning capture), CMD 0x06 light config (out-of-range zone index / undocumented status byte), the CMD 0x27 short-form valve data byte, CMD 0x38 timer day-bitmask bit 7, and the favourite-enable register — documented in PROTOCOL.md

Fixed

  • Fixed register writes made from the Viron Chlorinator's app (light zone on/off and color picks) being logged as unhandled: the chlorinator broadcasts the same CMD 0x3A Register Write the Gateway uses, so the command is now dispatched on its CMD byte regardless of source (handler renamed to handle_register_write_request), with the newly observed light-zone-color write target (0xD0–0xD7/slot 0x01) and its app color codes documented in PROTOCOL.md — flagged as an install-specific enumeration since the codes conflict with the reference system's color values
  • Fixed channel toggles made at the Connect 10 controller's physical buttons being logged as unhandled: the controller broadcasts the same CMD 0x10 Channel Toggle Command the Gateway uses for remote toggles (same 1-byte channel-index payload), so the command is now dispatched on its CMD byte regardless of source — controller-sourced examples (channel indexes 0x04–0x07) documented in PROTOCOL.md
  • Fixed favourite activations made at the Connect 10 controller itself (e.g. All Auto) being logged as unhandled: the controller unicasts the same CMD 0x2A Favourite Control Command to the Touchscreen as the Gateway does for remote activations, so the command is now dispatched on its CMD byte regardless of source and updates the active favourite in Home Assistant — both sources documented in PROTOCOL.md
  • Fixed CMD 0x26 Configuration broadcasts being counted as unknown/unhandled on every cycle: the handler returned false unconditionally, routing each normal config frame into the Unknown Messages buffer. It now decodes the frame, returns decoded, and records only genuine anomalies (see above), so the unknown-message counter and page reflect real unknowns
  • Fixed the inbuilt heater wrongly reporting On while the controller is in service mode with the heater off: the CMD 0x12 status byte was treated as a boolean (any non-zero value = On), so the service-mode value 0x02 read as heater on; the heater state now comes from bit 0 only

What's Changed

  • Decode Connect 10 service mode (CMD 0x12) and flag undocumented bytes across decoders by @marklynch in #67
  • Fixed channel toggles made at the Connect 10 controller's physical buttons by @marklynch in #69
  • Decoded the Genus Heater's (Active i25 Evo heat pump, 0x0070) CMD 0x12 by @marklynch in #70

Full Changelog: v1.8.2...v1.9.0

v1.8.2 - Firmware update polish and Mode Set command

Choose a tag to compare

@github-actions github-actions released this 16 Jul 02:59
Immutable release. Only release title and notes can be modified.

[1.8.2] - 2026-07-16

Changed

  • Decoded the newly discovered CMD 0x15 Mode Set Command: broadcast from the Touchscreen address (0x0050), switches Spa/Pool mode using the same encoding as the 0x14 status (Spa=0x00, Pool=0x01, unlike the inverted 0x2A values), confirmed by injection testing — documented in PROTOCOL.md and decoded into pool state, so mode switches commanded by other senders update Home Assistant immediately
  • The Home Assistant Spa/Pool mode select (mode/set) now switches mode with the dedicated CMD 0x15 Mode Set Command instead of activating the Pool/Spa built-in favourites via CMD 0x2A; mode values throughout the code now use the new MODE_SPA/MODE_POOL constants
  • Renamed CMD 0x2A from "Mode/Favourite Control Command" to "Favourite Control Command" throughout PROTOCOL.md and the code (handler, pattern constant, comments and logs), since its value space is the favourite slots; the Spa/Pool MQTT mode select is unaffected

Fixed

  • Fixed the Home Assistant "Firmware" update entity reverting to "Update available" (with a live Install button) during the post-flash reboot: the rebooting window now reports as still installing with an indeterminate progress bar, flipping to "Up-to-date" once the new firmware boots and reports its version

What's Changed

Full Changelog: v1.8.1...v1.8.2

v1.8.1 - Cleanup phantom channel entities

Choose a tag to compare

@github-actions github-actions released this 16 Jul 02:40
Immutable release. Only release title and notes can be modified.

[1.8.1] - 2026-07-16

Fixed

  • Added a logo to the Home Assistant "Firmware" update entity (shown on the Updates page and in the update dialog) by pointing its entity_picture at the project favicon hosted on GitHub
  • Fixed phantom "Unused" / "Unused Active" Home Assistant entities appearing for unconfigured channels: a channel-state register (0x8C–0x93), which the controller broadcasts for unused channels too, wrongly marked the channel as configured and published discovery for it under the fallback type name "Unused"; a channel is now only marked in use once its type is known

Full Changelog: v1.8.0...v1.8.1

v1.8.0 - Firmware update from github

Choose a tag to compare

@github-actions github-actions released this 16 Jul 01:50
Immutable release. Only release title and notes can be modified.

[1.8.0] - 2026-07-16

Added

  • GitHub firmware auto-update: the device now periodically checks the project's recent GitHub releases (every 12 h, plus shortly after boot) — the latest plus the previous 4 versions, read from the streamed releases.atom feed — and can install any of them over-the-air by pulling that release's pool-controller-update-<tag>.bin asset directly over HTTPS (esp_https_ota), no local file handling needed. The /update page gained an "Update from GitHub" section showing whether a newer release is available (with a release-notes link) and a version dropdown to install or roll back to a specific release, with live download progress. A new Home Assistant update entity ("Firmware") mirrors the check and installs the latest via the HA Install button, reporting installed/latest versions and download progress. Repo, number of versions tracked, check interval, and timeouts are configurable via FW_UPDATE_* in config.h. (New firmware_update module; see OTA_UPDATE.md.)
  • WiFi mesh roaming support: on connect the device now scans all channels and joins the strongest AP broadcasting the SSID (instead of the first one found), and advertises 802.11k/v so mesh networks like eero can steer it to a better node while connected
  • Decoded the Channel Category registers (0xF5–0xFC, slot 0x01, one per channel: Pool equipment / Light / Controlled Heater Power; only broadcast for in-use channels) — documented in PROTOCOL.md Appendix A and now decoded into pool state instead of being logged as unhandled registers
  • Decoded the Active Favourite register (0x20, slot 0x03): reports the currently active favourite/mode using the CMD 0x2A values, with 0xFF = none active (All Off reports as Pool and All Auto as none, since the controller treats them as momentary actions rather than states) — documented in PROTOCOL.md Appendix A and fed into the Home Assistant favourite select, which gains a status-only "No Favourite" option; favourites activated outside HA now update the select (immediately for gateway/MQTT commands, at the next ~8-minute register dump for touchscreen activations)

What's Changed

  • Added - Decoded the Channel Category registers by @marklynch in #61
  • Added - Decoded the Active Favourite register (0x20, slot 0x03) by @marklynch in #63
  • Add GitHub firmware auto-update with Home Assistant update entity by @marklynch in #66

Full Changelog: v1.7.0...v1.8.0

v1.7.0 - Improve message reliability and make unknown messages visible

Choose a tag to compare

@github-actions github-actions released this 14 Jul 00:29
Immutable release. Only release title and notes can be modified.

Lots of improvements from @lawther and collaboration from @zagnuts

[1.7.0] - 2026-07-14

Added

  • Saving of unknown bus messages, added web UI to view them
  • Periodic heap-stats logging (free, minimum-free watermark, largest free block) every 5 minutes via a new low-priority heap_monitor task, so a slow memory leak or growing fragmentation is visible as a trend in the console/TCP log history before it can exhaust the heap. The home page System table now also shows a Memory row (free / min-free), using the memory fields already present in the /status JSON.

Changed

  • Note about CMD 0x05 observed with payload 0x00. Added to PROTOCOL.md and new sample trace.
  • Note about CMD 0x12 observed with payload 0x01 0x00. Added to PROTOCOL.md and new sample trace.
  • Parsing for CMD 0x12 updated so 0x01 0x00 no longer reported as unexpected.
  • Full decoding of CMD 0x12 status byte for Gas Heaters (HiNRG 0x0072 and ICI (0x0074))
  • Rewrote frame level parser to use a sliding window for better resync on errors
  • Track frame resync events per failure type (no-start, header/data checksum, control, length, end-byte, overflow) and show the breakdown on the status page, separate from message-level decode errors
  • Unknown Messages page now tags each captured frame with its specific reason (no start, overflow, header/data checksum, length, bad framing) instead of a generic "error" chip
  • Handlers can now flag recognised frames carrying an undocumented field value via the new UNKNOWN_REASON_UNDOCUMENTED_PAYLOAD reason (record_undocumented()): the known fields are still decoded/published and the frame is counted as decoded, while the raw frame is surfaced on the Unknown Messages page as a non-error "undocumented" chip (amber, distinct from red framing errors). Applied across the decoders wherever a field value falls outside its documented set: heater 1/2 state (0xE6/0xE9), gas-heater status (0x12), Spa/Pool mode (0x14), touchscreen status (0x12), chlorinator mode (0x12) and pump-mode (0x0F), VX 11S status (0x12), pump speed command (0x18) and buttons (0x1B), mode/favourite command (0x2A), gateway register writes (0x3A), and the register-based channel type/state, light zone state/colour/name, valve state, and channel-status broadcasts (0x0B/0x27/0x38).
  • Breaking (HTTP): /status JSON's message_counts.errors (per-type protocol error counters) and message_counts.error_detail (introduced in 1.6.0-rc1) are removed, replaced by the new per-type resyncs object (total, no_start, header_checksum, bad_control, bad_length, bad_end, data_checksum, buffer_overflow) emitted by the sliding-window frame parser. Any external consumer reading the old fields needs to switch to resyncs.
  • Frame parser and decoder now also accept "discovery" packets (control bytes 0x00 0x00): a shorter 11-byte frame shape with no payload and no data-checksum byte, alongside the existing 0x80 0x00 data packets

Fixed

  • Fixed the home page Temperature row showing nothing useful since the per-source temperature refactor: the page's script still read the removed /status field temperature.current; it now renders one row per temperature-reporting device (e.g. "Genus Heater Temperature", "Connect 8/10 Temperature 1/2") from the per-device temperature1/temperature2 fields
  • TCP bridge no longer hangs when a connected client stalls or dies silently. The client socket is now non-blocking with a keep/drop policy on every send (full send keeps the client; a full send buffer drops the message but keeps the connection; a partial write or hard error drops the client), so a stuck peer can never wedge the bridge task — which is also the only task reading the UART, meaning such a wedge previously froze the whole device with no logs and no recovery. Added TCP keepalive (idle 30s, interval 5s, count 3) so a silently dead peer is detected and the single client slot freed.
  • MQTT client is no longer stopped from inside the WiFi event handler on disconnect. esp_mqtt_client_stop() blocks waiting for the MQTT task, and calling it from the system event-loop task could stall all further WiFi/IP event processing if the MQTT task was itself stuck on a dead socket during the same outage. The client is now started once on first connectivity and left to esp-mqtt's built-in reconnection, which handles WiFi drops/restores on its own. mqtt_client_start() is now idempotent so repeated reconnects are no-ops.

Security

  • Fixed a DOM-based XSS on the home page: channel/zone/valve names read from the bus were injected into the pool status table via innerHTML. They are now set with textContent so bus-derived strings can never be parsed as HTML/script.

What's Changed

  • feat: document unknown touchscreen messages by @lawther in #26
  • Add full CMD 0x12 gas heater status decoding (HiNRG 0x0072 / ICI 0x0074) by @lawther in #42
  • feat: VX 11S v3 chlorinator status handler by @lawther in #29
  • feat: Add capture buffer for unknown/error bus frames with web UI page by @lawther in #44
  • feat: sliding-window frame parser with per-type resync tracking by @lawther in #47
  • Support discovery packets (00 00 control bytes) in frame parsing by @lawther in #48
  • Fix DOM-based XSS on home page pool table by @marklynch in #51
  • Addresses PR #46 review comments: by @lawther in #50
  • Add decoder for CMD 0x0F chlorinator pump mode unicast (issue #23) by @lawther in #54
  • Record undocumented payloads in unknown buffer (issue #55) by @lawther in #56
  • Fix TCP bridge hang when a client stalls or dies silently by @marklynch in #49
  • Fixed the home page Temperature row showing nothing useful by @marklynch in #59

Full Changelog: v1.6.0...v1.7.0

v1.6.0 - Add second heater support

Choose a tag to compare

@github-actions github-actions released this 16 Jun 06:19
Immutable release. Only release title and notes can be modified.

[1.6.0] - 2026-06-16

Added

  • Global protocol-error counters alongside the decoded/unknown totals, broken down by failure type (no start byte, bad control bytes, no end found/buffer overflow, bad framing, length-field mismatch, header checksum, data checksum); exposed in the /status JSON (message_counts.errors and message_counts.error_detail) and the home page Messages row
  • Decode Heater 2 pool setpoint (register 0xEA) in both the touchscreen register broadcast and gateway register-write paths, instead of logging it as an unknown register
  • Updated CMD 0x17 to show its payload is Spa and Pool temperature setpoints (see issue #31)
  • Added HiNRG Gas Heater (0x0072) support (see issue #31)
  • Added per-heater pool and spa temperature setpoints, exposed as separate Home Assistant Number entities per heater ("Heater N Pool/Spa Setpoint"); writable via CMD 0x19 for Heater 1 and register writes 0xEA/0xEB for Heater 2
  • Added Heater 2 on/off support: state decoded from register 0xE9 and controllable via the "Heater 2" switch
  • VX 11S v3 chlorinator status handler

Changed

  • Combined the HiNRG and ICI gas-heater status handlers (CMD 0x12) into a single handler matched by either device pattern

Changed

  • Combined documentation for heater devices sending cmd 0x16 (water temperature)
  • Combined existing heater setpoint temperature handlers
  • Breaking (MQTT/HTTP): replaced the single global "Pool Setpoint"/"Spa Setpoint" entities with per-heater setpoints. The pool/<id>/setpoints/state topic and the global setpoint Number entities are gone, replaced by pool/<id>/heater/<index>/setpoints/state and per-heater entities; GET /status now carries setpoints inside each heaters[] entry instead of a global temperature block
  • Heater setpoints are now keyed per heater by the controller's registers (0xE7/0xE8 Heater 1, 0xEA/0xEB Heater 2); the physical heater devices' CMD 0x17 broadcasts are now log-only so an unplumbed heater's 0x0A default no longer clobbers the active setpoint
  • Introduce reg_id_t enum for named register IDs

Fixed

  • Fixed HiNRG Gas Heater (0x0072) status messages being logged as unhandled: the CMD 0x12 match pattern used the wrong header checksum byte (0x15 instead of 0x14), so the heater's device-side status was never decoded
  • Configured devcontainer to use ESP-IDF 5.5 for consistency
  • Fixed pool/spa setpoint commands having no effect after the per-heater refactor: the device now subscribes to the new heater/<n>/pool_setpoint/set and heater/<n>/spa_setpoint/set command topics (the old temperature/... subscriptions pointed at handlers that no longer exist)
  • Fixed phantom "Chlorine Output Level", "Pump Speed" and pH/ORP Home Assistant entities appearing on systems without a chlorinator or variable-speed pump: chemistry and pump discovery is now published lazily on each entity's first valid value instead of unconditionally at MQTT connect, and pH/ORP setpoints report null until a real setpoint has been received

What's Changed

  • feat: add findings about cmd 0x17, add device name for 0x72 by @lawther in #32
  • Update docs on the second heater setpoint by @marklynch in #33
  • refactor: Introduce reg_id_t enum for named register IDs (closes #36) by @lawther in #37
  • [ADDED] - Global protocol-error counters alongside the decoded/unknown by @marklynch in #38
  • Add support for second heater and pool spa setpoints by @marklynch in #34
  • [FIXED] Fixed phantom "Chlorine Output Level", "Pump Speed" and pH/ORP by @marklynch in #40

Full Changelog: v1.5.1...v1.6.0

v1.6.0-rc1

Choose a tag to compare

@github-actions github-actions released this 10 Jun 05:56
Immutable release. Only release title and notes can be modified.
18d13cd

Note - some of the entities have changed - so it is recommended to delete the MQTT device and it will then automatically add the correct entities back.

What's Changed

  • feat: add findings about cmd 0x17, add device name for 0x72 by @lawther in #32
  • Update docs on the second heater setpoint by @marklynch in #33
  • refactor: Introduce reg_id_t enum for named register IDs (closes #36) by @lawther in #37
  • [ADDED] - Global protocol-error counters alongside the decoded/unknown by @marklynch in #38
  • Add support for second heater and pool spa setpoints by @marklynch in #34
  • [FIXED] Fixed phantom "Chlorine Output Level", "Pump Speed" and pH/ORP by @marklynch in #40

Full Changelog: v1.5.1...v1.6.0-rc1

v1.5.1 - retry wifi for longer

Choose a tag to compare

@github-actions github-actions released this 25 May 10:09
Immutable release. Only release title and notes can be modified.

[1.5.1] - 2026-05-19

Changed

  • Increase the timeout and retry count if wifi is not available to 15 second retry and 10 attempts. #25

Full Changelog: v1.5.0...v1.5.1

v1.5.0 - adding more devices

Choose a tag to compare

@github-actions github-actions released this 25 May 09:59
Immutable release. Only release title and notes can be modified.

[1.5.0] - 2026-05-19

Added

  • Added tests runner for message decoder to make it easier to track changes and prevent regressions
  • Added GitHub Actions workflow that runs the host-based test suite on every push and pull request
  • Added many new unknown registers to PROTOCOL.md
  • Added Viron XT Variable Speed Pump support (device 0x00A0): decodes speed telemetry (CMD 0x3B)
  • Added pump physical speed preset button presses (CMD 0x1B, mapping Low/Med/High), and controller-to-pump speed commands (CMD 0x18, mapping Low/Med/High)

Changed

  • Renamed device 0x00A0 from "Internal Salt Cell" to "Viron XT Pump"
  • Renamed CMD 0x18 from "Chlorinator Cell Mode" to "Pump Speed Command" in the command name table
  • Updated temperature samples by @lawther

Fixed

  • Fixed mixed-validity temperature logging (CMD 0x16 / 0x31): when one of temp1 / temp2 is INVALID the still-valid temperature now logs its decoded °C value rather than a generic OK (raw 0xXX) label
  • Re-enabled the test_message_decoder and test_mqtt_commands host-test suites that had drifted out of sync with the current decoder/state and bus_send_bytes/s_pool_state interfaces, and cleared the SKIP_LIST in run_tests.sh
  • Fixed: ICI Gas Heater setpoints not being saved to pool state by @lawther

What's Changed

  • docs: add unknown command 0x2B by @lawther in #15
  • Add message decoding tests on build and fix existing tests by @marklynch in #16
  • Fix: ICI Gas Heater setpoints not being saved to pool state by @lawther in #14
  • Rename test suite workflows - and make them work with node 24 by @marklynch in #17
  • Add Viron XT Variable Speed Pump support (device 0x00A0) by @lawther in #19
  • tests: updated temperature samples by @lawther in #20
  • docs: add observed unknown registers to PROTOCOL.md by @lawther in #21
  • Add VX 11S v3 Salt Chlorinator support (ID 0x0081) by @lawther in #13
  • feat: add Viron XT pump button activity and controller speed command decoding by @lawther in #24

Full Changelog: v1.4.1...v1.5.0

v1.4.1 - add ICI Gas Heater

Choose a tag to compare

@github-actions github-actions released this 19 May 05:51
Immutable release. Only release title and notes can be modified.

What's Changed

  • Add support for ICI Gas Heater (0x0074) by @lawther in #12

New Contributors

Full Changelog: v1.4.0...v1.4.1