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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]
### Added
- Decoded CMD 0x3C as the Light Refresh Command, previously logged as unhandled: a touchscreen broadcast carrying a light zone index that refreshes/resyncs that zone's light, observed during light configuration sessions and color operations — handled log-only with out-of-range zone indexes recorded as "undocumented" entries on the Unknown Messages page; documented in PROTOCOL.md with the exact effect at the light hardware flagged unconfirmed
- Added light color control to the Home Assistant light entities for multicolor zones: each zone whose light is multicolor-capable (and whose system light type is known via register 0xF0) now exposes the model's color list — the SLX or Delta subset of the shared color table — as the light's effect list, with the current color shown as the active effect and color picks sent to the controller as a Gateway-sourced register write; light entity discovery re-publishes automatically if the light type or a zone's multicolor capability changes, and the MQTT light state now uses the canonical color names (e.g. "User 1" instead of "User1") so the reported color matches the effect list
- Decoded register 0xF0 (slot 0x01) as the system-wide Multicolor Light Type selection, previously an unknown register: the light model chosen in the touchscreen's light setup (0x00 = SLX, 0x01 = Delta, 0xFF = no multicolor light), confirmed by switching the type in the UI and watching the register rebroadcast. Surfaced as a "Multicolor Light Type" row on the status page and a `multicolor_light_type` field in the `/status` JSON ("None"/"SLX"/"Delta"); an unmapped model index is shown as "Unknown (0xXX)" and recorded as an "undocumented" entry on the Unknown Messages page — documented in PROTOCOL.md, where the selected model also determines which subset of the shared light color code table applies — the full SLX and Delta color code subsets were mapped by cycling every color on each type, and a Gateway-sourced 0x3A color write was verified working (sets the light and updates the touchscreen)
- Decoded CMD 0x07 as the Lighting Zone Color Broadcast, previously logged as unhandled every ~10 s on installs with a multicolor light: the color companion to the CMD 0x06 light config broadcast ({zone_idx, color} payload), emitted only for multicolor-capable zones, with the color byte mirroring the zone's Light Zone Color register (0xD0+zone, slot 0x01) — handled log-only since the register broadcast remains the state source, with out-of-range zone indexes recorded as "undocumented" entries on the Unknown Messages page; documented in PROTOCOL.md with the color enumeration flagged install-specific
- Decoded the Light Zone Enabled registers (0x90–0x93, slot 0x01), previously logged as unhandled: a 1-byte per-zone flag reporting whether each light zone is configured in the controller (0x01 = configured, 0x00 = not), completing the slot-0x01 per-zone register family alongside multicolor/name/state/color/active. Configured zones are rebroadcast regularly while unconfigured ones only appear at startup or after a configuration change; the flag now drives the zone's configured state (a newly seen zone wakes the register requester to poll its name/color, and an unconfigured zone stops publishing to Home Assistant) — documented in PROTOCOL.md
### Changed
Expand Down
95 changes: 70 additions & 25 deletions PROTOCOL.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ idf_component_register(
"web_handlers.c"
"tcp_bridge.c"
"framing.c"
"lighting_colors.c"
"message_decoder.c"
"register_requester.c"
"unknown_buffer.c"
Expand Down
67 changes: 67 additions & 0 deletions main/lighting_colors.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Lighting color name table
*
* The shared color value space used by the light zone color registers
* (0xD0+zone, slot 0x01), CMD 0x07 color broadcasts, and 0x3A color writes;
* each light model exposes a subset of it, selected by the Multicolor Light
* Type register (0xF0) — see PROTOCOL.md, Light Zone Color Control.
*
* Kept in its own translation unit so the host-side unit tests can link the
* real table without pulling in all of message_decoder.c.
*/

#include "pool_state.h"

const char *LIGHTING_COLOR_NAMES[LIGHTING_COLOR_COUNT] = {
"Unknown", // 0
"Red", // 1
"Orange", // 2
"Yellow", // 3
"Green", // 4
"Blue", // 5
"Purple", // 6
"White", // 7
"User 1", // 8
"User 2", // 9
"Disco", // 10
"Smooth", // 11
"Fade", // 12
"Magenta", // 13
"Cyan", // 14
"Pattern", // 15
"Rainbow", // 16
"Ocean", // 17
"Voodoo Lounge", // 18
"Deep Blue Sea", // 19
"Royal Blue", // 20
"Afternoon Skies", // 21
"Aqua Green", // 22
"Emerald", // 23
"Warm Red", // 24
"Flamingo", // 25
"Vivid Violet", // 26
"Sangria", // 27
"Twilight", // 28
"Tranquillity", // 29
"Gemstone", // 30
"USA", // 31
"Mardi Gras", // 32
"Cool Cabaret", // 33
"Sam", // 34
"Party", // 35
"Romance", // 36
"Caribbean", // 37
"American", // 38
"California Sunset", // 39
"Royal", // 40
"Hold", // 41
"Recall", // 42
"Peruvian Paradise", // 43
"Super Nova", // 44
"Northern Lights", // 45
"Tidal Wave", // 46
"Patriot Dream", // 47
"Desert Skies", // 48
"Nova", // 49
"Pink", // 50
};
195 changes: 135 additions & 60 deletions main/message_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ static const char *MSG_TYPE_MODE_SET_CMD = "02 00 50 FF FF 80 00 15 0D
static const char *MSG_TYPE_CHANNELS = "02 00 50 00 6F 80 00 0D 0D 5B";
static const char *MSG_TYPE_CHANNEL_STATUS = "02 00 50 FF FF 80 00 0B 25 00";
static const char *MSG_TYPE_LIGHT_CONFIG = "02 00 50 FF FF 80 00 06 0E E4";
static const char *MSG_TYPE_LIGHT_COLOR = "02 00 50 FF FF 80 00 07 0E E5";
static const char *MSG_TYPE_CONTROLLER_TIME = "02 00 50 FF FF 80 00 FD 0F DC";
static const char *MSG_TYPE_TOUCHSCREEN_UNKNOWN1 = "02 00 50 FF FF 80 00 12 0E F0";
static const char *MSG_TYPE_TOUCHSCREEN_UNKNOWN2 = "02 00 50 FF FF 80 00 27 0D 04";
Expand Down Expand Up @@ -272,6 +271,7 @@ static const cmd_name_entry_t CMD_NAME_TABLE[] = {
{0x38, "Register Response"},
{0x39, "Register Request"},
{0x3A, "Light Zone Control Cmd"},
{0x3C, "Light Refresh Cmd"},
{0xFD, "Controller Day/Time"},
};

Expand All @@ -296,6 +296,40 @@ static const char* get_cmd_name(uint8_t cmd, char *fallback_buf, size_t buf_size
return fallback_buf;
}

const char* multicolor_light_type_name(uint8_t type, char *fallback_buf, size_t buf_size) {
switch (type) {
case MULTICOLOR_LIGHT_TYPE_NONE: return "None";
case MULTICOLOR_LIGHT_TYPE_SLX: return "SLX";
case MULTICOLOR_LIGHT_TYPE_DELTA: return "Delta";
default:
snprintf(fallback_buf, buf_size, "Unknown (0x%02X)", type);
return fallback_buf;
}
}

// Per-model color code subsets of the shared color value space (indexes into
// LIGHTING_COLOR_NAMES) — see PROTOCOL.md, Light Zone Color Control.
static const uint8_t SLX_COLOR_CODES[] = {
0x01, 0x02, 0x04, 0x05, 0x07, 0x08, 0x0A, 0x0D, 0x0E, 0x0F, 0x10, 0x11
};
static const uint8_t DELTA_COLOR_CODES[] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C
};

