Skip to content

fix: handle multi-gang dimmer lights across all platforms (#681) - #683

Merged
asantaga merged 4 commits into
asantaga:devfrom
wolfgang-steinberg:fix/multi-gang-dimmer-lights
Aug 11, 2026
Merged

fix: handle multi-gang dimmer lights across all platforms (#681)#683
asantaga merged 4 commits into
asantaga:devfrom
wolfgang-steinberg:fix/multi-gang-dimmer-lights

Conversation

@wolfgang-steinberg

@wolfgang-steinberg wolfgang-steinberg commented Aug 7, 2026

Copy link
Copy Markdown

Fixes #681.

Problem

With a multi-gang dimmer (2GANG/DIMMER/2) both channels share the same device
id. The id property in aioWiserHeatAPI only disambiguates
2GANG/SWITCH/2 (id * 1000 + endpoint), not 2GANG/DIMMER/2, so
lights.get_by_id(id) matches more than one light and returns a list:

lights = [light for light in self.all if light.id == light_id]
if lights and len(lights) == 1:
    return lights[0]
return lights          # <-- list for a multi-gang dimmer

This breaks every platform that builds entities from lights:

  • light.pyWiserLight.__init__ stored the list in self._device and
    the next line (self._device.schedule) raised, aborting light setup (no
    lights at all).
  • select.py — same list, .available_modes raised → the whole select
    platform
    failed to set up (Mode / Power On Behaviour / LED Indicator selects
    for all devices, not just lights, were lost).
  • binary_sensor.py, switch.py, sensor.py — no crash, but the second
    channel's entities collided on unique_id and were dropped
    (… does not generate unique IDs … already exists - ignoring): the four light
    capability sensors, the "Away Mode Turns Off" / "Device Lock" / "Identify"
    switches, and the Signal sensor.

Fix

Two shapes, depending on whether the entity belongs to the channel or the
physical device:

Per-channel entities (independently controllable): key by the unique
per-channel light_id, resolve via lights.get_by_light_id() (always returns a
single object), derive the shared physical device_id from the resolved light
for grouping, and base name/unique_id on the per-channel light.

  • light.py: WiserLight / WiserDimmableLight
  • select.py: WiserLightModeSelect, WiserLightPowerOnBehaviourSelect,
    WiserLightLedIndicatorSelect
  • binary_sensor.py: WiserStateIsDimmable (given a light-aware __init__ so
    the generic BaseBinarySensor stays untouched for rooms/shutters)
  • switch.py: WiserLightAwayActionSwitch

Device-level entities (one physical device, one entity): emit once per
physical device id instead of once per channel.

  • switch.py: "Device Lock" and "Identify" (WiserDeviceSwitch loop)
  • sensor.py: the Signal sensor

Entity migration

_WiserLight.id (hub Devices section) and light_id (hub Lighting section) are
different id spaces even for single-gang lights, so on upgrade the unique_id
of every per-channel light entity changes — the light, the mode/LED/power-on
selects, the away-mode switch and the four capability binary_sensors.
241f547 adds build_light_unique_id_migration() (in helpers.py), run from
async_setup_entry before the platforms create entities, which rebuilds each
old unique_id from live hub data and renames the matching registry entry to
the new light_id-based one, so entity_ids, history and dashboards carry over.
The rename callback checks async_get_entity_id first, because
async_update_entity raises on a pre-existing target and would otherwise abort
config-entry setup.

Multi-gang dimmer channels created before the fix are deliberately left to
orphan: their light/select platforms had crashed (nothing to migrate) and their
binary_sensor/switch entities were keyed on the shared device id (one entity
for two channels), so there is no unambiguous target. Single-gang-only setups
migrate completely. The device-level dedupe (Device Lock, Identify, Signal)
keeps its unique_ids and does not migrate.

Testing

Tested on a live hub with five 2-gang dimmers (12 light channels) plus
single-channel lights, on integration 3.4.20 with aioWiserHeatAPI==1.7.3.

  • Before: light platform crashed (0 lights), select platform crashed, and the
    second channel of every dimmer lost its select / switch / binary-sensor
    entities.
  • After: startup log is clean (no crash, no duplicate-unique-id warnings), the
    entity registry has a Mode select for all 12 channels (both channels of
    every 2-gang dimmer), device-level switches/sensors appear once per device,
    and single-channel lights are unaffected.
  • Migration cross-checked against the live registry: every new unique_id
    build_light_unique_id_migration() computes matches an existing entity.
  • Added tests/test_light_unique_id_migration.py (single-gang mapping,
    multi-gang skip, no-op filter). 71cc6f7 also repairs test_switch_hot_water.py,
    whose wiser.const stub lacked the ENTITY_PREFIX this PR now imports in
    switch.py. Full unittest suite green.

Multi-gang dimmers (2GANG/DIMMER/2) share one device id across their
channels. get_by_id() returns a list when several lights resolve to the
same id, so WiserLight.__init__ received a list instead of a single
device and the light platform crashed during setup (0 lights).

Key each entity by the unique per-channel light_id and resolve it via
get_by_light_id(), which always returns a single object. Derive the
(possibly shared) physical device id from the resolved light so device
grouping still works. Base name and unique_id on the per-channel light
so the two channels of a multi-gang dimmer no longer collide.
…, sensor (asantaga#681)

The same shared-device-id problem that broke light.py affects every other
platform that builds entities from lights.

- select.py: get_by_id() returns a list for a multi-gang dimmer, so
  .available_modes raised and the whole select platform failed to set up
  (Mode / Power On Behaviour / LED Indicator selects for all devices were
  lost). Resolve the three light selects by the unique per-channel light_id
  and make name/unique_id per channel.
- binary_sensor.py: the four light capability sensors collided on unique_id,
  dropping the second channel. Give WiserStateIsDimmable a light-aware
  __init__ and per-channel name so BaseBinarySensor stays generic.
- switch.py: WiserLightAwayActionSwitch is per-channel now (light_id +
  per-channel name/unique_id). Device Lock and Identify act on the physical
  device, so they are emitted once per device id instead of once per channel.
- sensor.py: signal strength is a property of the physical device, so the
  signal sensor is emitted once per device id.

Verified on a hub with five 2-gang dimmers (12 light channels): before, the
select platform crashed and the second channel lost its select/switch/binary
sensor entities; after, every channel has its own entities and single-channel
lights are unaffected.
@wolfgang-steinberg wolfgang-steinberg changed the title fix: handle multi-gang dimmer lights (#681) fix: handle multi-gang dimmer lights across all platforms (#681) Aug 8, 2026
@wolfgang-steinberg

Copy link
Copy Markdown
Author

Extended this PR beyond light.py: the same shared-device-id issue also crashed the select platform and dropped the second channel's entities in binary_sensor, switch and sensor. Second commit covers all of them (per-channel where the entity is channel-specific, one-per-device for Device Lock / Identify / Signal). Verified on a hub with five 2-gang dimmers — updated the description with details. Still happy to add an entity-migration shim if you want to preserve existing unique IDs.

@asantaga

asantaga commented Aug 8, 2026

Copy link
Copy Markdown
Owner

thank you , let me review the PR and then I'll merge it in.. Alas I dont have a V2 hub , or lights, so I'll just be doing a general review

@msp1974 Mark, did u upgrade your hub?

@asantaga

asantaga commented Aug 8, 2026

Copy link
Copy Markdown
Owner

@wolfgang-steinberg

Happy to add an async_migrate_entries shim to carry the
old unique IDs across if you'd prefer — just say the word

Will we break any existing integrations? If we will then yes we'll need the async shim, otherwise I dont think its an issue..

@wolfgang-steinberg

Copy link
Copy Markdown
Author

Yes — existing setups are affected. The per-channel entities (light, select, switch, binary_sensor) change from name-based to light_id-based unique IDs, so on upgrade HA orphans the old entities and creates new ones; automations/dashboards referencing the old entity_ids would break and history restarts on the new entities.

I ran into exactly this upgrading my own hub: 35 entities went stale (28 capability binary_sensors + 7 away-mode switches). The device-level entities I de-duplicated (Device Lock, Identify, Signal) keep their unique IDs, so those do not migrate — only the per-channel ones do.

So yes, we need the shim. I'll add an async_migrate_entries that maps the old unique IDs to the new light_id-based ones so entities and history carry over, and follow up here with a commit.

switch.py began importing ENTITY_PREFIX from .const in the multi-gang fix
(a4acfc6), but the module stub in test_switch_hot_water.py did not expose it,
so loading the switch module raised ImportError and the whole test errored.
Add ENTITY_PREFIX to the stub to restore the test.
…santaga#681)

Keying light entities on the per-channel light_id instead of the physical
device id fixes multi-gang dimmers, but light_id (hub Lighting section) and id
(hub Devices section) are different id spaces even for single-gang lights. So
the unique_ids of every light-derived entity change on update -- the light, its
mode/LED/power-on selects, its away-mode switch and its four capability
binary_sensors -- orphaning the existing entities and losing their history and
dashboard references.

Add build_light_unique_id_migration(), which rebuilds each pre-fix unique_id
from live hub data and maps it to the new one, and run it from
async_setup_entry (before the platforms create entities) via
async_migrate_light_unique_ids(). The callback guards against a pre-existing
target unique_id, since async_update_entity raises on collision and would
otherwise abort config entry setup.

Multi-gang dimmer channels are skipped: before the fix their light and select
platforms crashed and their other entities collided on unique_id, so their
pre-fix registry state is ambiguous; they are left to orphan.

Adds tests/test_light_unique_id_migration.py covering the single-gang mapping,
the multi-gang skip and the no-op filter.
@wolfgang-steinberg

Copy link
Copy Markdown
Author

Added the migration shim in 241f547 (plus 71cc6f7, a test stub fix — switch.py started importing ENTITY_PREFIX in this PR, which broke test_switch_hot_water.py's stub).

Scope — it's a bit wider than my earlier comment. I traced the library: _WiserLight.id comes from the Devices section, light_id from the Lighting section — they're different id spaces even for single-gang lights. So on a single-gang setup, upgrading changes the unique_id of every light-derived entity, not just the ones I first spotted:

  • light (name-based → light_id)
  • mode / LED-indicator / power-on selects (device idlight_id)
  • away-mode switch (name + id)
  • the four capability binary_sensors (name-based)

The shim (build_light_unique_id_migration in helpers.py, run from async_setup_entry via async_migrate_light_unique_ids) rebuilds each old unique_id from live hub data and renames the matching registry entry to the new scheme, so entity_ids, history and dashboard references carry over. It runs before the platforms create entities, so there's no transient-duplicate window, and the callback checks async_get_entity_id before returning a rename — async_update_entity raises if the target unique_id already exists, and this keeps a partially-migrated or re-run setup from aborting config-entry setup.

One deliberate limitation: multi-gang dimmer channels created before the fix are left to orphan. On a hub with a 2-gang dimmer, lights.get_by_id() returned a list, so the light and select platforms crashed outright (nothing to migrate), while binary_sensor/switch resolved via the generic devices.get_by_id() (first match) and so kept only one entity for the two channels — there's no unambiguous new target to map that to. Single-gang-only setups migrate completely; the ambiguous 2-gang artifacts are the handful a user deletes once. The device-level entities I de-duplicated (Device Lock, Identify, Signal) keep their unique_ids, so they don't migrate.

Testing: added tests/test_light_unique_id_migration.py (single-gang mapping, multi-gang skip, no-op filter; full suite green). I also cross-checked on my live hub (12 light channels, 5 of them 2-gang dimmers): every new unique_id the shim computes matches the registry exactly.

@asantaga
asantaga merged commit 288effc into asantaga:dev Aug 11, 2026
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.

2 participants