Initial public release
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial import ConvexHull, QhullError
|
||||
|
||||
from ha_deconz_bridge.color import Color
|
||||
from ha_deconz_bridge.lights import ControllerModeName, VirtualLightSpec
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class CompiledChannel:
|
||||
address: str
|
||||
controller_id: str
|
||||
mode_name: ControllerModeName
|
||||
role: str
|
||||
color: Color
|
||||
semantic_label: str | None
|
||||
order_index: int
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class CompiledOption:
|
||||
key: str
|
||||
controller_modes: dict[str, ControllerModeName | None]
|
||||
dimmable_channels: tuple[CompiledChannel, ...]
|
||||
onoff_channels: tuple[CompiledChannel, ...]
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class CompiledCombination:
|
||||
key: str
|
||||
controller_modes: dict[str, ControllerModeName | None]
|
||||
dimmable_channels: tuple[CompiledChannel, ...]
|
||||
onoff_channels: tuple[CompiledChannel, ...]
|
||||
matrix: np.ndarray
|
||||
base_xyz: np.ndarray
|
||||
xy_bounds: tuple[float, float, float, float]
|
||||
xy_hull_equations: np.ndarray
|
||||
output_channel_count: int
|
||||
total_available_lumens: float
|
||||
order_key: tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CompiledLightModel:
|
||||
spec: VirtualLightSpec
|
||||
combinations: tuple[CompiledCombination, ...]
|
||||
reference_combinations: tuple[CompiledCombination, ...]
|
||||
chroma_tolerance: float
|
||||
target_cache_scale: int
|
||||
approximate_chroma_band: float = 0.025
|
||||
reference_cache: dict[tuple[str, int, int], tuple[object, ...]] = field(default_factory=dict)
|
||||
max_brightness_cache: dict[tuple[str, int, int], float] = field(default_factory=dict)
|
||||
|
||||
|
||||
def compile_light_model(
|
||||
spec: VirtualLightSpec,
|
||||
*,
|
||||
chroma_tolerance: float = 1e-6,
|
||||
target_cache_scale: int = 10000,
|
||||
approximate_chroma_band: float = 0.025,
|
||||
) -> CompiledLightModel:
|
||||
choice_groups: list[tuple[CompiledOption, ...]] = []
|
||||
order_index = 0
|
||||
for controller in spec.controllers.values():
|
||||
options: list[CompiledOption] = []
|
||||
if controller.supported_modes == {"onoff"}:
|
||||
options.append(
|
||||
CompiledOption(
|
||||
key=f"{controller.zigbee_id}:off",
|
||||
controller_modes={controller.zigbee_id: None},
|
||||
dimmable_channels=(),
|
||||
onoff_channels=(),
|
||||
)
|
||||
)
|
||||
for mode in controller.modes.values():
|
||||
dimmable_channels: list[CompiledChannel] = []
|
||||
onoff_channels: list[CompiledChannel] = []
|
||||
target_list = onoff_channels if mode.name == "onoff" else dimmable_channels
|
||||
for role, channel in mode.channels.items():
|
||||
target_list.append(
|
||||
CompiledChannel(
|
||||
address=f"{controller.zigbee_id}.{mode.name}.{role}",
|
||||
controller_id=controller.zigbee_id,
|
||||
mode_name=mode.name,
|
||||
role=role,
|
||||
color=channel.color,
|
||||
semantic_label=channel.effective_label(mode.name),
|
||||
order_index=order_index,
|
||||
)
|
||||
)
|
||||
order_index += 1
|
||||
options.append(
|
||||
CompiledOption(
|
||||
key=f"{controller.zigbee_id}:{mode.name}",
|
||||
controller_modes={controller.zigbee_id: mode.name},
|
||||
dimmable_channels=tuple(dimmable_channels),
|
||||
onoff_channels=tuple(onoff_channels),
|
||||
)
|
||||
)
|
||||
choice_groups.append(tuple(options))
|
||||
|
||||
combinations: list[CompiledCombination] = []
|
||||
for product in itertools.product(*choice_groups):
|
||||
controller_modes: dict[str, ControllerModeName | None] = {}
|
||||
dimmable_channels: list[CompiledChannel] = []
|
||||
onoff_channels: list[CompiledChannel] = []
|
||||
order_key: list[str] = []
|
||||
for option in product:
|
||||
controller_modes.update(option.controller_modes)
|
||||
dimmable_channels.extend(option.dimmable_channels)
|
||||
onoff_channels.extend(option.onoff_channels)
|
||||
order_key.append(option.key)
|
||||
|
||||
dimmable_channels.sort(key=lambda channel: channel.order_index)
|
||||
onoff_channels.sort(key=lambda channel: channel.order_index)
|
||||
|
||||
if dimmable_channels:
|
||||
matrix = np.column_stack([channel.color.xyz for channel in dimmable_channels])
|
||||
else:
|
||||
matrix = np.zeros((3, 0), dtype=float)
|
||||
if onoff_channels:
|
||||
base_xyz = Color.weighted_sum(
|
||||
[channel.color for channel in onoff_channels],
|
||||
[1.0] * len(onoff_channels),
|
||||
).xyz
|
||||
else:
|
||||
base_xyz = np.zeros(3, dtype=float)
|
||||
|
||||
all_channels = dimmable_channels + onoff_channels
|
||||
if all_channels:
|
||||
xs = [channel.color.xy[0] for channel in all_channels]
|
||||
ys = [channel.color.xy[1] for channel in all_channels]
|
||||
xy_bounds = (min(xs), max(xs), min(ys), max(ys))
|
||||
else:
|
||||
xy_bounds = (0.0, 0.0, 0.0, 0.0)
|
||||
xy_hull_equations = _xy_hull_equations(all_channels, base_xyz)
|
||||
combinations.append(
|
||||
CompiledCombination(
|
||||
key="|".join(order_key),
|
||||
controller_modes=controller_modes,
|
||||
dimmable_channels=tuple(dimmable_channels),
|
||||
onoff_channels=tuple(onoff_channels),
|
||||
matrix=matrix,
|
||||
base_xyz=base_xyz,
|
||||
xy_bounds=xy_bounds,
|
||||
xy_hull_equations=xy_hull_equations,
|
||||
output_channel_count=len(all_channels),
|
||||
total_available_lumens=sum(channel.color.brightness for channel in all_channels),
|
||||
order_key=tuple(order_key),
|
||||
)
|
||||
)
|
||||
|
||||
return CompiledLightModel(
|
||||
spec=spec,
|
||||
combinations=tuple(combinations),
|
||||
reference_combinations=_deduplicate_reference_combinations(combinations),
|
||||
chroma_tolerance=chroma_tolerance,
|
||||
target_cache_scale=max(1, int(target_cache_scale)),
|
||||
approximate_chroma_band=approximate_chroma_band,
|
||||
)
|
||||
|
||||
|
||||
def _deduplicate_reference_combinations(
|
||||
combinations: list[CompiledCombination],
|
||||
) -> tuple[CompiledCombination, ...]:
|
||||
seen: set[tuple[object, ...]] = set()
|
||||
unique: list[CompiledCombination] = []
|
||||
for combination in combinations:
|
||||
key = _reference_signature(combination)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
unique.append(combination)
|
||||
return tuple(unique)
|
||||
|
||||
|
||||
def _reference_signature(combination: CompiledCombination) -> tuple[object, ...]:
|
||||
dimmable = tuple(
|
||||
sorted(
|
||||
(
|
||||
tuple(round(float(value), 8) for value in channel.color.xyz),
|
||||
channel.semantic_label,
|
||||
channel.mode_name,
|
||||
channel.role,
|
||||
)
|
||||
for channel in combination.dimmable_channels
|
||||
)
|
||||
)
|
||||
onoff = tuple(
|
||||
sorted(
|
||||
(
|
||||
tuple(round(float(value), 8) for value in channel.color.xyz),
|
||||
channel.semantic_label,
|
||||
channel.mode_name,
|
||||
channel.role,
|
||||
)
|
||||
for channel in combination.onoff_channels
|
||||
)
|
||||
)
|
||||
return (dimmable, onoff)
|
||||
|
||||
|
||||
def _xy_hull_equations(
|
||||
channels: list[CompiledChannel],
|
||||
base_xyz: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
points = [channel.color.xy for channel in channels]
|
||||
if float(base_xyz[1]) > 0.0:
|
||||
base_sum = float(np.sum(base_xyz))
|
||||
if base_sum > 0.0:
|
||||
points.append((float(base_xyz[0] / base_sum), float(base_xyz[1] / base_sum)))
|
||||
unique = sorted({(round(x, 10), round(y, 10)) for x, y in points})
|
||||
if len(unique) < 3:
|
||||
return np.zeros((0, 3), dtype=float)
|
||||
try:
|
||||
hull = ConvexHull(np.asarray(unique, dtype=float))
|
||||
except QhullError:
|
||||
return np.zeros((0, 3), dtype=float)
|
||||
return np.asarray(hull.equations, dtype=float)
|
||||
Reference in New Issue
Block a user