from collections.abc import Mapping from typing import Any from ha_deconz_bridge.color import Color from ha_deconz_bridge.lights import ChannelSpec, ControllerModeSpec, ControllerSpec, VirtualLightSpec RGB_RED = (0.68, 0.31) RGB_GREEN = (0.18, 0.70) RGB_BLUE = (0.14, 0.05) CT_RANGE = (153, 500) DAYLIGHT_MIREDS = 182 def define_lights(raw_lights: Mapping[str, Mapping[str, Any]]) -> dict[str, VirtualLightSpec]: return { light_name: _virtual_light(light_name, raw_spec) for light_name, raw_spec in raw_lights.items() } def _virtual_light(light_name: str, raw_spec: Mapping[str, Any]) -> VirtualLightSpec: controllers = { controller_id: _controller_spec( controller_id, raw_controller, default_name=light_name, ) for controller_id, raw_controller in raw_spec["controllers"].items() } return VirtualLightSpec( name=str(raw_spec.get("name", light_name)), supported_color_modes=tuple(raw_spec["supported_color_modes"]), controllers=controllers, ) def _controller_spec( controller_id: str, raw_controller: Mapping[str, Any] | ControllerSpec, *, default_name: str, ) -> ControllerSpec: if isinstance(raw_controller, ControllerSpec): if raw_controller.zigbee_id != controller_id: raise ValueError( f"Controller key {controller_id!r} does not match spec zigbee_id {raw_controller.zigbee_id!r}." ) return raw_controller mireds = raw_controller.get("mireds") if mireds is not None: mireds = tuple(sorted(int(value) for value in mireds)) raw_modes = raw_controller.get("modes") if raw_modes is None: raw_modes = { key: value for key, value in raw_controller.items() if key in {"onoff", "brightness", "rgb", "color_temp"} } modes = { mode_name: _mode_spec(mode_name, raw_mode, mireds=mireds) for mode_name, raw_mode in raw_modes.items() } return ControllerSpec( zigbee_id=controller_id, name=str(raw_controller.get("name", default_name)), modes=modes, mireds=mireds, ) def _mode_spec( mode_name: str, raw_mode: Mapping[str, Any], *, mireds: tuple[int, int] | None, ) -> ControllerModeSpec: channels = raw_mode.get("channels", raw_mode) return ControllerModeSpec( name=mode_name, channels={ role: _channel_spec(role, raw_channel, mode_name=mode_name, mireds=mireds) for role, raw_channel in channels.items() if role not in {"name", "channels"} }, ) def _channel_spec( role: str, raw_channel: Mapping[str, Any], *, mode_name: str, mireds: tuple[int, int] | None, ) -> ChannelSpec: label = raw_channel.get("label", raw_channel.get("semantic_label")) return ChannelSpec( role, _parse_color( raw_channel["color"], brightness=raw_channel.get("brightness"), temp_range=mireds or CT_RANGE, ), label, ) def _parse_color( value: Color | int | float | tuple[float, ...] | list[float], *, brightness: float | int | None, temp_range: tuple[int, int], ) -> Color: output = 1.0 if brightness is None else float(brightness) if isinstance(value, Color): return value.with_brightness(output) if brightness is not None else value if isinstance(value, int | float): mireds = float(value) if mireds >= 1000.0: mireds = 1_000_000.0 / mireds return Color.from_color_temp(mireds, brightness=output, temp_range=temp_range) components = tuple(float(component) for component in value) if len(components) == 2: return Color.from_xy(components, brightness=output) if len(components) == 3: scale = 255.0 if any(component > 1.0 for component in components) else 1.0 return Color.from_rgb(tuple(component / scale for component in components), brightness=output) raise ValueError(f"Unsupported color value {value!r}.")