from __future__ import annotations from datetime import datetime from ha_deconz_bridge.automation import ( ActionSpec, AutomationEngine, AutomationRule, RemoteGestureDetector, RemoteSpec, TimeCondition, automation_rules, ) def test_hue_dimmer_short_press_maps_to_single_and_double() -> None: remote = RemoteSpec("4", "room", "Room remote", double_press_window=0.5) detector = RemoteGestureDetector(default_double_press_window=0.45) first = detector.handle( remote, {"buttonevent": 1002, "eventduration": 1}, received_at=10.0, ) detector.handle(remote, {"buttonevent": 1000, "eventduration": 0}, received_at=10.2) second = detector.handle( remote, {"buttonevent": 1002, "eventduration": 1}, received_at=10.3, ) assert [(event.button, event.gesture) for event in first] == [("top", "single")] assert [(event.button, event.gesture) for event in second] == [("top", "double")] def test_double_press_state_is_per_physical_remote() -> None: detector = RemoteGestureDetector(default_double_press_window=0.45) first_remote = RemoteSpec("4", "room", "First") second_remote = RemoteSpec("5", "room", "Second") detector.handle(first_remote, {"buttonevent": 1002}, received_at=10.0) events = detector.handle(second_remote, {"buttonevent": 1002}, received_at=10.2) assert [event.gesture for event in events] == ["single"] def test_hue_dimmer_long_hold_maps_to_repeat_and_release() -> None: remote = RemoteSpec("4", "room", "Room remote") detector = RemoteGestureDetector(default_double_press_window=0.45) repeat = detector.handle( remote, {"buttonevent": 2001, "eventduration": 8}, received_at=10.8, ) release = detector.handle( remote, {"buttonevent": 2003, "eventduration": 22}, received_at=12.2, ) assert [(event.button, event.gesture, event.eventduration, event.hold_count) for event in repeat] == [ ("up", "hold", 8, 1) ] assert [(event.button, event.gesture, event.eventduration) for event in release] == [ ("up", "long", 22) ] def test_double_hold_and_double_long_use_same_physical_remote() -> None: remote = RemoteSpec("4", "room", "Room remote", double_press_window=0.5) detector = RemoteGestureDetector(default_double_press_window=0.45) detector.handle(remote, {"buttonevent": 1002}, received_at=10.0) detector.handle(remote, {"buttonevent": 1000}, received_at=10.2) first_hold = detector.handle(remote, {"buttonevent": 1001, "eventduration": 8}, received_at=10.4) repeat_hold = detector.handle(remote, {"buttonevent": 1001, "eventduration": 16}, received_at=11.2) release = detector.handle(remote, {"buttonevent": 1003, "eventduration": 20}, received_at=11.6) assert [(event.gesture, event.hold_count) for event in first_hold] == [("double_hold", 1)] assert [(event.gesture, event.hold_count) for event in repeat_hold] == [("double_hold", 2)] assert [event.gesture for event in release] == ["double_long"] def test_double_long_state_is_per_physical_remote() -> None: detector = RemoteGestureDetector(default_double_press_window=0.45) first_remote = RemoteSpec("4", "room", "First") second_remote = RemoteSpec("5", "room", "Second") detector.handle(first_remote, {"buttonevent": 1002}, received_at=10.0) detector.handle(second_remote, {"buttonevent": 1000}, received_at=10.1) events = detector.handle(second_remote, {"buttonevent": 1001}, received_at=10.2) assert [event.gesture for event in events] == ["hold"] def test_hold_count_gesture_patterns() -> None: action = ActionSpec("turn_off", target="Example Living") engine = AutomationEngine( remotes={"4": RemoteSpec("4", "room", "Room remote")}, rules=( AutomationRule("room", "up", "hold", (action,)), AutomationRule("room", "up", "hold_2", (action,)), AutomationRule("room", "up", "hold_2_2", (action,)), AutomationRule("room", "up", "hold_2_3", (action,)), AutomationRule("room", "up", "hold_3_2", (action,)), ), default_double_press_window=0.45, ) first = engine.handle_sensor_event("4", {"buttonevent": 2001}, received_at=10.0) second = engine.handle_sensor_event("4", {"buttonevent": 2001}, received_at=11.0) third = engine.handle_sensor_event("4", {"buttonevent": 2001}, received_at=12.0) fourth = engine.handle_sensor_event("4", {"buttonevent": 2001}, received_at=13.0) assert first is not None assert second is not None assert third is not None assert fourth is not None assert len(first[1]) == 1 assert len(second[1]) == 4 assert len(third[1]) == 3 assert len(fourth[1]) == 2 def test_automation_rules_bind_to_internal_remote_id() -> None: action = ActionSpec("turn_off", target="Example Living") engine = AutomationEngine( remotes={ "4": RemoteSpec("4", "room", "First"), "5": RemoteSpec("5", "room", "Second"), }, rules=( AutomationRule( remote_id="room", button="bottom", gesture="single", actions=(action,), ), ), default_double_press_window=0.45, ) first = engine.handle_sensor_event( "4", {"buttonevent": 4002}, received_at=10.0, ) second = engine.handle_sensor_event( "5", {"buttonevent": 4002}, received_at=20.0, ) assert first is not None assert second is not None assert first[1] == (action,) assert second[1] == (action,) def test_time_condition_handles_midnight_wrap() -> None: condition = TimeCondition("22:00", "06:00") assert condition.matches(datetime(2026, 1, 1, 23, 0)) assert condition.matches(datetime(2026, 1, 1, 5, 59)) assert not condition.matches(datetime(2026, 1, 1, 12, 0)) def test_automation_rules_can_be_built_from_nested_config() -> None: action = ActionSpec("turn_off", target="Example Living") rules = automation_rules( { "remote": { "bottom": { "single": (action,), "hold_2_0": (action,), }, }, } ) assert [(rule.remote_id, rule.button, rule.gesture, rule.actions) for rule in rules] == [ ("remote", "bottom", "single", (action,)), ("remote", "bottom", "hold_2_0", (action,)), ]