Initial public release

This commit is contained in:
ajp_anton
2026-05-30 15:44:40 +00:00
commit cc4e0c2c0f
56 changed files with 11840 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
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}.")