const uint8_t* multicolor_light_color_codes(uint8_t light_type, int *count) {
switch (light_type) {
case MULTICOLOR_LIGHT_TYPE_SLX:
*count = sizeof(SLX_COLOR_CODES);
return SLX_COLOR_CODES;
case MULTICOLOR_LIGHT_TYPE_DELTA:
*count = sizeof(DELTA_COLOR_CODES);
return DELTA_COLOR_CODES;
default:
*count = 0;
return NULL;
}
}

// Channel state names
const char *CHANNEL_STATE_NAMES[] = {
"Off", // 0
Expand Down Expand Up @@ -353,60 +387,8 @@ const char *DAY_OF_WEEK_NAMES[] = {
};
#define DAY_OF_WEEK_COUNT 7

// Lighting color names
const char *LIGHTING_COLOR_NAMES[] = {
"Unknown", // 0
"Red", // 1
"Orange", // 2
"Yellow", // 3
"Green", // 4
"Blue", // 5
"Purple", // 6
"White", // 7
"User 1", // 8
"User 2", // 9
"Disco", // 10
"Smooth", // 11
"Fade", // 12
"Magenta", // 13
"Cyan", // 14
"Pattern", // 15
"Rainbow", // 16
"Ocean", // 17
"Voodoo Lounge", // 18
"Deep Blue Sea", // 19
"Royal Blue", // 20
"Afternoon Skies", // 21
"Aqua Green", // 22
"Emerald", // 23
"Warm Red", // 24
"Flamingo", // 25
"Vivid Violet", // 26
"Sangria", // 27
"Twilight", // 28
"Tranquillity", // 29
"Gemstone", // 30
"USA", // 31
"Mardi Gras", // 32
"Cool Cabaret", // 33
"Sam", // 34
"Party", // 35
"Romance", // 36
"Caribbean", // 37
"American", // 38
"California Sunset", // 39
"Royal", // 40
"Hold", // 41
"Recall", // 42
"Peruvian Paradise", // 43
"Super Nova", // 44
"Northern Lights", // 45
"Tidal Wave", // 46
"Patriot Dream", // 47
"Desert Skies", // 48
"Nova", // 49
"Pink", // 50
};
// Lighting color names are defined in lighting_colors.c (shared color value
// space; see PROTOCOL.md, Light Zone Color Control)

// Gateway comms status lookup table
typedef struct {
Expand Down Expand Up @@ -633,6 +615,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_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);
static bool handle_valve_state(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 @@ -689,6 +672,8 @@ static const register_handler_t REGISTER_HANDLERS[] = {
// Heater 2 setpoints (slot 0x00, registers 0xEA=Pool, 0xEB=Spa)
{REG_ID_HEATER2_POOL_SETPOINT, REG_ID_HEATER2_SPA_SETPOINT, 0x00, handle_temp_setpoint, "Heater 2 Setpoint"},

{REG_ID_MULTICOLOR_LIGHT_TYPE, REG_ID_MULTICOLOR_LIGHT_TYPE, 0x01, handle_multicolor_light_type, "Multicolor Light Type"},

{REG_ID_CHANNEL_COUNT, REG_ID_CHANNEL_COUNT, 0x01, handle_channel_count, "Channel Count"},

// Channel categories (slot 0x01, registers 0xF5-0xFC = channels 1-8);
Expand Down Expand Up @@ -949,6 +934,59 @@ static bool handle_heater2_state(
return true;
}

/**
* Handler: Multicolor light type selection
* Register 0xF0, Slot 0x01
* System-wide light model selected in the touchscreen's light setup:
* 0x00=SLX, 0x01=Delta, 0xFF=no multicolor light configured. A value outside
* the known set is a new model index worth capturing.
*/
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)
{
if (payload_len < 3) return false;

uint8_t type = payload[2];
char name_buf[16];
const char *name = multicolor_light_type_name(type, name_buf, sizeof(name_buf));

ESP_LOGI(TAG, "%s Multicolor light type - %s", addr_info, name);

if (type != MULTICOLOR_LIGHT_TYPE_NONE &&
type != MULTICOLOR_LIGHT_TYPE_SLX &&
type != MULTICOLOR_LIGHT_TYPE_DELTA) {
record_undocumented(data, len);
}

bool type_changed = false;
pool_state_t state_snapshot;

if (ctx->state_mutex && xSemaphoreTake(ctx->state_mutex, pdMS_TO_TICKS(MUTEX_TIMEOUT_MS)) == pdTRUE) {
type_changed = !ctx->pool_state->multicolor_light_type_valid ||
ctx->pool_state->multicolor_light_type != type;
ctx->pool_state->multicolor_light_type = type;
ctx->pool_state->multicolor_light_type_valid = true;
ctx->pool_state->last_update_ms = xTaskGetTickCount() * portTICK_PERIOD_MS;
state_snapshot = *ctx->pool_state;
xSemaphoreGive(ctx->state_mutex);
}

// The type selects the color effect list on the HA light entities, so
// re-publish configured zones when it changes (refreshes their discovery)
if (type_changed && ctx->enable_mqtt) {
for (int i = 0; i < MAX_LIGHT_ZONES; i++) {
if (state_snapshot.lighting[i].configured) {
mqtt_publish_light(&state_snapshot, state_snapshot.lighting[i].zone);
}
}
}

return true;
}

/**
* Handler: Channel count
* Register 0xF4, Slot 0x01
Expand Down Expand Up @@ -2494,7 +2532,7 @@ static bool handle_light_config(

/**
* Handler: Lighting zone color broadcast (CMD 0x07) — log-only
* Pattern: "02 00 50 FF FF 80 00 07 0E E5"
* Dispatched on the CMD byte alone (source-agnostic).
*
* Color companion to the CMD 0x06 light config broadcast; only emitted for
* multicolor-capable zones. The color byte mirrors the zone's Light Zone
Expand Down Expand Up @@ -2523,6 +2561,35 @@ static bool handle_light_color(
return true;
}

/**
* Handler: Light refresh command (CMD 0x3C) — log-only
* Dispatched on the CMD byte alone (source-agnostic).
*
* Refreshes/resyncs a light zone's light; observed during light configuration
* and color operations. What the refresh does at the light hardware is not
* yet confirmed, so this handler just logs.
*/
static bool handle_light_refresh(
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;

uint8_t zone_idx = payload[0];

if (zone_idx > 3) { // PROTOCOL.md 0x3C: zones 1-4 map to index 0-3 only
ESP_LOGW(TAG, "%s Light refresh - zone index out of range 0-3: %d", addr_info, zone_idx);
record_undocumented(data, len);
return true;
}

ESP_LOGI(TAG, "%s Light zone %d refresh", addr_info, zone_idx + 1);

return true;
}

// ======================================================
// Register message handlers (dispatched by register range and slot)
// ======================================================
Expand Down Expand Up @@ -3798,15 +3865,23 @@ static bool dispatch_message(
return handle_register_write_request(data, len, payload, payload_len, addr_info, ctx);
}

// Lighting zone color broadcast (CMD 0x07) — dispatched on the CMD byte
// alone; only the Touchscreen (0x0050) observed sending it so far
if (cmd == 0x07) {
return handle_light_color(data, len, payload, payload_len, addr_info, ctx);
}

// Light refresh command (CMD 0x3C) — dispatched on the CMD byte alone;
// only the Touchscreen (0x0050) observed sending it so far
if (cmd == 0x3C) {
return handle_light_refresh(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);
}

if (match_pattern(data, len, MSG_TYPE_LIGHT_COLOR)) {
return handle_light_color(data, len, payload, payload_len, addr_info, ctx);
}

if (match_pattern(data, len, MSG_TYPE_CONFIG)) {
return handle_config(data, len, payload, payload_len, addr_info, ctx);
}
Expand Down
23 changes: 23 additions & 0 deletions main/message_decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,28 @@ extern const char *LIGHTING_COLOR_NAMES[];
*/
const char* get_channel_type_name(uint8_t type_code);

/**
* Get multicolor light type name from the register 0xF0 light model index
* Known values: MULTICOLOR_LIGHT_TYPE_NONE ("None"), MULTICOLOR_LIGHT_TYPE_SLX
* ("SLX"), MULTICOLOR_LIGHT_TYPE_DELTA ("Delta"). Otherwise formats
* "Unknown (0xXX)" into the caller-supplied buffer and returns it.
* @param type Multicolor light type index from register 0xF0, slot 0x01
* @param fallback_buf Buffer for the unknown-value fallback string
* @param buf_size Size of fallback_buf
* @return Multicolor light type name string
*/
const char* multicolor_light_type_name(uint8_t type, char *fallback_buf, size_t buf_size);

/**
* Get the color codes available on a multicolor light model
* Returns the model's subset of the shared color value space (each code is an
* index into LIGHTING_COLOR_NAMES) — see PROTOCOL.md, Light Zone Color Control.
* @param light_type Multicolor light type index from register 0xF0, slot 0x01
* @param count Out: number of codes (0 if the type is unknown or None)
* @return Pointer to the code array, or NULL if the type has no color table
*/
const uint8_t* multicolor_light_color_codes(uint8_t light_type, int *count);

// Special channel type markers
#define CHANNEL_UNUSED 0x00 // Unused/unconfigured channel
#define CHANNEL_TYPE_HEATER 0xFD // Channel is a heater (handled separately)
Expand Down Expand Up @@ -164,6 +186,7 @@ typedef enum {
REG_ID_HEATER2_ONOFF = 0xE9, // Heater 2 on/off state — tentative, see PROTOCOL.md Appendix A
REG_ID_HEATER2_POOL_SETPOINT = 0xEA, // Heater 2 pool setpoint
REG_ID_HEATER2_SPA_SETPOINT = 0xEB, // Heater 2 spa setpoint — tentative, see PROTOCOL.md Appendix A
REG_ID_MULTICOLOR_LIGHT_TYPE = 0xF0, // System-wide multicolor light type selection (slot 0x01): MULTICOLOR_LIGHT_TYPE_*
REG_ID_CHANNEL_COUNT = 0xF4, // Total number of channels

REG_ID_CHANNEL_CATEGORY_0 = 0xF5, // Channel 0 category (0–7 → 0xF5–0xFC, slot 0x01)
Expand Down
Loading