689 lines
28 KiB
Python
689 lines
28 KiB
Python
from __future__ import annotations
|
|
|
|
from configs.example import GROUPS, LIGHTS
|
|
from ha_deconz_bridge.color import Color
|
|
from ha_deconz_bridge.debugger.web_debugger import _combine_specs, _group_debug_spec
|
|
from ha_deconz_bridge.lights import ChannelSpec, ControllerModeSpec, ControllerSpec, VirtualLightSpec
|
|
from ha_deconz_bridge.solver import solve_light
|
|
from ha_deconz_bridge.solver_model import compile_light_model
|
|
from ha_deconz_bridge.translate import ct_from_mix
|
|
|
|
|
|
def test_solver_reaches_exact_rgb_target(rgb_light_spec) -> None:
|
|
model = compile_light_model(rgb_light_spec)
|
|
target = Color.from_rgb((255, 0, 0), brightness=1.0)
|
|
result = solve_light(model, target, brightness=255, requested_mode="rgb")
|
|
assert result.exact_chroma_match is True
|
|
assert result.achieved_color.rgb[0] > 0.95
|
|
assert result.achieved_color.rgb[1] < 0.2
|
|
assert result.controller_modes["1"] == "rgb"
|
|
|
|
|
|
def test_solver_reaches_exact_ct_target(ct_light_spec) -> None:
|
|
model = compile_light_model(ct_light_spec)
|
|
target = Color.from_color_temp(400, brightness=1.0, temp_range=ct_light_spec.exposed_mireds)
|
|
result = solve_light(model, target, brightness=255, requested_mode="color_temp")
|
|
assert result.exact_chroma_match is False
|
|
assert result.achieved_color.brightness > 1.0
|
|
assert result.chroma_error < 0.02
|
|
|
|
|
|
def test_solver_prefers_native_ct_mode(mixed_light_spec) -> None:
|
|
model = compile_light_model(mixed_light_spec)
|
|
target = Color.from_color_temp(450, brightness=1.0, temp_range=mixed_light_spec.exposed_mireds)
|
|
result = solve_light(model, target, brightness=200, requested_mode="color_temp")
|
|
assert result.controller_modes["2"] == "color_temp"
|
|
assert result.chroma_error < 0.02
|
|
|
|
|
|
def test_solver_clamps_out_of_gamut_without_drift(red_only_spec) -> None:
|
|
model = compile_light_model(red_only_spec)
|
|
request = Color.from_rgb((0, 255, 0), brightness=1.0)
|
|
first = solve_light(model, request, brightness=255, requested_mode="rgb")
|
|
assert first.exact_chroma_match is False
|
|
second = solve_light(
|
|
model,
|
|
Color.from_rgb(first.achieved_color.rgb, brightness=1.0),
|
|
brightness=255,
|
|
requested_mode="rgb",
|
|
)
|
|
assert second.achieved_color.uv_distance(first.achieved_color) < 1e-6
|
|
|
|
|
|
def test_low_brightness_extreme_ct_uses_extreme_channel(ct_light_spec) -> None:
|
|
model = compile_light_model(ct_light_spec)
|
|
target = Color.from_color_temp(500, brightness=1.0, temp_range=ct_light_spec.exposed_mireds)
|
|
result = solve_light(model, target, brightness=16, requested_mode="color_temp")
|
|
assert result.controller_commands["2"].color_temp >= 480
|
|
assert result.controller_commands["2"].brightness <= 16
|
|
|
|
|
|
def test_rgb_request_uses_only_rgb_labeled_channels_when_available() -> None:
|
|
spec = VirtualLightSpec(
|
|
name="mixed_priority",
|
|
supported_color_modes=("rgb", "color_temp"),
|
|
controllers={
|
|
"rgb": ControllerSpec(
|
|
zigbee_id="rgb",
|
|
name="RGB",
|
|
modes={
|
|
"rgb": ControllerModeSpec(
|
|
name="rgb",
|
|
channels={
|
|
"r": ChannelSpec("r", Color.from_rgb((1.0, 0.0, 0.0), brightness=0.5), "rgb"),
|
|
"g": ChannelSpec("g", Color.from_rgb((0.0, 1.0, 0.0), brightness=1.0), "rgb"),
|
|
"b": ChannelSpec("b", Color.from_rgb((0.0, 0.0, 1.0), brightness=0.5), "rgb"),
|
|
},
|
|
)
|
|
},
|
|
),
|
|
"ct": ControllerSpec(
|
|
zigbee_id="ct",
|
|
name="CT",
|
|
modes={
|
|
"color_temp": ControllerModeSpec(
|
|
name="color_temp",
|
|
channels={
|
|
"ww": ChannelSpec(
|
|
"ww",
|
|
Color.from_color_temp(450, brightness=0.8, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
"cw": ChannelSpec(
|
|
"cw",
|
|
Color.from_color_temp(200, brightness=0.8, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
},
|
|
)
|
|
},
|
|
),
|
|
},
|
|
)
|
|
model = compile_light_model(spec)
|
|
target = Color.from_rgb((128, 255, 128), brightness=1.0)
|
|
|
|
low = solve_light(model, target, brightness=96, requested_mode="rgb")
|
|
assert low.controller_commands["ct"].on is False
|
|
|
|
high = solve_light(model, target, brightness=255, requested_mode="rgb")
|
|
assert high.controller_commands["ct"].on is False
|
|
assert all(".rgb." in channel for channel in high.channel_levels)
|
|
|
|
|
|
def test_rgb_request_can_use_non_rgb_labels_when_no_rgb_labels_exist() -> None:
|
|
spec = VirtualLightSpec(
|
|
name="miswired_rgb",
|
|
supported_color_modes=("rgb",),
|
|
controllers={
|
|
"rgb": ControllerSpec(
|
|
zigbee_id="rgb",
|
|
name="Miswired RGB",
|
|
modes={
|
|
"rgb": ControllerModeSpec(
|
|
name="rgb",
|
|
channels={
|
|
"r": ChannelSpec(
|
|
"r",
|
|
Color.from_color_temp(500, brightness=1.0, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
"g": ChannelSpec(
|
|
"g",
|
|
Color.from_color_temp(300, brightness=1.0, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
"b": ChannelSpec(
|
|
"b",
|
|
Color.from_color_temp(153, brightness=1.0, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
},
|
|
)
|
|
},
|
|
),
|
|
},
|
|
)
|
|
model = compile_light_model(spec)
|
|
|
|
result = solve_light(model, Color.from_rgb((255, 255, 255), brightness=1.0), brightness=255, requested_mode="rgb")
|
|
|
|
assert result.controller_commands["rgb"].on is True
|
|
assert result.channel_levels
|
|
|
|
|
|
def test_onoff_booster_is_held_back_until_dimmables_are_exhausted() -> None:
|
|
spec = VirtualLightSpec(
|
|
name="boosters",
|
|
supported_color_modes=("brightness",),
|
|
controllers={
|
|
"dim": ControllerSpec(
|
|
zigbee_id="dim",
|
|
name="Dim",
|
|
modes={
|
|
"brightness": ControllerModeSpec(
|
|
name="brightness",
|
|
channels={
|
|
"w": ChannelSpec(
|
|
"w",
|
|
Color.from_color_temp(350, brightness=1.0, temp_range=(153, 500)),
|
|
"color_temp",
|
|
)
|
|
},
|
|
)
|
|
},
|
|
),
|
|
"plug": ControllerSpec(
|
|
zigbee_id="plug",
|
|
name="Plug",
|
|
modes={
|
|
"onoff": ControllerModeSpec(
|
|
name="onoff",
|
|
channels={
|
|
"w": ChannelSpec(
|
|
"w",
|
|
Color.from_color_temp(350, brightness=0.7, temp_range=(153, 500)),
|
|
"color_temp",
|
|
)
|
|
},
|
|
)
|
|
},
|
|
),
|
|
},
|
|
)
|
|
model = compile_light_model(spec)
|
|
target = Color.from_color_temp(350, brightness=1.0, temp_range=(153, 500))
|
|
|
|
low = solve_light(model, target, brightness=96, requested_mode="brightness")
|
|
assert low.controller_commands["plug"].on is False
|
|
|
|
high = solve_light(model, target, brightness=255, requested_mode="brightness")
|
|
assert high.controller_commands["plug"].on is True
|
|
|
|
|
|
def test_brightness_mode_compensates_when_onoff_booster_turns_on() -> None:
|
|
spec = _combine_specs(LIGHTS, ["Example Plug A", "Example Fixed White A"])
|
|
model = compile_light_model(spec)
|
|
target = Color.from_rgb((255, 255, 255), brightness=1.0)
|
|
|
|
before_switch = solve_light(model, target, brightness=120, requested_mode="brightness")
|
|
assert before_switch.controller_commands["plug_a"].on is False
|
|
assert before_switch.controller_commands["fixed_a"].on is True
|
|
|
|
after_switch = solve_light(model, target, brightness=200, requested_mode="brightness")
|
|
assert after_switch.controller_commands["plug_a"].on is True
|
|
assert after_switch.channel_levels["plug_a.onoff.w"] == 1.0
|
|
assert after_switch.channel_levels["fixed_a.color_temp.ww"] < 1.0
|
|
|
|
above_booster = solve_light(model, target, brightness=255, requested_mode="brightness")
|
|
assert above_booster.controller_commands["plug_a"].on is True
|
|
assert above_booster.controller_commands["fixed_a"].on is True
|
|
assert above_booster.channel_levels["fixed_a.color_temp.ww"] == 1.0
|
|
|
|
|
|
def test_ct_request_keeps_native_ct_saturated_while_rgb_boosting() -> None:
|
|
spec = VirtualLightSpec(
|
|
name="ct_priority",
|
|
supported_color_modes=("rgb", "color_temp"),
|
|
controllers={
|
|
"rgb": ControllerSpec(
|
|
zigbee_id="rgb",
|
|
name="RGB",
|
|
modes={
|
|
"rgb": ControllerModeSpec(
|
|
name="rgb",
|
|
channels={
|
|
"r": ChannelSpec("r", Color.from_xy((0.68, 0.31), brightness=180.0), "rgb"),
|
|
"g": ChannelSpec("g", Color.from_xy((0.18, 0.70), brightness=360.0), "rgb"),
|
|
"b": ChannelSpec("b", Color.from_xy((0.14, 0.05), brightness=110.0), "rgb"),
|
|
},
|
|
)
|
|
},
|
|
),
|
|
"ct": ControllerSpec(
|
|
zigbee_id="ct",
|
|
name="CT",
|
|
modes={
|
|
"color_temp": ControllerModeSpec(
|
|
name="color_temp",
|
|
channels={
|
|
"ww": ChannelSpec(
|
|
"ww",
|
|
Color.from_color_temp(500, brightness=340.0, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
"cw": ChannelSpec(
|
|
"cw",
|
|
Color.from_color_temp(153, brightness=360.0, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
},
|
|
)
|
|
},
|
|
),
|
|
},
|
|
)
|
|
model = compile_light_model(spec)
|
|
target = Color.from_color_temp(330, brightness=1.0, temp_range=(153, 500))
|
|
|
|
low = solve_light(model, target, brightness=160, requested_mode="color_temp")
|
|
threshold = solve_light(model, target, brightness=161, requested_mode="color_temp")
|
|
assert low.controller_commands["rgb"].on is True
|
|
assert threshold.controller_commands["rgb"].on is True
|
|
assert low.controller_commands["ct"].brightness == 255
|
|
assert threshold.controller_commands["ct"].brightness == 255
|
|
assert abs(low.controller_commands["ct"].color_temp - threshold.controller_commands["ct"].color_temp) <= 1
|
|
assert abs(low.channel_levels["ct.color_temp.ww"] - threshold.channel_levels["ct.color_temp.ww"]) < 1e-6
|
|
assert threshold.channel_levels["ct.color_temp.ww"] > 0.99
|
|
assert threshold.channel_levels["ct.color_temp.cw"] > low.channel_levels["ct.color_temp.cw"]
|
|
assert low.controller_commands["rgb"].brightness is not None
|
|
assert threshold.controller_commands["rgb"].brightness is not None
|
|
assert threshold.controller_commands["rgb"].brightness > low.controller_commands["rgb"].brightness
|
|
assert threshold.controller_commands["rgb"].brightness < 100
|
|
|
|
|
|
def test_ct_request_does_not_trade_down_warm_extreme_for_tiny_rgb_gain() -> None:
|
|
spec = VirtualLightSpec(
|
|
name="ct_warm_priority",
|
|
supported_color_modes=("rgb", "color_temp"),
|
|
controllers={
|
|
"rgb": ControllerSpec(
|
|
zigbee_id="rgb",
|
|
name="RGB",
|
|
modes={
|
|
"rgb": ControllerModeSpec(
|
|
name="rgb",
|
|
channels={
|
|
"r": ChannelSpec("r", Color.from_xy((0.68, 0.31), brightness=180.0), "rgb"),
|
|
"g": ChannelSpec("g", Color.from_xy((0.18, 0.70), brightness=360.0), "rgb"),
|
|
"b": ChannelSpec("b", Color.from_xy((0.14, 0.05), brightness=110.0), "rgb"),
|
|
},
|
|
)
|
|
},
|
|
),
|
|
"ct": ControllerSpec(
|
|
zigbee_id="ct",
|
|
name="CT",
|
|
modes={
|
|
"color_temp": ControllerModeSpec(
|
|
name="color_temp",
|
|
channels={
|
|
"ww": ChannelSpec(
|
|
"ww",
|
|
Color.from_color_temp(500, brightness=340.0, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
"cw": ChannelSpec(
|
|
"cw",
|
|
Color.from_color_temp(153, brightness=360.0, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
},
|
|
)
|
|
},
|
|
),
|
|
},
|
|
)
|
|
model = compile_light_model(spec)
|
|
target = Color.from_color_temp(505, brightness=1.0, temp_range=(153, 500))
|
|
|
|
low = solve_light(model, target, brightness=126, requested_mode="color_temp")
|
|
assert low.controller_commands["rgb"].on is True
|
|
assert low.channel_levels["ct.color_temp.ww"] > 0.9
|
|
|
|
near_threshold = solve_light(model, target, brightness=127, requested_mode="color_temp")
|
|
assert near_threshold.controller_commands["rgb"].on is True
|
|
assert near_threshold.channel_levels["ct.color_temp.ww"] >= low.channel_levels["ct.color_temp.ww"]
|
|
assert near_threshold.controller_commands["rgb"].brightness is not None
|
|
assert low.controller_commands["rgb"].brightness is not None
|
|
assert near_threshold.controller_commands["rgb"].brightness >= low.controller_commands["rgb"].brightness
|
|
assert near_threshold.controller_commands["ct"].brightness is not None
|
|
assert near_threshold.controller_commands["ct"].brightness >= 230
|
|
|
|
|
|
def test_ct_line_balancing_uses_comparable_same_label_channels_stably() -> None:
|
|
spec = VirtualLightSpec(
|
|
name="ct_stability",
|
|
supported_color_modes=("color_temp",),
|
|
controllers={
|
|
"near": ControllerSpec(
|
|
zigbee_id="near",
|
|
name="Near CW",
|
|
modes={
|
|
"color_temp": ControllerModeSpec(
|
|
name="color_temp",
|
|
channels={
|
|
"cw": ChannelSpec(
|
|
"cw",
|
|
Color.from_color_temp(190, brightness=0.8, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
},
|
|
)
|
|
},
|
|
),
|
|
"far": ControllerSpec(
|
|
zigbee_id="far",
|
|
name="Far CW",
|
|
modes={
|
|
"color_temp": ControllerModeSpec(
|
|
name="color_temp",
|
|
channels={
|
|
"cw": ChannelSpec(
|
|
"cw",
|
|
Color.from_color_temp(153, brightness=1.0, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
},
|
|
)
|
|
},
|
|
),
|
|
"warm": ControllerSpec(
|
|
zigbee_id="warm",
|
|
name="Warm",
|
|
modes={
|
|
"color_temp": ControllerModeSpec(
|
|
name="color_temp",
|
|
channels={
|
|
"ww": ChannelSpec(
|
|
"ww",
|
|
Color.from_color_temp(500, brightness=1.0, temp_range=(153, 500)),
|
|
"color_temp",
|
|
),
|
|
},
|
|
)
|
|
},
|
|
),
|
|
},
|
|
)
|
|
model = compile_light_model(spec)
|
|
target = Color.from_color_temp(330, brightness=1.0, temp_range=(153, 500))
|
|
|
|
for brightness in range(96, 181, 7):
|
|
result = solve_light(model, target, brightness=brightness, requested_mode="color_temp")
|
|
assert result.channel_levels.get("near.color_temp.cw", 0.0) > 0.0
|
|
assert result.channel_levels.get("far.color_temp.cw", 0.0) > 0.0
|
|
assert abs(result.achieved_color.color_temp - 330) <= 2
|
|
|
|
|
|
def test_rgb_request_does_not_use_white_boost_when_rgb_labeled_channels_exist() -> None:
|
|
spec = _combine_specs(LIGHTS, ["Example Fixed White A", "Example Mixed North"])
|
|
model = compile_light_model(spec)
|
|
target = Color.from_rgb((255, 255, 255), brightness=1.0)
|
|
|
|
result = solve_light(model, target, brightness=103, requested_mode="rgb")
|
|
assert all(".rgb." in channel for channel in result.channel_levels)
|
|
assert result.controller_commands["fixed_a"].on is False
|
|
assert result.controller_commands["mixed_ct"].on is False
|
|
|
|
|
|
def test_example_fixed_white_controller_uses_full_ct_command_range() -> None:
|
|
controller = LIGHTS["Example Fixed White A"].controllers["fixed_a"]
|
|
assert controller.native_ct_mireds == (153, 500)
|
|
|
|
|
|
def test_example_standard_ct_controllers_use_configured_ct_command_range() -> None:
|
|
for light_name in ["Example Mixed North", "Example Hall Floor", "Example Hall Corner", "Example Hall Cabinets"]:
|
|
for controller in LIGHTS[light_name].controllers.values():
|
|
if "color_temp" in controller.modes:
|
|
assert controller.native_ct_mireds == (153, 500)
|
|
|
|
|
|
def test_ct_request_uses_native_ct_mode_on_rgb_cct_controller() -> None:
|
|
spec = LIGHTS["Example Kitchen Counter"]
|
|
result = solve_light(
|
|
compile_light_model(spec),
|
|
Color.from_color_temp(455, brightness=1.0),
|
|
brightness=51,
|
|
requested_mode="color_temp",
|
|
)
|
|
|
|
assert result.controller_modes == {"kitchen_counter": "color_temp"}
|
|
assert result.controller_commands["kitchen_counter"].mode == "color_temp"
|
|
peak = max(result.channel_levels["kitchen_counter.color_temp.cw"], result.channel_levels["kitchen_counter.color_temp.ww"])
|
|
assert result.controller_commands["kitchen_counter"].color_temp == ct_from_mix(
|
|
result.channel_levels["kitchen_counter.color_temp.cw"] / peak,
|
|
result.channel_levels["kitchen_counter.color_temp.ww"] / peak,
|
|
spec.controllers["kitchen_counter"].native_ct_mireds,
|
|
)
|
|
|
|
|
|
def test_ct_slack_prefers_near_ct_over_exact_rgb_when_within_band() -> None:
|
|
target = Color.from_color_temp(330, brightness=1.0, temp_range=(153, 500))
|
|
spec = VirtualLightSpec(
|
|
name="ct_slack",
|
|
supported_color_modes=("rgb", "color_temp"),
|
|
controllers={
|
|
"shared": ControllerSpec(
|
|
zigbee_id="shared",
|
|
name="Shared",
|
|
mireds=(153, 500),
|
|
modes={
|
|
"rgb": ControllerModeSpec(
|
|
name="rgb",
|
|
channels={"r": ChannelSpec("r", target, "rgb")},
|
|
),
|
|
"color_temp": ControllerModeSpec(
|
|
name="color_temp",
|
|
channels={
|
|
"ww": ChannelSpec(
|
|
"ww",
|
|
Color.from_xy((target.xy[0] + 0.002, target.xy[1]), brightness=5.0),
|
|
"color_temp",
|
|
),
|
|
"cw": ChannelSpec(
|
|
"cw",
|
|
Color.from_xy((target.xy[0] - 0.002, target.xy[1] + 0.001), brightness=5.0),
|
|
"color_temp",
|
|
),
|
|
},
|
|
),
|
|
},
|
|
)
|
|
},
|
|
)
|
|
|
|
result = solve_light(
|
|
compile_light_model(spec, approximate_chroma_band=0.01),
|
|
target,
|
|
brightness=255,
|
|
requested_mode="color_temp",
|
|
)
|
|
|
|
assert result.controller_modes == {"shared": "color_temp"}
|
|
assert result.exact_chroma_match is False
|
|
assert result.chroma_error <= 0.01
|
|
|
|
|
|
def test_ct_slack_does_not_use_near_ct_when_outside_band() -> None:
|
|
target = Color.from_color_temp(330, brightness=1.0, temp_range=(153, 500))
|
|
spec = VirtualLightSpec(
|
|
name="ct_slack_strict",
|
|
supported_color_modes=("rgb", "color_temp"),
|
|
controllers={
|
|
"shared": ControllerSpec(
|
|
zigbee_id="shared",
|
|
name="Shared",
|
|
mireds=(153, 500),
|
|
modes={
|
|
"rgb": ControllerModeSpec(
|
|
name="rgb",
|
|
channels={"r": ChannelSpec("r", target, "rgb")},
|
|
),
|
|
"color_temp": ControllerModeSpec(
|
|
name="color_temp",
|
|
channels={
|
|
"ww": ChannelSpec(
|
|
"ww",
|
|
Color.from_xy((target.xy[0] + 0.002, target.xy[1]), brightness=5.0),
|
|
"color_temp",
|
|
),
|
|
"cw": ChannelSpec(
|
|
"cw",
|
|
Color.from_xy((target.xy[0] - 0.002, target.xy[1] + 0.001), brightness=5.0),
|
|
"color_temp",
|
|
),
|
|
},
|
|
),
|
|
},
|
|
)
|
|
},
|
|
)
|
|
|
|
result = solve_light(
|
|
compile_light_model(spec, approximate_chroma_band=0.0001),
|
|
target,
|
|
brightness=255,
|
|
requested_mode="color_temp",
|
|
)
|
|
|
|
assert result.controller_modes == {"shared": "rgb"}
|
|
assert result.exact_chroma_match is True
|
|
|
|
|
|
def test_wc_ct_sweep_stays_near_requested_ct_line_through_middle_range() -> None:
|
|
spec = _group_debug_spec(GROUPS["Example Bathroom"], LIGHTS)
|
|
model = compile_light_model(spec)
|
|
|
|
for mireds in [220, 250, 286, 320, 360]:
|
|
result = solve_light(
|
|
model,
|
|
Color.from_color_temp(mireds, brightness=1.0, temp_range=spec.exposed_mireds),
|
|
brightness=255,
|
|
requested_mode="color_temp",
|
|
)
|
|
|
|
assert result.chroma_error <= model.approximate_chroma_band
|
|
assert abs(result.achieved_color.color_temp - mireds) <= 2
|
|
assert any(".color_temp." in address for address in result.channel_levels)
|
|
|
|
|
|
def test_wc_low_brightness_ct_can_use_rgb_correction_without_redefining_reference() -> None:
|
|
spec = _group_debug_spec(GROUPS["Example Bathroom"], LIGHTS)
|
|
model = compile_light_model(spec)
|
|
target = Color.from_color_temp(286, brightness=1.0, temp_range=spec.exposed_mireds)
|
|
|
|
low = solve_light(model, target, brightness=50, requested_mode="color_temp")
|
|
high = solve_light(model, target, brightness=255, requested_mode="color_temp")
|
|
|
|
assert low.reference_color.brightness == high.reference_color.brightness
|
|
assert any(".rgb." in address for address in low.channel_levels)
|
|
assert low.chroma_error < high.chroma_error
|
|
assert abs(high.achieved_color.color_temp - 286) <= 2
|
|
|
|
|
|
def test_wc_katto_low_brightness_ct_can_sacrifice_one_bulb_for_rgb_correction() -> None:
|
|
spec = LIGHTS["Example Bathroom Ceiling"]
|
|
model = compile_light_model(spec)
|
|
target = Color.from_color_temp(286, brightness=1.0, temp_range=spec.exposed_mireds)
|
|
|
|
low = solve_light(model, target, brightness=50, requested_mode="color_temp")
|
|
exact_edge = solve_light(model, target, brightness=83, requested_mode="color_temp")
|
|
best_effort = solve_light(model, target, brightness=105, requested_mode="color_temp")
|
|
high = solve_light(model, target, brightness=255, requested_mode="color_temp")
|
|
|
|
assert any(mode == "rgb" for mode in low.controller_modes.values())
|
|
assert any(mode == "rgb" for mode in exact_edge.controller_modes.values())
|
|
assert any(mode == "rgb" for mode in best_effort.controller_modes.values())
|
|
assert all(mode == "color_temp" for mode in high.controller_modes.values())
|
|
assert exact_edge.chroma_error <= model.chroma_tolerance
|
|
assert best_effort.chroma_error < high.chroma_error
|
|
assert low.chroma_error < high.chroma_error
|
|
assert abs(high.achieved_color.color_temp - 286) <= 2
|
|
|
|
|
|
def test_wc_katto_balances_equivalent_channels() -> None:
|
|
spec = LIGHTS["Example Bathroom Ceiling"]
|
|
model = compile_light_model(spec)
|
|
target = Color.from_color_temp(300, brightness=1.0, temp_range=spec.exposed_mireds)
|
|
|
|
rgb_correction = solve_light(model, target, brightness=90, requested_mode="color_temp")
|
|
assert rgb_correction.controller_modes["bath_ceiling_1"] == "rgb"
|
|
assert rgb_correction.controller_modes["bath_ceiling_2"] == "rgb"
|
|
assert abs(rgb_correction.channel_levels["bath_ceiling_1.rgb.r"] - rgb_correction.channel_levels["bath_ceiling_2.rgb.r"]) < 1e-6
|
|
assert abs(rgb_correction.channel_levels["bath_ceiling_1.rgb.g"] - rgb_correction.channel_levels["bath_ceiling_2.rgb.g"]) < 1e-6
|
|
|
|
ct_correction = solve_light(model, target, brightness=99, requested_mode="color_temp")
|
|
assert ct_correction.controller_modes["bath_ceiling_2"] == "color_temp"
|
|
assert ct_correction.controller_modes["bath_ceiling_3"] == "color_temp"
|
|
assert abs(ct_correction.channel_levels["bath_ceiling_2.color_temp.ww"] - ct_correction.channel_levels["bath_ceiling_3.color_temp.ww"]) < 1e-6
|
|
assert abs(ct_correction.channel_levels["bath_ceiling_2.color_temp.cw"] - ct_correction.channel_levels["bath_ceiling_3.color_temp.cw"]) < 1e-6
|
|
|
|
|
|
def test_example_all_lights_does_not_branch_on_dimmable_off_states() -> None:
|
|
spec = _combine_specs(
|
|
LIGHTS,
|
|
[
|
|
"Example Fixed White A",
|
|
"Example Fixed White B",
|
|
"Example Mixed North",
|
|
"Example Plug A",
|
|
"Example Plug B",
|
|
"Example Hall Floor",
|
|
"Example Hall Corner",
|
|
"Example Hall Cabinets",
|
|
],
|
|
)
|
|
model = compile_light_model(spec)
|
|
assert len(model.combinations) == 32
|
|
|
|
|
|
def test_example_all_lights_deduplicates_reference_combinations() -> None:
|
|
spec = _combine_specs(
|
|
LIGHTS,
|
|
[
|
|
"Example Fixed White A",
|
|
"Example Fixed White B",
|
|
"Example Mixed North",
|
|
"Example Plug A",
|
|
"Example Plug B",
|
|
"Example Hall Floor",
|
|
"Example Hall Corner",
|
|
"Example Hall Cabinets",
|
|
],
|
|
)
|
|
model = compile_light_model(spec)
|
|
assert len(model.reference_combinations) < len(model.combinations)
|
|
|
|
|
|
def test_example_all_lights_ct_sweep_does_not_use_unused_onoff_reference() -> None:
|
|
spec = _combine_specs(
|
|
LIGHTS,
|
|
[
|
|
"Example Fixed White A",
|
|
"Example Fixed White B",
|
|
"Example Mixed North",
|
|
"Example Plug A",
|
|
"Example Plug B",
|
|
"Example Hall Floor",
|
|
"Example Hall Corner",
|
|
"Example Hall Cabinets",
|
|
],
|
|
)
|
|
model = compile_light_model(spec)
|
|
previous: dict[str, float] | None = None
|
|
max_delta = 0.0
|
|
|
|
for mireds in range(300, 321):
|
|
result = solve_light(
|
|
model,
|
|
Color.from_color_temp(mireds, brightness=1.0),
|
|
brightness=181,
|
|
requested_mode="color_temp",
|
|
)
|
|
assert result.channel_levels.get("plug_a.onoff.w", 0.0) == 0.0
|
|
assert result.channel_levels.get("plug_b.onoff.w", 0.0) == 0.0
|
|
levels = {
|
|
channel: level
|
|
for channel, level in result.channel_levels.items()
|
|
if level > 1e-6
|
|
}
|
|
if previous is not None:
|
|
max_delta = max(
|
|
max_delta,
|
|
max(abs(levels.get(channel, 0.0) - previous.get(channel, 0.0)) for channel in set(levels) | set(previous)),
|
|
)
|
|
previous = levels
|
|
|
|
assert max_delta < 0.9
|