Skip to content

feat: scene/multi-zone world — per-zone lights, zone services, hypercolor 0.2.0 - #1

Merged
hyperb1iss merged 6 commits into
mainfrom
nova/scene-zone-world
Jun 12, 2026
Merged

feat: scene/multi-zone world — per-zone lights, zone services, hypercolor 0.2.0#1
hyperb1iss merged 6 commits into
mainfrom
nova/scene-zone-world

Conversation

@hyperb1iss

Copy link
Copy Markdown
Owner

What this does

Adapts the integration to Hypercolor's scene/multi-zone system (spec 64) on top of the hypercolor 0.2.0 client that landed in hyperb1iss/hypercolor#103.

Per-zone light entities

The state coordinator now carries the active scene's render groups — zones, groups_revision, and the scene name flow from GET /scenes/active into coordinator data. Every non-display zone of the active scene becomes its own light entity: brightness maps the zone's 0..1 multiplier (lossless round-trip across the full 0-255 range), effects apply with render_group targeting so one zone changes without touching its siblings, and turn-off disables the zone. Zones are scene-scoped, so entities appear dynamically as scenes introduce new zone ids and go unavailable when the active scene no longer carries theirs; stale registry entries are pruned at setup so UUID zone churn can't grow the registry without bound. Display-face zones are excluded — they drive LCDs and have their own service surface.

The WebSocket refresh triggers also widen to the zone-world events (render_group_changed, active_scene_changed, layer_*, brightness_changed) that previously slipped through the prefix filter.

Zone-aware services

apply_effect (and its preset routing) accepts zone_id. New services: set_zone patches zone name/brightness/enabled/make_primary, list_zones returns the zone set with its groups_revision for automations, deactivate_scene returns to the synthesized default, and set_unassigned_behavior sets the off/hold/fallback policy for unclaimed device outputs. Zone services default to the active scene when no scene_id is given.

Fixes that came out of live testing

The live-control number entities were reading bare scalars out of control_values, but a real daemon sends externally tagged values ({"float": 12.0}) — they showed unknown against actual hardware. A control_scalar helper unwraps tagged values while tolerating the bare legacy form, and the e2e fake daemon now serves the real control wire shape so this can't drift silently again.

Verification

  • ruff + ty clean, 37 tests passing (mocked e2e drives a full config entry through zone brightness, zone-targeted effect apply, disable, and registry pruning).
  • The gated real-daemon e2e (HYPERCOLOR_HASS_REAL_E2E=1) passed against a live daemon with 21 devices and a 5-zone active scene.
  • Adversarially reviewed: client signatures, event names, and brightness round-trips verified against the daemon and client sources; the registry-accumulation finding from that review is fixed here.

Requires hypercolor>=0.2.0 (in the main repo as of #103; needs a PyPI release before HACS installs resolve).

🤖 Generated with Claude Code

hyperb1iss and others added 6 commits June 12, 2026 00:23
Mention the companion Lovelace card in the overview section so new
users discover it immediately. Add a lead-in to the dashboard recipe
that links hyper-light-card before the stock entities example, and
update the related-projects bullet to describe it as a companion to
this integration rather than a generic catalog card.
The client realigned with the daemon's scene/multi-zone API: device
payloads carry origin/presentation/connection, control definitions are
name/control_type/default_value, and control values arrive externally
tagged ({"float": 12.0}). Pin hypercolor>=0.2.0,<0.3.0; the local dev
source stays at ../hypercolor/python, the client's home inside the
main repo.

The live-control numbers were reading bare scalars out of
control_values, which a real daemon never sends — they showed unknown
against live hardware. A control_scalar helper unwraps tagged values
(and tolerates the bare form older payloads used), and the control
matcher also accepts the new name field. The e2e fake daemon now
serves the real control wire shape so this can't drift silently again.

Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
The state coordinator now carries the active scene's render groups:
zones, groups_revision, and active_scene_name flow from
GET /scenes/active into coordinator data, and the WebSocket refresh
triggers cover the zone-world events (render_group_changed,
active_scene_changed, layer_*, brightness_changed) that previously
slipped through the prefix filter.

Each non-display zone becomes a light entity: brightness maps the
zone's 0..1 multiplier, effects apply with render_group targeting so
one zone changes without touching its siblings, and turn_off disables
the zone. Zones are scene-scoped, so entities appear dynamically as
scenes introduce new zone ids and go unavailable when the active
scene no longer carries theirs. Display-face zones are excluded —
they drive LCDs, not LEDs, and have their own service surface.

The e2e fake daemon grew a full active-scene fixture with a primary
zone and a zone PATCH route; the test drives brightness, zone-targeted
effect apply, and disable round-trips through a real config entry.

Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
apply_effect (and its preset routing) accepts zone_id for render-group
targeting. New services: set_zone patches zone name/brightness/enabled/
make_primary (brightness as 0-100 percent, scaled to the daemon's 0..1
multiplier), list_zones returns the zone set with its groups_revision
for automations, deactivate_scene returns to the synthesized default,
and set_unassigned_behavior sets the off/hold/fallback policy for
unclaimed device outputs. Zone services default to the active scene
when no scene_id is given.

Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
Matches the manifest version and the hypercolor 0.2.0 client adoption
that brought the scene/multi-zone surface.

Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
Zone ids are per-scene UUIDs, so zone churn (deleting zones, cycling
through scenes) would grow the entity registry without bound — every
vanished zone left a permanently-unavailable light behind. Setup now
removes registry entries under the instance's zone namespace whose
zone no longer exists in the active scene.

Pruning is deliberately setup-only: mid-session scene switches leave
entities unavailable instead of yanking them out from under dashboards
and automations; the registry gets reconciled on the next reload.

Found by adversarial review of the zone adaptation.

Co-Authored-By: Nova (Claude Fable 5) <noreply@anthropic.com>
@hyperb1iss
hyperb1iss merged commit 821fcf1 into main Jun 12, 2026
3 of 4 checks passed
@hyperb1iss
hyperb1iss deleted the nova/scene-zone-world branch June 12, 2026 07:25
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.

1 participant