diff --git a/CHANGELOG.md b/CHANGELOG.md index 3732ddf..0c047af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Added a device reboot control: a Reboot button on the firmware-update web page (with confirmation and auto-reload countdown) backed by a new `/reboot` endpoint, and a matching Home Assistant Reboot button via MQTT (`pool//reboot`) +- Decoded the solar protocol messages, previously logged as unhandled: register 0x3A (slot 0x01) is the Solar Setpoint in °C (confirmed by changing the setpoint in the UI and watching the rebroadcast; polled by the Internet Gateway every cycle), CMD 0x2D is the Solar Setpoint Broadcast the touchscreen fires when the setpoint is changed (1-byte °C), and CMD 0x2C is the Solar Status Broadcast carrying the solar mode (Off/Auto/On), a config bitmask (Winter/Summer season, Flush daily, Filter pump required for solar), the temperature differential, and two bytes read as the pool-water/roof temperature readings (byte order unconfirmed) — all three handled log-only and documented in PROTOCOL.md, with the remaining 0x2C unknowns (a byte tracking differential − 3, an always-set bit) flagged for further captures ### Changed ### Removed ### Fixed diff --git a/PROTOCOL.md b/PROTOCOL.md index 0551ce9..071b594 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -34,6 +34,8 @@ This document describes the proprietary serial protocol used by the Connect 10 p - [0x28 — Valve Control Command ✅](#0x28--valve-control-command-) - [0x2A — Favourite Control Command ✅](#0x2a--favourite-control-command-) - [0x2B — Unknown ⚠️](#0x2b--unknown-️) + - [0x2C — Solar Status Broadcast ⚠️](#0x2c--solar-status-broadcast-️) + - [0x2D — Solar Setpoint Broadcast ✅](#0x2d--solar-setpoint-broadcast-) - [0x31 — Water Temperature Reading (alt) ✅](#0x31--water-temperature-reading-alt-) - [0x37 — Internet Gateway Info ⚠️](#0x37--internet-gateway-info-️) - [0x38 — Register Data ⚠️](#0x38--register-data-️) @@ -146,6 +148,8 @@ Click any CMD in the first column to jump to the full section in [Commands](#com | [`0x28`](#0x28--valve-control-command-) | Valve Control Command | `0x00F0` Gateway → Broadcast | | **No (doc only)** | | [`0x2A`](#0x2a--favourite-control-command-) | Favourite Control Command | `0x00F0` Gateway, `0x0062` Connect 8/10 → `0x0050` Touchscreen | Unicast; dispatched on CMD byte alone (source-agnostic) | Yes (unified handler) | | [`0x2B`](#0x2b--unknown-️) | Unknown (heartbeat) | `0x0062` Connect 8/10 → `0x0050` Touchscreen | Unicast ~60 s; payload `[02 00]` observed; meaning unknown. Handler flags any deviation | Yes (log-only + flag) | +| [`0x2C`](#0x2c--solar-status-broadcast-️) | Solar Status Broadcast | `0x0050` Touchscreen → Broadcast | Byte 11 = solar mode (Off/Auto/On); byte 10 bit 5 = Winter/Summer; byte 15 = temperature differential °C; dispatched on CMD byte alone | Yes (log-only) | +| [`0x2D`](#0x2d--solar-setpoint-broadcast-) | Solar Setpoint Broadcast | `0x0050` Touchscreen → Broadcast | Fired when the solar setpoint is changed; 1-byte °C value; dispatched on CMD byte alone | Yes (log-only) | | [`0x31`](#0x31--water-temperature-reading-alt-) | Water Temperature Reading (alt) | `0x0062` → Broadcast | Same `{temp1, temp2}` field layout as `0x16`; different disconnected encoding (`>= 0xA0` vs `0x00`); shared handler, log-only | Yes (unified handler) | | [`0x37`](#0x37--internet-gateway-info-️) | Internet Gateway Info | `0x00F0` → Broadcast | LEN distinguishes serial (`0x11`), network config (`0x15`), comms status (`0x0F`) variants | Yes (3 handlers) | | [`0x38`](#0x38--register-data-️) | Register Data (Response) | `0x0050` Touchscreen → Broadcast | Universal register system — sub-dispatched by register + slot (see [Appendix A](#appendix-a-register-dispatch-table)); dispatched on CMD byte alone | Yes (unified handler) | @@ -1363,6 +1367,73 @@ Unicast message sent by the Connect 8/10 Controller (`0x0062`) directly to the T --- +### 0x2C — Solar Status Broadcast ⚠️ + +Broadcast from the Touchscreen (`0x0050`) carrying the solar mode and further, not yet decoded, solar fields. Like [0x2D](#0x2d--solar-setpoint-broadcast-), it only appears on systems where the solar configuration has been touched. + +**Pattern:** `02 00 50 FF FF 80 00 2C 12 0E` + +**Examples:** + +``` +02 00 50 FF FF 80 00 2C 12 0E 23 00 00 00 04 07 2E 03 Solar mode Off +02 00 50 FF FF 80 00 2C 12 0E 23 01 00 00 04 07 2F 03 Solar mode Auto +02 00 50 FF FF 80 00 2C 12 0E 23 02 00 00 04 07 30 03 Solar mode On + ^^ Solar mode +02 00 50 FF FF 80 00 2C 12 0E 03 00 00 00 04 07 0E 03 Winter (byte 10 bit 5 clear) +02 00 50 FF FF 80 00 2C 12 0E 23 00 00 00 04 07 2E 03 Summer (byte 10 bit 5 set) + ^^ Season/config bitmask +02 00 50 FF FF 80 00 2C 12 0E 21 00 00 00 03 06 2A 03 Temperature differential 6°C + ^^ Temperature differential +02 00 50 FF FF 80 00 2C 12 0E 2B 00 00 00 03 06 34 03 Filter pump required for solar (byte 10 bit 3 set) +``` + +**Data Fields:** + +- Byte 10: Configuration bitmask + - Bit 5: `0` = Winter, `1` = Summer + - Bit 3: Filter pump required for solar (`0` = no, `1` = yes) + - Bit 1: Flush daily (`0` = disabled, `1` = enabled) + - Bit 0: Unknown — always `1` in observed samples + - Other bits: `0` in observed samples +- Byte 11: Solar mode (`0x00`=Off, `0x01`=Auto, `0x02`=On — same encoding as channel state) +- Bytes 12–13: Likely the two solar temperature readings the touchscreen shows as "Pool water was" and "Roof temperature" (°C) — both read 0 on the observed system, matching the constant `0x00 0x00`. Which byte is which (and the confirmation itself) needs a capture with live solar sensors reporting non-zero values. +- Byte 14: Unknown — has tracked byte 15 at (differential − 3) in all observed samples (`04`/`07`, `03`/`06`) +- Byte 15: Temperature differential in °C +- Byte 16: Data checksum + +**Notes:** + +- Decoded in code by `handle_solar_status_broadcast` — log-only, no `pool_state` update. + +--- + +### 0x2D — Solar Setpoint Broadcast ✅ + +Broadcast from the Touchscreen (`0x0050`) when the solar temperature setpoint is changed, carrying the setpoint as a single byte. The same value lives in the [Solar Setpoint register](#appendix-a-register-dispatch-table) (`0x3A`/slot `0x01`), which the Gateway polls — this CMD is the push notification of a change, mirroring how the heater setpoints have both register and dedicated-CMD representations. + +**Pattern:** `02 00 50 FF FF 80 00 2D 0D 0A` + +**Example:** + +``` +02 00 50 FF FF 80 00 2D 0D 0A 19 19 03 Solar setpoint 25°C + ^^ Setpoint °C + ^^ Data checksum (equals byte 10, the only data byte) +``` + +**Data Fields:** + +- Byte 10: Solar setpoint in °C +- Byte 11: Data checksum (equals byte 10) + +**Notes:** + +- Fired on a solar setpoint change at the touchscreen. Never seen in captures from systems where the solar config was untouched. +- Decoded in code by `handle_solar_setpoint_broadcast` — log-only, no `pool_state` update. + +--- + ### 0x31 — Water Temperature Reading (alt) ✅ Second water-temperature variant broadcast by the Connect 8/10 Controller (`0x0062`), in parallel to the [0x16](#0x16--water-temperature-reading-) reading. The two CMDs carry the same `{temp1, temp2}` field layout; the only practical difference is the **disconnected-sensor encoding**: @@ -1947,6 +2018,7 @@ The register ID and slot together determine the message meaning. The slot distin | `0x20` | `0x03` | Active Favourite | CMD `0x2A` value of the active favourite (`0x00`=Pool … `0x07`=Favourite 6); `0xFF` = none active — see note below | | `0x21`–`0x28` | `0x03` | Favourite Enable | 1-byte flag (`0x01`=enabled, `0x00`=disabled). Maps to CMD `0x2A` values `0x00`–`0x07` in order. Pool (`0x21`) and Spa (`0x22`) are always `0x01`. | | `0x31`–`0x38` | `0x03` | Favourite Labels | Null-terminated ASCII string. Maps to CMD `0x2A` values `0x00`–`0x07` in order. `0x31`=Pool, `0x32`=Spa, `0x33`–`0x38`=user Favourites 1–6. | +| `0x3A` | `0x01` | Solar Setpoint | 1-byte °C value — see note below | | `0x64`-`0x65` ⚠️| `0x00`| Unknown | Only `0x01` observed. Repeats ~every 8 minutes | | `0x6C`–`0x73` | `0x02` | Channel Types | 1-byte type code (see [0x0B](#0x0b--channel-status-) channel types) | | `0x7C`–`0x83` | `0x02` | Channel Names | Null-terminated ASCII string | @@ -1981,6 +2053,7 @@ The register ID and slot together determine the message meaning. The slot distin - Register ranges can overlap (e.g., `0xD0`–`0xD7`) but are distinguished by the slot value - The same slot value (e.g., `0x02`) can represent different data formats depending on the register - Slot values appear to be context-dependent rather than globally defining a data type +- **`0x3A` (Solar Setpoint) — confirmed ✅**: 1-byte solar temperature setpoint in °C. Confirmed by UI experiment: changing the solar setpoint to 23°C rebroadcast `3A 01 17`. Polled by the Internet Gateway every cycle via [0x39](#0x39--register-read-request-) (never appears in the touchscreen's spontaneous ~8-minute dump); on installs where the setting is untouched it reads the `0x19` (25°C) default. Setpoint changes are also announced via the dedicated [0x2D Solar Setpoint Broadcast](#0x2d--solar-setpoint-broadcast-). - **`0xE9`/`0xEA`/`0xEB` (Heater 2 trio) — confirmed ✅**: Slot `0x00` holds the Heater 1 trio at `0xE6` (state), `0xE7` (Pool setpoint), `0xE8` (Spa setpoint), and `0xE9`/`0xEA`/`0xEB` are the analogous trio for the second heater: `0xE9` = state (`0x00`=Off, `0x01`=On), `0xEA` = Pool setpoint, `0xEB` = Spa setpoint (1-byte °C). All three are writable via the gateway register-write command (CMD `0x3A` / second-byte `0xB9`). `0xEA` was confirmed by UI capture — changing the setpoint in the UI sends a `0x3A` write to `0xEA`/slot `0x00` and the touchscreen rebroadcasts the new value via CMD `0x38` (observed 21°C `EA 00 15`, 22°C `EA 00 16`, 27°C `EA 00 1B`; the 27°C value matched the **H2** value in the heater's `0x0070` CMD `0x17` broadcast). `0xE9` and `0xEB` are confirmed as Heater 2 state and spa setpoint respectively. (On the test install the second heater is a heat pump; "Heater 2" is kept as the generic name since another install's second heater may be a different type.) - **Mutually exclusive broadcast**: in captures observed so far the touchscreen broadcasts *either* the `E6/E7/E8` trio *or* the `E9/EA/EB` trio in slot `0x00`, but not both — consistent with a config-dependent enable (likely [0x26](#0x26--configuration-️) byte 10 bit 3 = heater count). - **`0xEB` default**: when the second heater isn't plumbed to spa, `0xEB` reads `0x0A` (10°C) — an unused default at the minimum setpoint rather than a live value. diff --git a/main/message_decoder.c b/main/message_decoder.c index 066a5c9..d9fe5b8 100644 --- a/main/message_decoder.c +++ b/main/message_decoder.c @@ -266,6 +266,8 @@ static const cmd_name_entry_t CMD_NAME_TABLE[] = { {0x27, "Valve State"}, {0x2A, "Favourite Cmd"}, {0x2B, "Controller Heartbeat"}, + {0x2C, "Solar Status"}, + {0x2D, "Solar Setpoint"}, {0x31, "Temperature Reading (alt)"}, {0x37, "Gateway Info Req/Resp"}, {0x38, "Register Response"}, @@ -615,6 +617,7 @@ static bool handle_favourite_label(const uint8_t *data, int len, const uint8_t * static bool handle_favourite_enable(const uint8_t *data, int len, const uint8_t *payload, int payload_len, const char *addr_info, message_decoder_context_t *ctx); static bool handle_active_favourite(const uint8_t *data, int len, const uint8_t *payload, int payload_len, const char *addr_info, message_decoder_context_t *ctx); static bool handle_temp_setpoint(const uint8_t *data, int len, const uint8_t *payload, int payload_len, const char *addr_info, message_decoder_context_t *ctx); +static bool handle_solar_setpoint(const uint8_t *data, int len, const uint8_t *payload, int payload_len, const char *addr_info, message_decoder_context_t *ctx); static bool handle_multicolor_light_type(const uint8_t *data, int len, const uint8_t *payload, int payload_len, const char *addr_info, message_decoder_context_t *ctx); static bool handle_channel_count(const uint8_t *data, int len, const uint8_t *payload, int payload_len, const char *addr_info, message_decoder_context_t *ctx); static bool handle_channel_category(const uint8_t *data, int len, const uint8_t *payload, int payload_len, const char *addr_info, message_decoder_context_t *ctx); @@ -657,6 +660,9 @@ static const register_handler_t REGISTER_HANDLERS[] = { // Favourite labels (slot 0x03, registers 0x31-0x38 = Pool,Spa,Fav1-6) {REG_ID_FAVOURITE_LABEL_0, REG_ID_FAVOURITE_LABEL_7, 0x03, handle_favourite_label, "Favourite Label"}, + // Solar setpoint (slot 0x01, register 0x3A) + {REG_ID_SOLAR_SETPOINT, REG_ID_SOLAR_SETPOINT, 0x01, handle_solar_setpoint, "Solar Setpoint"}, + // Heater 1 state (slot 0x00, register 0xE6) — touchscreen register-response broadcast. // The CMD 0x12 broadcast from 0x0062 is the authoritative state source that updates // pool_state->heaters[0]; this entry just names the register-response broadcast so it @@ -867,6 +873,25 @@ static bool handle_temp_setpoint( return true; } +/** + * Handler: Solar temperature setpoint + * Register 0x3A, Slot 0x01, 1-byte °C value. Log-only — not yet surfaced in + * pool_state or MQTT. + */ +static bool handle_solar_setpoint( + const uint8_t *data, int len, + const uint8_t *payload, int payload_len, + const char *addr_info, + message_decoder_context_t *ctx) +{ + if (payload_len < 3) return false; + + uint8_t temp_c = payload[2]; + ESP_LOGI(TAG, "%s Solar setpoint - %d°C", addr_info, temp_c); + + return true; +} + /** * Handler: Heater 1 state register-response broadcast * Register 0xE6, Slot 0x00. Log-only — authoritative state still flows through @@ -2590,6 +2615,59 @@ static bool handle_light_refresh( return true; } +/** + * Handler: Solar setpoint broadcast (CMD 0x2D) — log-only + * Dispatched on the CMD byte alone (source-agnostic). + * + * Broadcast by the Touchscreen when the solar setpoint is changed; carries the + * setpoint as a single °C byte. The same value lives in the solar setpoint + * register (0x3A/slot 0x01, handle_solar_setpoint). + */ +static bool handle_solar_setpoint_broadcast( + const uint8_t *data, int len, + const uint8_t *payload, int payload_len, + const char *addr_info, + message_decoder_context_t *ctx) +{ + if (payload_len < 1) return false; + + ESP_LOGI(TAG, "%s Solar setpoint broadcast - %d°C", addr_info, payload[0]); + + return true; +} + +/** + * Handler: Solar status broadcast (CMD 0x2C) — log-only + * Dispatched on the CMD byte alone (source-agnostic). + * + * Solar config block from the Touchscreen: config bitmask (season, flush + * daily, filter pump required), mode, two temperature readings (likely pool + * water / roof, byte order unconfirmed) and the temperature differential. + * See PROTOCOL.md 0x2C for the payload layout and remaining unknowns. + */ +static bool handle_solar_status_broadcast( + const uint8_t *data, int len, + const uint8_t *payload, int payload_len, + const char *addr_info, + message_decoder_context_t *ctx) +{ + if (payload_len < 6) return false; + + uint8_t flags = payload[0]; + uint8_t mode = payload[1]; + const char *mode_name = (mode == 0x00) ? "Off" : (mode == 0x01) ? "Auto" : (mode == 0x02) ? "On" : "Unknown"; + + ESP_LOGI(TAG, "%s Solar status - Mode: %s, Season: %s, Flush daily: %s, " + "Filter pump required: %s, Differential: %d°C, Temps: %d°C/%d°C", + addr_info, mode_name, + (flags & 0x20) ? "Summer" : "Winter", + (flags & 0x02) ? "on" : "off", + (flags & 0x08) ? "yes" : "no", + payload[5], payload[2], payload[3]); + + return true; +} + // ====================================================== // Register message handlers (dispatched by register range and slot) // ====================================================== @@ -3877,6 +3955,18 @@ static bool dispatch_message( return handle_light_refresh(data, len, payload, payload_len, addr_info, ctx); } + // Solar setpoint broadcast (CMD 0x2D) — dispatched on the CMD byte alone; + // only the Touchscreen (0x0050) observed sending it so far + if (cmd == 0x2D) { + return handle_solar_setpoint_broadcast(data, len, payload, payload_len, addr_info, ctx); + } + + // Solar status broadcast (CMD 0x2C) — dispatched on the CMD byte alone; + // only the Touchscreen (0x0050) observed sending it so far + if (cmd == 0x2C) { + return handle_solar_status_broadcast(data, len, payload, payload_len, addr_info, ctx); + } + // Configuration messages if (match_pattern(data, len, MSG_TYPE_LIGHT_CONFIG)) { return handle_light_config(data, len, payload, payload_len, addr_info, ctx); diff --git a/main/message_decoder.h b/main/message_decoder.h index f62ca76..a37e5e3 100644 --- a/main/message_decoder.h +++ b/main/message_decoder.h @@ -143,6 +143,8 @@ typedef enum { REG_ID_FAVOURITE_LABEL_0 = 0x31, // Fav label index 0 (0–7 → 0x31–0x38, slot 0x03) REG_ID_FAVOURITE_LABEL_7 = 0x38, // Fav label index 7 (last) + REG_ID_SOLAR_SETPOINT = 0x3A, // Solar temperature setpoint in °C (slot 0x01) + REG_ID_CHANNEL_TYPE_0 = 0x6C, // Channel 0 type (0–7 → 0x6C–0x73, slot 0x02) REG_ID_CHANNEL_TYPE_7 = 0x73, // Channel 7 type (last)