Releases: marklynch/pool-controller-code
Releases · marklynch/pool-controller-code
Release list
v1.9.0 - Service mode handling and reduced unknown messages
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 00in every capture, previously logged as an unhandled frame each cycle. Now handled log-only byhandle_controller_heartbeat, which records any deviation from02 00as 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_modefield in the/statusJSON - 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
falseunconditionally, 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
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
- CMD 0x15 Mode Set Command by @marklynch in #65
Full Changelog: v1.8.1...v1.8.2
v1.8.1 - Cleanup phantom channel entities
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_pictureat 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
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.atomfeed — and can install any of them over-the-air by pulling that release'spool-controller-update-<tag>.binasset directly over HTTPS (esp_https_ota), no local file handling needed. The/updatepage 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 Assistantupdateentity ("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 viaFW_UPDATE_*inconfig.h. (Newfirmware_updatemodule; 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
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_monitortask, 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 thememoryfields already present in the/statusJSON.
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 0x00no longer reported as unexpected. - Full decoding of CMD 0x12 status byte for Gas Heaters (HiNRG
0x0072and 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_PAYLOADreason (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):
/statusJSON'smessage_counts.errors(per-type protocol error counters) andmessage_counts.error_detail(introduced in 1.6.0-rc1) are removed, replaced by the new per-typeresyncsobject (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 toresyncs. - 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 existing0x80 0x00data 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
/statusfieldtemperature.current; it now renders one row per temperature-reporting device (e.g. "Genus Heater Temperature", "Connect 8/10 Temperature 1/2") from the per-devicetemperature1/temperature2fields - 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 withtextContentso 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
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
/statusJSON (message_counts.errorsandmessage_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
0x19for Heater 1 and register writes0xEA/0xEBfor Heater 2 - Added Heater 2 on/off support: state decoded from register
0xE9and 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/statetopic and the global setpoint Number entities are gone, replaced bypool/<id>/heater/<index>/setpoints/stateand per-heater entities;GET /statusnow carries setpoints inside eachheaters[]entry instead of a globaltemperatureblock - Heater setpoints are now keyed per heater by the controller's registers (
0xE7/0xE8Heater 1,0xEA/0xEBHeater 2); the physical heater devices' CMD0x17broadcasts are now log-only so an unplumbed heater's0x0Adefault 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/setandheater/<n>/spa_setpoint/setcommand topics (the oldtemperature/...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
nulluntil 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
Immutable
release. Only release title and notes can be modified.
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
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
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 (CMD0x3B) - Added pump physical speed preset button presses (CMD
0x1B, mapping Low/Med/High), and controller-to-pump speed commands (CMD0x18, mapping Low/Med/High)
Changed
- Renamed device
0x00A0from "Internal Salt Cell" to "Viron XT Pump" - Renamed CMD
0x18from "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 oftemp1/temp2is INVALID the still-valid temperature now logs its decoded °C value rather than a genericOK (raw 0xXX)label - Re-enabled the
test_message_decoderandtest_mqtt_commandshost-test suites that had drifted out of sync with the current decoder/state andbus_send_bytes/s_pool_stateinterfaces, and cleared theSKIP_LISTinrun_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
Immutable
release. Only release title and notes can be modified.