Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<device_id>/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
Expand Down
73 changes: 73 additions & 0 deletions PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-️)
Expand Down Expand Up @@ -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) |
Expand Down Expand Up @@ -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**:
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down
90 changes: 90 additions & 0 deletions main/message_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
// ======================================================
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions main/message_decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down