diff --git a/CHANGELOG.md b/CHANGELOG.md index 61f705a..8264bd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/PROTOCOL.md b/PROTOCOL.md index 1aff5d7..0551ce9 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -40,6 +40,7 @@ This document describes the proprietary serial protocol used by the Connect 10 p - [0x39 — Register Read Request ✅](#0x39--register-read-request-) - [0x3A — Register Write / Control ✅](#0x3a--register-write--control-) - [0x3B — Pump Speed Telemetry ✅](#0x3b--pump-speed-) + - [0x3C — Light Refresh Command ⚠️](#0x3c--light-refresh-command-️) - [0xFD — Controller Day/Time/Clock ✅](#0xfd--controller-daytimeclock-) - [Appendix A: Register Dispatch Table](#appendix-a-register-dispatch-table) - [Implementation Notes](#implementation-notes) @@ -123,7 +124,7 @@ Click any CMD in the first column to jump to the full section in [Commands](#com |----------------------------------------------------------------|-------------------------------------|------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|-------------------------| | [`0x05`](#0x05--touchscreen-activation-ack-️) | Touchscreen Activation Ack | `0x0050` → Broadcast | 1-byte payload `0x01`; sent after favourite changes | Yes (log-only) | | [`0x06`](#0x06--lighting-zone-configuration-) | Lighting Zone Configuration | `0x0050` → Broadcast | | Yes | -| [`0x07`](#0x07--lighting-zone-color-broadcast-️) | Lighting Zone Color Broadcast | `0x0050` → Broadcast | `{zone_idx, color}`; only emitted for multicolor-capable zones; color codes install-specific | Yes (log-only) | +| [`0x07`](#0x07--lighting-zone-color-broadcast-️) | Lighting Zone Color Broadcast | `0x0050` → Broadcast | `{zone_idx, color}`; only emitted for multicolor-capable zones; shared color code table, per-model subsets; dispatched on CMD byte alone | Yes (log-only) | | [`0x0A`](#0x0a--firmware-version-) | Firmware Version | `0x0050`, `0x0062`, `0x0070`, `0x0081`, `0x0084`, `0x00A0`, `0x00F0` → Broadcast | Same `{major, minor}` payload across all sources; dispatched on CMD byte alone | Yes (unified handler) | | [`0x0B`](#0x0b--channel-status-) | Channel Status | `0x0050` → Broadcast | | Yes | | [`0x0D`](#0x0d--active-channels-bitmask-) | Active Channels Bitmask | `0x0050` → `0x006F` Internal Channels | Unicast | Yes | @@ -151,6 +152,7 @@ Click any CMD in the first column to jump to the full section in [Commands](#com | [`0x39`](#0x39--register-read-request-) | Register Read Request | `0x00F0` Gateway, `0x0070` Genus Heater → Broadcast | Dispatched on CMD byte alone (source-agnostic) | Yes (unified handler) | | [`0x3A`](#0x3a--register-write--control-) | Register Write / Control | `0x00F0`, `0x0084` → Broadcast | Same `{register, slot, value}` payload from either source; dispatched on CMD byte alone. Used for Light Zone state (`0xC0`–`0xC7`/slot `0x01`) and color (`0xD0`–`0xD7`/slot `0x01`), Heater Control (`0xE6`/slot `0x00`), and Heater 2 pool setpoint (`0xEA`/slot `0x00`) | Yes (both) | | [`0x3B`](#0x3b--pump-speed-) | Pump Speed Telemetry | `0x00A0` Viron XT Pump → Broadcast | 2-byte big-endian RPM value; broadcast every ~60 seconds | Yes | +| [`0x3C`](#0x3c--light-refresh-command-️) | Light Refresh Command | `0x0050` → Broadcast | 1-byte zone index; refreshes/resyncs the zone's light; observed during light config and color operations; dispatched on CMD byte alone | Yes (log-only) | | [`0xFD`](#0xfd--controller-daytimeclock-) | Controller Day/Time/Clock | `0x0050` → Broadcast | | Yes | --- @@ -235,7 +237,7 @@ Reports a lighting zone's current color — the CMD companion to [0x06 Lighting **Notes:** - The color byte tracks the zone's `0xD0`+/slot `0x01` register exactly: in a capture where the zone-1 color was changed from the Viron Chlorinator's app, the `0x07` payload flipped `00 05` → `00 01` within 120 ms of the `0xD0` register rebroadcasting `0x01` (Red). -- The color enumeration is install/light-brand-specific — see [Light Zone Color Control](#light-zone-color-control-register-0xd00xd7-slot-0x01-️). +- Color codes come from the shared color table (each light model exposes a subset, selected by the Multicolor Light Type register `0xF0`) — see [Light Zone Color Control](#light-zone-color-control-register-0xd00xd7-slot-0x01-️). - ⚠️ Only zone index `0x00` has been observed (the only multicolor-configured zone on the observed install), so the zone-index reading of byte 10 is inferred from the CMD `0x06` layout rather than confirmed across zones. --- @@ -1749,36 +1751,51 @@ Sets a light zone's state (Off/Auto/On). #### Light Zone Color Control (Register `0xD0`–`0xD7`, Slot `0x01`) ⚠️ -Sets a light zone's color — a write to the same Light Zone Color register that the Touchscreen reports via [`0x38` broadcasts](#appendix-a-register-dispatch-table). Observed from the Viron Chlorinator (`0x0084`) when a color is picked in the chlorinator's app. +Sets a light zone's color — a write to the same Light Zone Color register that the Touchscreen reports via [`0x38` broadcasts](#appendix-a-register-dispatch-table). Observed from the Viron Chlorinator (`0x0084`) when a color is picked in the chlorinator's app; also confirmed working when injected with the Gateway source address (`0x00F0`) — the controller applies the write and the touchscreen updates to the new color. -**Example — Set Zone 1 to Blue (app label):** +**Example — Set Zone 1 to Blue (Gateway-sourced, Delta light type, confirmed by injection):** ``` -02 00 84 FF FF 80 00 3A 0F 4D D0 01 0D DE 03 +02 00 F0 FF FF 80 00 3A 0F B9 D0 01 05 D6 03 ^^ Register ID (0xD0 = Zone 1) ^^ Slot (0x01 = Color) - ^^ Color code (0x0D = Blue) - ^^ Checksum (0xD0 + 0x01 + 0x0D = 0xDE) + ^^ Color code (0x05 = Blue on a Delta install) + ^^ Checksum (0xD0 + 0x01 + 0x05 = 0xD6) ``` -**Observed color codes** (labels from the Viron Chlorinator app): +**Example — Set Zone 1 to Magenta (Chlorinator-sourced):** -| Code | App label | -|--------|----------------| -| `0x01` | Magenta | -| `0x02` | Red | -| `0x04` | Orange | -| `0x05` | Disco | -| `0x07` | Aqua | -| `0x08` | White | -| `0x0A` | Ocean | -| `0x0D` | Blue | -| `0x0E` | Green | -| `0x0F` | Custom Colour | -| `0x10` | Custom Pattern | -| `0x11` | Rainbow | +``` +02 00 84 FF FF 80 00 3A 0F 4D D0 01 0D DE 03 + ^^ Register ID (0xD0 = Zone 1) + ^^ Slot (0x01 = Color) + ^^ Color code (0x0D = Magenta) + ^^ Checksum (0xD0 + 0x01 + 0x0D = 0xDE) +``` -⚠️ These codes conflict with the color enumeration seen on the reference system (where e.g. `0x05` = Blue in the `0x38` Light Zone Color broadcast), so the color value space appears to be install/light-brand-specific rather than universal. Pending confirmation via the `0x38` color-register rebroadcast that follows one of these writes. +**Color codes** — all light models share a single color value space; each model exposes a subset of it, selected by the [Multicolor Light Type register (`0xF0`)](#dispatch-table). Confirmed by cycling every color at the touchscreen on a Delta and an SLX install: + +| Code | Color | Delta | SLX | +|--------|---------|:-----:|:---:| +| `0x01` | Red | ✓ | ✓ | +| `0x02` | Orange | ✓ | ✓ | +| `0x03` | Yellow | ✓ | — | +| `0x04` | Green | ✓ | ✓ | +| `0x05` | Blue | ✓ | ✓ | +| `0x06` | Purple | ✓ | — | +| `0x07` | White | ✓ | ✓ | +| `0x08` | User 1 | ✓ | ✓ | +| `0x09` | User 2 | ✓ | — | +| `0x0A` | Disco | ✓ | ✓ | +| `0x0B` | Smooth | ✓ | — | +| `0x0C` | Fade | ✓ | — | +| `0x0D` | Magenta | — | ✓ | +| `0x0E` | Cyan | — | ✓ | +| `0x0F` | Pattern | — | ✓ (shown as "Custom" in the UI) | +| `0x10` | Rainbow | — | ✓ | +| `0x11` | Ocean | — | ✓ | + +These are the values carried by the `0x38` register broadcasts, [CMD 0x07](#0x07--lighting-zone-color-broadcast-️), and `0x3A` writes regardless of source (write confirmed on Delta: writing `0x05` sets Blue and updates the touchscreen). The firmware's `LIGHTING_COLOR_NAMES` table indexes this same space. --- @@ -1855,6 +1872,33 @@ Speed telemetry broadcast by the Viron XT Variable Speed Pump (`0x00A0`). Emitte --- +### 0x3C — Light Refresh Command ⚠️ + +Refreshes/resynchronizes a light zone's light. Broadcast by the Touchscreen (`0x0050`); observed during light configuration sessions and color operations (one capture shows a zone-1 refresh a few seconds before the zone's color-change rebroadcast). + +**Pattern:** `02 00 50 FF FF 80 00 3C 0D 19` + +**Examples:** + +``` +02 00 50 FF FF 80 00 3C 0D 19 00 00 03 + ^^ Zone index (0x00 = Light 1) + +02 00 50 FF FF 80 00 3C 0D 19 01 01 03 + ^^ Zone index (0x01 = Light 2) +``` + +**Data Fields:** + +- Byte 10: Zone index (`0x00` to `0x03` for zones 1-4, matching [CMD 0x06](#0x06--lighting-zone-configuration-)) + +**Notes:** + +- ⚠️ Exactly what the refresh does at the light hardware (e.g. a power-cycle color resync) is not yet confirmed; the zone-index reading of byte 10 is based on refreshes observed for zones 1 and 2. +- Decoded in code by `handle_light_refresh` — dispatched on the CMD byte alone (source-agnostic), log-only, no `pool_state` update. + +--- + ### 0xFD — Controller Day/Time/Clock ✅ Current time from the Touchscreen's (`0x0050`) internal clock. Broadcast periodically for device time synchronisation. @@ -1918,7 +1962,7 @@ The register ID and slot together determine the message meaning. The slot distin | `0xC0`–`0xC3` ⚠️| `0x0D` | Unknown | Only `0xFF` observed. Repeats ~every 8 minutes | | `0xC8` ⚠️ | `0x00` | Unknown | Only `0x01` observed. Repeats ~every 8 minutes | | `0xD0`–`0xD1` | `0x02` | Valve Labels | Null-terminated ASCII string | -| `0xD0`–`0xD7` | `0x01` | Light Zone Color | 1-byte color code — writable via CMD `0x3A`; the code enumeration appears install/light-brand-specific, see [Light Zone Color Control](#light-zone-color-control-register-0xd00xd7-slot-0x01-️) | +| `0xD0`–`0xD7` | `0x01` | Light Zone Color | 1-byte color code — writable via CMD `0x3A`; one shared code space across light models, each model exposing a subset selected by register `0xF0` — full table in [Light Zone Color Control](#light-zone-color-control-register-0xd00xd7-slot-0x01-️) | | `0xE0`–`0xE7` | `0x01` | Light Zone Active | 1-byte binary (`0x00`=Inactive, `0x01`=Active) | | `0xF4` | `0x01` | Channel Count | 1-byte total number of channels in the system | | `0xE6` | `0x00` | Heater State (Heater 1) | 1-byte (`0x00`=Off, `0x01`=On) | @@ -1929,7 +1973,7 @@ The register ID and slot together determine the message meaning. The slot distin | `0xEA` | `0x00` | Heater 2 Pool Setpoint | 1-byte °C value — writable via gateway CMD `0x3A`. See note below. | | `0xEB` | `0x00` | Heater 2 Spa Setpoint | 1-byte °C value — writable via gateway CMD `0x3A`. See note below. | | `0xEC` ⚠️ | `0x00` | Unknown | Only `0x01` observed. Repeats ~every 8 minutes | -| `0xF0` ⚠️ | `0x01` | Unknown | Only `0xFF` observed. Repeats ~every 8 minutes | +| `0xF0` | `0x01` | Multicolor Light Type | 1-byte system-wide light model index: `0x00`=SLX, `0x01`=Delta, `0xFF`=none selected — see note below | | `0xF5`–`0xFC` | `0x01` | Channel Category | 1-byte category code (`0x01`=Pool equipment, `0x02`=Light, `0x03`=Controlled Heater Power) — see note below | **Notes:** @@ -1941,6 +1985,7 @@ The register ID and slot together determine the message meaning. The slot distin - **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. - **`0x20` (Active Favourite) — confirmed ✅**: slot `0x03` is the Favourite slot, and `0x20` sits immediately before the Favourite Enable block (`0x21`–`0x28`). Reports which favourite is currently active, using the same value space as CMD [`0x2A`](#0x2a--favourite-control-command-): `0x00`=Pool, `0x01`=Spa, `0x02`–`0x07`=user Favourites 1–6; `0xFF` = no favourite active. The All Off/All Auto command values (`0x80`/`0x81`) never appear here — the controller does not remember them as states: All Off reports as `0x00` (it is implemented as Pool mode with all channels off; the accompanying [0x14 Mode](#0x14--mode-spapool-) broadcast also says Pool) and All Auto reports as `0xFF`. Broadcast ~1 s after a gateway-commanded activation ([0x2A](#0x2a--favourite-control-command-)), when an active favourite is knocked out by a manual channel change, and in the periodic ~8-minute register dump — but **not** when a favourite is activated at the touchscreen itself (the new value then only appears in the next dump). +- **`0xF0` (Multicolor Light Type) — confirmed ✅**: system-wide multicolor light model selection from the touchscreen's light setup. Confirmed by UI experiment: setting the light type to SLX multicolor rebroadcasts `0x00`, Delta rebroadcasts `0x01`, and with no multicolor light configured the register reads `0xFF` (none selected — same sentinel convention as Active Favourite `0x20`). A single global register, not per-zone (no `0xF1`–`0xF3` siblings exist, and the Gateway's periodic polling requests `0xF0` only), sitting at the front of the slot-`0x01` system-config block (`0xF4` channel count, `0xF5`–`0xFC` channel categories). Only the SLX and Delta indexes have been mapped; other models in the setup list presumably take further values. The selected model determines which subset of the shared color code table applies to the Light Zone Color registers and [CMD 0x07](#0x07--lighting-zone-color-broadcast-️) — full table in [Light Zone Color Control](#light-zone-color-control-register-0xd00xd7-slot-0x01-️). - **`0x90`–`0x97` (Light Zone Enabled)**: 1-byte flag reporting whether the light zone is configured in the controller: `0x01` = configured/present, `0x00` = not configured. Sits in the same slot-`0x01` per-zone register family as the other light-zone blocks (`0xA0` multicolor, `0xB0` name, `0xC0` state, `0xD0` color, `0xE0` active). Enabled zones are rebroadcast regularly; disabled zones are only broadcast at startup or after a configuration change. Only `0x90`–`0x93` (zones 1–4) observed; `0x94`–`0x97` inferred from the 8-wide sibling blocks. `0x90`–`0x93` overlaps the slot-`0x02` Channel State range — distinguished by slot as usual. - **`0xF5`–`0xFC` (Channel Category) — confirmed ✅**: per-channel category code following the Channel Count register (`0xF4`): `0xF5` = Channel 1, `0xF6` = Channel 2, … `0xFC` = Channel 8. Values: `0x01` = Pool equipment, `0x02` = Light, `0x03` = Controlled Heater Power. Registers for unused channels are not broadcast (only `0xF5`–`0xFB` observed on a 7-channel system, so `0xFC` = Channel 8 is inferred from the range width of the other per-channel register blocks). This is a coarser classification than the per-channel [Channel Type](#0x0b--channel-status-) codes at `0x6C`–`0x73`. diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 0b67b16..dcec64d 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -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" diff --git a/main/lighting_colors.c b/main/lighting_colors.c new file mode 100644 index 0000000..1f8397a --- /dev/null +++ b/main/lighting_colors.c @@ -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 +}; diff --git a/main/message_decoder.c b/main/message_decoder.c index b654b82..066a5c9 100644 --- a/main/message_decoder.c +++ b/main/message_decoder.c @@ -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"; @@ -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"}, }; @@ -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 @@ -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 { @@ -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); @@ -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); @@ -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 @@ -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 @@ -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) // ====================================================== @@ -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); } diff --git a/main/message_decoder.h b/main/message_decoder.h index ea9d325..f62ca76 100644 --- a/main/message_decoder.h +++ b/main/message_decoder.h @@ -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) @@ -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) diff --git a/main/mqtt_commands.c b/main/mqtt_commands.c index cb5f4f6..7c6afae 100644 --- a/main/mqtt_commands.c +++ b/main/mqtt_commands.c @@ -103,6 +103,50 @@ static void handle_light_command(int zone, const char *payload, int payload_len) send_uart_command(cmd, sizeof(cmd)); } +static void handle_light_color_command(int zone, const char *payload, int payload_len) +{ + ESP_LOGI(TAG, "Light zone %d color command: %.*s", zone, payload_len, payload); + + // Map the color name (an HA effect_list entry) back to its code in the + // shared color table; the discovery effect list constrains what HA offers + uint8_t color = 0; + for (int i = 1; i < LIGHTING_COLOR_COUNT; i++) { + if ((int)strlen(LIGHTING_COLOR_NAMES[i]) == payload_len && + strncmp(payload, LIGHTING_COLOR_NAMES[i], payload_len) == 0) { + color = (uint8_t)i; + break; + } + } + if (color == 0) { + ESP_LOGE(TAG, "Unknown light color: %.*s", payload_len, payload); + return; + } + + uint8_t reg_id = REG_ID_LIGHT_ZONE_COLOR_0 + (zone - 1); + + // Build UART command + // Pattern: 02 00 F0 FF FF 80 00 3A 0F B9 [REG_ID] 01 [COLOR] [CHECKSUM] 03 + uint8_t cmd[] = { + 0x02, // START + 0x00, 0xF0, // SOURCE: Internet Gateway + 0xFF, 0xFF, // DEST: Broadcast + 0x80, 0x00, // CONTROL + 0x3A, 0x0F, 0xB9, // Command pattern + reg_id, // Register ID (light zone color) + 0x01, // Slot ID + color, // Color code (shared color table) + 0x00, // Checksum (calculated below) + 0x03 // END + }; + + // Calculate checksum (sum of bytes 10-12) + cmd[13] = (reg_id + 0x01 + color) & 0xFF; + + ESP_LOGI(TAG, "Sending light zone %d color %s (0x%02X) command", zone, + LIGHTING_COLOR_NAMES[color], color); + send_uart_command(cmd, sizeof(cmd)); +} + // ====================================================== // Heater Control // ====================================================== @@ -446,17 +490,24 @@ void mqtt_handle_command(const char *topic, int topic_len, const char *data, int } } else if (strncmp(cmd_topic, "light/", 6) == 0 && cmd_topic_len > 10) { - // Extract zone number (format: "light/N/set") + // Extract zone number (formats: "light/N/set", "light/N/color/set") char *endptr; int zone = (int)strtol(cmd_topic + 6, &endptr, 10); if (endptr == cmd_topic + 6 || *endptr != '/') { ESP_LOGE(TAG, "Invalid light topic format: %s", cmd_topic); return; } - if (zone >= 1 && zone <= MAX_LIGHT_ZONES) { + if (zone < 1 || zone > MAX_LIGHT_ZONES) { + ESP_LOGE(TAG, "Invalid light zone: %d", zone); + return; + } + int suffix_len = cmd_topic_len - (int)(endptr - cmd_topic); + if (suffix_len == 4 && strncmp(endptr, "/set", 4) == 0) { handle_light_command(zone, data, data_len); + } else if (suffix_len == 10 && strncmp(endptr, "/color/set", 10) == 0) { + handle_light_color_command(zone, data, data_len); } else { - ESP_LOGE(TAG, "Invalid light zone: %d", zone); + ESP_LOGE(TAG, "Invalid light topic format: %s", cmd_topic); } } else if (strncmp(cmd_topic, "valve/", 6) == 0 && cmd_topic_len > 8) { diff --git a/main/mqtt_discovery.c b/main/mqtt_discovery.c index 3e5f599..91e2cd6 100644 --- a/main/mqtt_discovery.c +++ b/main/mqtt_discovery.c @@ -621,7 +621,8 @@ static void publish_channel_discovery(const char *device_id, const char *mac_suf // ====================================================== static void publish_light_discovery(const char *device_id, const char *mac_suffix, - int zone_num, const char *zone_name) + int zone_num, const char *zone_name, + uint8_t multicolor_light_type) { char avail_topic[128]; char state_topic[128]; @@ -651,6 +652,28 @@ static void publish_light_discovery(const char *device_id, const char *mac_suffi cJSON_AddStringToObject(root, "unique_id", uid); cJSON_AddStringToObject(root, "object_id", uid); cJSON_AddStringToObject(root, "availability_topic", avail_topic); + + // Color selection as an effect list for multicolor zones: the model's + // subset of the shared color table (register 0xF0 selects the model) + int color_count = 0; + const uint8_t *color_codes = multicolor_light_color_codes(multicolor_light_type, &color_count); + if (color_codes && color_count > 0) { + char effect_command_topic[128]; + snprintf(effect_command_topic, sizeof(effect_command_topic), + "pool/%s/light/%d/color/set", device_id, zone_num); + + cJSON *effect_list = cJSON_CreateArray(); + for (int i = 0; i < color_count; i++) { + if (color_codes[i] < LIGHTING_COLOR_COUNT) { + cJSON_AddItemToArray(effect_list, cJSON_CreateString(LIGHTING_COLOR_NAMES[color_codes[i]])); + } + } + cJSON_AddItemToObject(root, "effect_list", effect_list); + cJSON_AddStringToObject(root, "effect_command_topic", effect_command_topic); + cJSON_AddStringToObject(root, "effect_state_topic", state_topic); + cJSON_AddStringToObject(root, "effect_value_template", "{{ value_json.color }}"); + } + cJSON_AddItemToObject(root, "device", build_device_cjson(device_id, mac_suffix)); char *json_str = cJSON_PrintUnformatted(root); @@ -953,7 +976,7 @@ void mqtt_publish_channel_discovery_single(int channel_num, const char *channel_ publish_channel_discovery(device_id, mac_suffix, channel_num, channel_name); } -void mqtt_publish_light_discovery_single(int zone_num, const char *zone_name) +void mqtt_publish_light_discovery_single(int zone_num, const char *zone_name, uint8_t multicolor_light_type) { char device_id[32]; mqtt_get_device_id(device_id, sizeof(device_id)); @@ -962,7 +985,7 @@ void mqtt_publish_light_discovery_single(int zone_num, const char *zone_name) device_get_mac_suffix(mac_suffix, sizeof(mac_suffix)); ESP_LOGI(TAG, "Publishing discovery for light zone %d: %s", zone_num, zone_name ? zone_name : "(unnamed)"); - publish_light_discovery(device_id, mac_suffix, zone_num, zone_name); + publish_light_discovery(device_id, mac_suffix, zone_num, zone_name, multicolor_light_type); } // ====================================================== diff --git a/main/mqtt_discovery.h b/main/mqtt_discovery.h index 216c2b5..714b17b 100644 --- a/main/mqtt_discovery.h +++ b/main/mqtt_discovery.h @@ -16,7 +16,9 @@ void mqtt_publish_update_discovery_single(void); void mqtt_publish_channel_discovery_single(int channel_num, const char *channel_name); // Publish individual light discovery (called when light first configured or name changes) -void mqtt_publish_light_discovery_single(int zone_num, const char *zone_name); +// multicolor_light_type: MULTICOLOR_LIGHT_TYPE_* — pass MULTICOLOR_LIGHT_TYPE_NONE +// for non-multicolor zones; a known type adds the color effect list to the entity +void mqtt_publish_light_discovery_single(int zone_num, const char *zone_name, uint8_t multicolor_light_type); // Publish individual valve discovery (called when valve first configured or name changes) void mqtt_publish_valve_discovery_single(int valve_num, const char *valve_name); diff --git a/main/mqtt_poolclient.c b/main/mqtt_poolclient.c index 0b46d22..8402968 100644 --- a/main/mqtt_poolclient.c +++ b/main/mqtt_poolclient.c @@ -179,10 +179,12 @@ static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_ esp_mqtt_client_subscribe(s_mqtt_client, topic, 0); } - // Subscribe to light commands (zones 1-MAX_LIGHT_ZONES) + // Subscribe to light commands (zones 1-MAX_LIGHT_ZONES): state and color for (int i = 1; i <= MAX_LIGHT_ZONES; i++) { snprintf(topic, sizeof(topic), "pool/%s/light/%d/set", device_id, i); esp_mqtt_client_subscribe(s_mqtt_client, topic, 0); + snprintf(topic, sizeof(topic), "pool/%s/light/%d/color/set", device_id, i); + esp_mqtt_client_subscribe(s_mqtt_client, topic, 0); } // Subscribe to heater commands: on/off and pool/spa setpoints per heater diff --git a/main/mqtt_publish.c b/main/mqtt_publish.c index 2454272..2283b01 100644 --- a/main/mqtt_publish.c +++ b/main/mqtt_publish.c @@ -32,6 +32,11 @@ static struct { bool service_mode; } s_discovery_published = {0}; +// Effective multicolor light type each zone's discovery was last published with +// (drives the color effect list); only meaningful while the zone's +// s_discovery_published.lights flag is set +static uint8_t s_light_discovery_type[MAX_LIGHT_ZONES]; + // ====================================================== // Setpoint and Temperature Publishing // ====================================================== @@ -365,17 +370,26 @@ void mqtt_publish_light(const pool_state_t *current_state, uint8_t zone) const char *zone_name = (light->name_valid && light->name_id < LIGHT_ZONE_NAME_COUNT) ? LIGHT_ZONE_NAME_TABLE[light->name_id] : NULL; - // Re-publish discovery if the zone name has changed since last publish + // Multicolor light type in effect for this zone: the system-wide model + // selection, but only once the zone is known multicolor-capable + uint8_t effective_light_type = + (light->multicolor_valid && light->multicolor && current_state->multicolor_light_type_valid) ? + current_state->multicolor_light_type : MULTICOLOR_LIGHT_TYPE_NONE; + + // Re-publish discovery if the zone name or the effective light type + // (drives the color effect list) changed since last publish if (s_discovery_published.lights[idx] && (s_last_published_state.lighting[idx].name_id != light->name_id || - s_last_published_state.lighting[idx].name_valid != light->name_valid)) { + s_last_published_state.lighting[idx].name_valid != light->name_valid || + s_light_discovery_type[idx] != effective_light_type)) { s_discovery_published.lights[idx] = false; } - // Publish discovery if this is the first time seeing this light zone (or name changed) + // Publish discovery if this is the first time seeing this light zone (or config changed) if (!s_discovery_published.lights[idx]) { - mqtt_publish_light_discovery_single(zone, zone_name); + mqtt_publish_light_discovery_single(zone, zone_name, effective_light_type); s_discovery_published.lights[idx] = true; + s_light_discovery_type[idx] = effective_light_type; } // Check if anything changed @@ -400,12 +414,10 @@ void mqtt_publish_light(const pool_state_t *current_state, uint8_t zone) static const char *STATE_NAMES[] = {"Off", "Auto", "On"}; const char *state_name = (light->state < 3) ? STATE_NAMES[light->state] : "Unknown"; - // Color names (subset - full list is in main.c) - static const char *COLOR_NAMES[] = { - "Unknown", "Red", "Orange", "Yellow", "Green", "Blue", "Purple", "White", - "User1", "User2", "Disco", "Smooth", "Fade", "Magenta", "Cyan" - }; - const char *color_name = (light->color < 15) ? COLOR_NAMES[light->color] : "Unknown"; + // Color names from the shared color table — must match the discovery + // effect_list entries so HA can bind the current effect + const char *color_name = (light->color < LIGHTING_COLOR_COUNT) ? + LIGHTING_COLOR_NAMES[light->color] : "Unknown"; char fallback_name[24]; if (!zone_name) { diff --git a/main/pool_state.h b/main/pool_state.h index 291765f..afadb0e 100644 --- a/main/pool_state.h +++ b/main/pool_state.h @@ -162,6 +162,11 @@ typedef struct { #define FAVOURITE_ALL_AUTO 0x81 #define FAVOURITE_NONE 0xFF // no favourite active (register 0x20, slot 0x03) +// Multicolor light type selection (register 0xF0, slot 0x01) — system-wide light model +#define MULTICOLOR_LIGHT_TYPE_SLX 0x00 +#define MULTICOLOR_LIGHT_TYPE_DELTA 0x01 +#define MULTICOLOR_LIGHT_TYPE_NONE 0xFF // no multicolor light configured + typedef struct { char name[32]; // Label from register 0x31+index, slot 0x03 bool enabled; // Enable flag from register 0x21+index, slot 0x03 @@ -192,6 +197,10 @@ typedef struct { // Lighting (up to MAX_LIGHT_ZONES) lighting_state_t lighting[MAX_LIGHT_ZONES]; + // System-wide multicolor light type selection (register 0xF0, slot 0x01): MULTICOLOR_LIGHT_TYPE_* + uint8_t multicolor_light_type; + bool multicolor_light_type_valid; + // Register labels (general storage for register names like favourites, etc.) register_label_t register_labels[MAX_REGISTER_LABELS]; diff --git a/main/web_handlers.c b/main/web_handlers.c index 39a478e..c0da794 100644 --- a/main/web_handlers.c +++ b/main/web_handlers.c @@ -263,6 +263,7 @@ static esp_err_t home_get_handler(httpd_req_t *req) "data.lighting.forEach(lt=>{" "let s=lt.state;if(lt.active&<.color)s+=': '+lt.color;" "rows.push(['Lighting zone '+lt.zone,s]);});" + "if(data.multicolor_light_type)rows.push(['Multicolor Light Type',data.multicolor_light_type]);" "const c=data.chlorinator;" "if(c.ph_reading!==null)rows.push(['pH',c.ph_reading+' (setpoint '+c.ph_setpoint+')']);" "if(c.orp_reading!==null)rows.push(['ORP',c.orp_reading+'mV (setpoint '+c.orp_setpoint+'mV)']);" @@ -801,6 +802,15 @@ static esp_err_t status_get_handler(httpd_req_t *req) } cJSON_AddItemToObject(root, "lighting", lighting); + // Multicolor light type (register 0xF0, slot 0x01) + if (state.multicolor_light_type_valid) { + char type_buf[16]; + cJSON_AddStringToObject(root, "multicolor_light_type", + multicolor_light_type_name(state.multicolor_light_type, type_buf, sizeof(type_buf))); + } else { + cJSON_AddNullToObject(root, "multicolor_light_type"); + } + // Valves cJSON *valves = cJSON_CreateArray(); for (int i = 0; i < state.num_valve_slots && i < MAX_VALVE_SLOTS; i++) { diff --git a/test/run_tests.sh b/test/run_tests.sh index 0867e19..f50df9d 100755 --- a/test/run_tests.sh +++ b/test/run_tests.sh @@ -44,17 +44,21 @@ for test_src in test_*.c; do binary="./run_${module}" # message_decoder.c now shares its data/discovery packet classification - # with framing.c - link it in for that one module. + # with framing.c - link it in for that one module. The lighting color + # name table lives in lighting_colors.c, needed by both message_decoder + # and mqtt_commands. extra_srcs=() if [ "$module" = "message_decoder" ]; then - extra_srcs=(../main/framing.c) + extra_srcs=(../main/framing.c ../main/lighting_colors.c) + elif [ "$module" = "mqtt_commands" ]; then + extra_srcs=(../main/lighting_colors.c) fi echo "========================================" echo " Compiling: $test_src" echo "========================================" - if gcc -I. -I.. -o "$binary" "$test_src" "$main_src" ${extra_srcs[@]+"${extra_srcs[@]}"} "${SHARED[@]}" 2>&1; then + if gcc -fno-common -I. -I.. -o "$binary" "$test_src" "$main_src" ${extra_srcs[@]+"${extra_srcs[@]}"} "${SHARED[@]}" 2>&1; then if "$binary"; then PASS=$((PASS + 1)) else @@ -74,7 +78,7 @@ if [ -f test_framing.c ]; then echo " Compiling: test_framing.c" echo "========================================" - if gcc -I. -I.. -o ./run_framing test_framing.c "${SHARED[@]}" 2>&1; then + if gcc -fno-common -I. -I.. -o ./run_framing test_framing.c "${SHARED[@]}" 2>&1; then if ./run_framing; then PASS=$((PASS + 1)) else @@ -94,7 +98,7 @@ if [ -f test_replay.c ]; then echo " Compiling: test_replay.c" echo "========================================" - if gcc -I. -I.. -o ./run_replay test_replay.c ../main/message_decoder.c ../main/framing.c "${SHARED[@]}" 2>&1; then + if gcc -fno-common -I. -I.. -o ./run_replay test_replay.c ../main/message_decoder.c ../main/framing.c ../main/lighting_colors.c "${SHARED[@]}" 2>&1; then if ./run_replay; then PASS=$((PASS + 1)) else diff --git a/test/test_mqtt_commands.c b/test/test_mqtt_commands.c index ac71859..8c65af5 100644 --- a/test/test_mqtt_commands.c +++ b/test/test_mqtt_commands.c @@ -390,6 +390,45 @@ void test_light_invalid_payload(void) TEST_ASSERT(s_uart_calls == 0, "light/1/set BLINK: no UART write (invalid payload)"); } +void test_light_1_color_blue(void) +{ + send_cmd("light/1/color/set", "Blue"); + + // reg_id=0xD0, color=0x05 (Blue), checksum=(0xD0+0x01+0x05)&0xFF=0xD6 + uint8_t expected[] = { + 0x02, 0x00, 0xF0, 0xFF, 0xFF, 0x80, 0x00, + 0x3A, 0x0F, 0xB9, + 0xD0, 0x01, 0x05, + 0xD6, + 0x03 + }; + TEST_ASSERT(s_uart_calls == 1, "light/1/color/set Blue: exactly one UART write"); + TEST_ASSERT(s_uart_len == sizeof(expected), "light/1/color/set Blue: correct length"); + TEST_ASSERT(memcmp(s_uart_buf, expected, sizeof(expected)) == 0, "light/1/color/set Blue: correct bytes"); +} + +void test_light_2_color_magenta(void) +{ + send_cmd("light/2/color/set", "Magenta"); + + // reg_id=0xD1, color=0x0D (Magenta), checksum=(0xD1+0x01+0x0D)&0xFF=0xDF + uint8_t expected[] = { + 0x02, 0x00, 0xF0, 0xFF, 0xFF, 0x80, 0x00, + 0x3A, 0x0F, 0xB9, + 0xD1, 0x01, 0x0D, + 0xDF, + 0x03 + }; + TEST_ASSERT(s_uart_calls == 1, "light/2/color/set Magenta: exactly one UART write"); + TEST_ASSERT(memcmp(s_uart_buf, expected, sizeof(expected)) == 0, "light/2/color/set Magenta: correct bytes"); +} + +void test_light_color_unknown(void) +{ + send_cmd("light/1/color/set", "Chartreuse"); + TEST_ASSERT(s_uart_calls == 0, "light/1/color/set Chartreuse: no UART write (unknown color)"); +} + // ====================================================== // Valve tests // ====================================================== @@ -496,6 +535,9 @@ int main(void) test_light_1_on(); test_light_2_off(); test_light_invalid_payload(); + test_light_1_color_blue(); + test_light_2_color_magenta(); + test_light_color_unknown(); printf("\n--- Valve Tests ---\n"); test_valve_1_on();