Split devices

Some physical devices expose multiple controllable endpoints (multi-zone lights, security sensors, portable AC power toggles). aioafero splits them into separate logical devices so integrations can expose one entity per zone.

Not every device with more than one brightness control is split. RGB+WW fixtures such as flushmounts and RGBCW strips expose separate color and white dimming channels on one fixture; those stay a single light (see Dual-channel RGB+WW lights).

How splitting works

  1. During discovery, a controller’s DEVICE_SPLIT_CALLBACKS transforms one AferoDevice into several clones with synthetic IDs.

  2. Each clone stores a SplitDeviceId on split (for example identifier light and instance main). str(split) is the synthetic id {parent_id}-{identifier}-{instance}. Polls and writes use split.parent_id so names such as light-sensor-enabled do not collide with the split token.

  3. split on the model enables StandardMixin properties:

    • id — the synthetic ID (entity identity), same as str(split)

    • update_id — split.parent_id used for API writes

    • instance — split.instance

    • split_identifier — split.identifier (integrations that still key off the token)

Dual-channel RGB+WW lights

Some lights expose separate RGB and warm-white LED drivers on one metadevice. The API uses distinct brightness functionInstance values — typically color, white, and primary — while color controls (color-rgb, color-mode, color-temperature) often share a null instance across the whole fixture.

These fixtures are not split into {parent}-light-color / {parent}-light-white light clones. Splitting would drop the shared null-instance color states and break RGB/CCT control. Channel independence is exposed on the combined Light as:

  • Light.channels — per-channel brightness and on/off (color/white toggles are not cloned into Switch resources)

  • LightController.set_state(..., channel=) — toggle or dim a single channel

Detection: both color and white brightness zones (from states, functions, or capabilities). True multi-zone lights such as main/trim still split normally.

Integrations may expose one light entity or two (color + white) using the helpers in Lights.

Implementing a new split type

On the primary controller (non-split class):

  • Register a callback in DEVICE_SPLIT_CALLBACKS that returns CallbackResponse with split_devices=[...] and remove_original=....

On the split model:

  • Use a unique synthetic _id, not the parent metadevice ID.

  • Set split to a SplitDeviceId (AferoDevice.apply_split) and use str(split) as the synthetic _id.

  • Inherit StandardMixin when the model needs update_id / instance.

Existing split patterns

  • Lights — true multi-zone light instances (for example main / trim); parent may become a Wi-Fi metadata device. Dual-channel RGB+WW fixtures are excluded (see Dual-channel RGB+WW lights).

  • Portable ACs — power toggle → bridge.switches.

  • Exhaust fans — toggles, fan, and light instances on one unit.

  • Security systems — sensors extracted to bridge.security_systems_sensors.

Pollers deduplicate by parent metadevice ID so split entities do not multiply API traffic. State polls merge incoming rows into the cached AferoDevice by functionClass / functionInstance so partial responses do not drop other zones. See Bridge configuration and lifecycle.