Skip to content

feat: equipment timer read/write controls (switch, time, select entities)#3

Open
davidbell81 wants to merge 9 commits into
robmarkoski:mainfrom
davidbell81:feat/equipment-timers
Open

feat: equipment timer read/write controls (switch, time, select entities)#3
davidbell81 wants to merge 9 commits into
robmarkoski:mainfrom
davidbell81:feat/equipment-timers

Conversation

@davidbell81
Copy link
Copy Markdown

Summary

This PR adds full read/write support for the Halo controller's equipment timer slots via the cloud WebSocket API.

Library (pychlorinator_cloud)

timers.py — complete rewrite of timer parsing:

  • Correct TimeConfigCharacteristic3 struct confirmed from decompiled app source: <BBBBHBBBBBBB (13 bytes, Pack=1)
  • Fields: TimerType, TimerIndex, TimerMode, TimerEnabled, Enables(uint16 LE), StartMode, StartHour, StartMin, StopMode, StopHour, StopMin, SpeedCode
  • Full EnablesValues bitmask constants: ENABLES_FILTER_PUMP (0x0002), ENABLES_HEATER (0x0004), plus Outlet1-4, Valve1-4, Relay1-2
  • build_timer_config_payload() for constructing 0x0193 write payloads
  • TIMER_TYPE_PUMP, TIMER_TYPE_LIGHTING, TIMER_MODE_WINTER, TIMER_MODE_SUMMER constants

websocket_client.py — timer data wiring:

  • ChlorinatorLiveData gains equipment_timer_configs and lighting_timer_configs (both dict[int, dict], keyed by slot index) — equipment and lighting timers at the same slot index no longer overwrite each other
  • write_timer_slot() method: read-modify-write for any combination of enabled, enables, start_hour/minute, stop_hour/minute, speed_code
  • Cmd 0x0193 included in initial catch-all data request
  • request_timer_data() refreshes all four timer characteristics (0x0190–0x0193)

Custom component (custom_components/astralpool_halo_cloud)

New platforms registered in const.py: switch, time

switch.py (new): 4 × per-slot entities:

  • Timer Slot N Active — enables/disables the timer slot
  • Timer Slot N Heater — enables/disables the heater bit in the slot's enables bitmask

time.py (new): 4 × 2 = 8 entities:

  • Timer Slot N Start / Timer Slot N Stop — set start and stop times for each slot

select.py additions:

  • Timer Slot N Pump Speed — Low / Medium / High per slot

sensor.py: updated _active_timer_count and _timer_summary_attributes to use the renamed equipment_timer_configs field (was timer_configs)

All new timer entities are disabled by default (entity_registry_enabled_default=False) and only become available once the device has streamed its timer config.

Protocol notes

  • The cloud API only streams Winter timer slot configs regardless of season or read-request payload — Summer timer data is only accessible via BLE. This integration stores whatever the device sends and does not attempt season switching.
  • Timer slot reads use cmd 0x0193; writes use cmd 0x0193 with prefix 0x03 and a 20-byte padded frame.
  • All four timer entity groups (active switch, heater switch, start time, stop time, pump speed) perform read-modify-write so only the changed field is altered.

Test plan

  • After restart, equipment_timer_configs dict populates with slot 0 data from device stream
  • Timer Slot 1 Active switch reflects device state and toggles correctly
  • Timer Slot 1 Start / Timer Slot 1 Stop time entities show correct times and can be written
  • Timer Slot 1 Pump Speed select reflects device speed code and writes correctly
  • Timer Slot 1 Heater switch reads and toggles the ENABLES_HEATER bit without clobbering other bits
  • All timer entities become unavailable when cloud connection drops
  • Tag v0.3.0 and update manifest requirements after merging

🤖 Generated with Claude Code

drdavidwbell-create and others added 9 commits May 10, 2026 12:01
Confirmed write protocol from decompiled app source (TimeConfigCharacteristic3).
Fixes parser bugs and adds writable HA entities for timer management.

Parser fixes (pychlorinator_cloud/timers.py):
- slot_index was reading data[0] (TimerType, always 0) — fixed to data[1] (TimerIndex)
- enables was reading 1 byte — fixed to uint16 LE from bytes [4-5]
- Updated TIMER_EQUIPMENT_FLAGS to full EnablesValues bitmask from C# source
- Added timer_mode / start_mode / stop_mode fields to TimerConfig

Library additions (pychlorinator_cloud/websocket_client.py):
- Add timer cmd IDs 400-403 to _request_all_data so timers are fetched on connect
- Add request_timer_data() for on-demand refresh
- Add write_timer_slot() with read-modify-write for unspecified fields
- Add TIMER_CONFIG_CMD_ID = 0x0193

HA component additions:
- const.py: add "switch" and "time" to PLATFORMS
- select.py: HaloTimerSeasonSelect — Summer/Winter profile switcher
- switch.py: HaloTimerSlotSwitch — enable/disable each of 4 timer slots
- time.py: HaloTimerSlotTime — start and stop time pickers per slot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntities

Plain dataclasses lack translation_key and device_class attributes that HA
2026.4 reads from entity_description when computing entity.translation_key
and entity.device_class, causing AttributeError during entity registration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ed + heater controls

- Split timer_configs into equipment_timer_configs (type=0) and lighting_timer_configs
  (type=1) so equipment and lighting timer slot 0 no longer overwrite each other
- Add debug logging for every timer_config frame received
- write_timer_slot now accepts enables param for bitmask control
- New HaloTimerSlotHeaterSwitch: toggles ENABLES_HEATER bit per equipment timer slot
- New HaloTimerSlotPumpSpeedSelect: Low/Medium/High per equipment timer slot
- Update sensor.py timer summary to read from equipment_timer_configs
… configs

The protocol requires specifying [timer_type, slot_index, timer_mode] in
the read request payload — all-zero payload only returns Winter equipment
slot 0. Now request all 16 combinations (pump+lighting × 4 slots × winter+summer).

Store all received configs in all_equipment/lighting_timer_configs keyed by
(slot_index, timer_mode). equipment/lighting_timer_configs are rebuilt to
show only the currently active season's data whenever timer_season is set.

write_timer_slot now resolves the active timer_mode from timer_season and
reads existing config from all_equipment_timer_configs for that mode.
The cloud API ignores the timer config request payload and always returns the
same frame (Winter equipment slot 0). Simplified back to a single request.

_rebuild_active_timer_configs now falls back to the other season's data when
the active season's config isn't available, so entities stay usable in Summer
mode even though the cloud only streams Winter timer configs.
Cloud API only streams Winter timer data regardless of season or request
payload. Permanently use Winter mode; remove all_*_timer_configs dicts,
_rebuild_active_timer_configs, _is_active_timer_mode,
_request_all_timer_slot_configs, and HaloTimerSeasonSelect entity.
Timer slot configs now stored directly by slot_index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@robmarkoski
Copy link
Copy Markdown
Owner

Hi @davidbell81 , really appreciate the depth of this !! apologies for late response, ive been busy at home/work/life.

The <BBBBHBBBBBBB confirmation and the bitmask constants pulled from the decompile are exactly the kind of grounded protocol work this project needs. Thank you.

I should give you a heads-up: in parallel I've been working on a substantial rewrite for 0.3.0-preview.1 that I'm about to publish. It's a cloud-first architecture reset on top of 0.2.3 pychlorinator_cloud/timers.py, websocket_client.py, select.py, sensor.py, the manifest, the entity layout, and the bundled Lovelace card surface all change significantly. There's also 0x0193 write support and an 8-slot Winter/Summer timer surface in the new branch, so this PR overlaps with parts of that work.

What that means for this PR specifically:

  1. It will conflict heavily once 0.3.0-preview.1 lands on main.
  2. I don't want to just close it/ your timers.py parser refinements and the per-slot switch/time/select entity decomposition (vs. my bundled-Lovelace approach) are both worth comparing properly rather than throwing away.

Plan: I'll get 0.3.0-preview.1 out as a tagged release this weekend (family obligations permitting!!!) so the diff is concrete, then come back here and walk through where this PR's ideas can land most likely as targeted follow-ups against the new code rather than a verbatim merge. The bitmask constants and the cmd-0x0193 framing are the clearest wins.!

Have you tested any of this against firmware 2.7+? Your other PR (#2) suggests yes — useful to know the timer write path actually round-trips on the gated firmware too, since I only have fw 2.3 to test against.

@davidbell81
Copy link
Copy Markdown
Author

No apology needed, your time is your time.

On the fw 2.7+ question: yes, all of this is tested round-trip on fw 2.7. The 0x0193 writes with the [0x03, ...] framing reach the device and the new state is reflected back on the next cloud read. Confirmed for enabled, enables (including heater-bit toggle without clobbering other bits in the bitmask), start_hour/stop_hour, and speed_code. Which makes sense — once past the BLE pairing gate on fw 2.7+, the cloud surface is firmware-version-agnostic.

Caveat: I'm only testing on a single Halo Chlor 25 with a single Viron NXT pump, so multi-pump and multi-zone configurations are untested from my end.

On the rewrite: all good. Once preview.1 is tagged I'll diff against this branch and we can figure out which pieces (the <BBBBHBBBBBBB struct refinement, the bitmask constants, per-slot entity decomp vs. bundled Lovelace) make sense as targeted follow-ups. The struct framing and bitmask constants are the most portable bits; entity layout is a taste call and I'm fine deferring to whatever you've designed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants