zencontrol-python 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ import asyncio
2
+ import yaml
3
+ from pathlib import Path
4
+ from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
5
+
6
+ async def main():
7
+ """Test the async ZenProtocol with controller queries"""
8
+ # Load configuration
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
10
+
11
+ # Create protocol and controller
12
+ async with ZenProtocol(print_traffic=True) as tpi:
13
+ ctrl = ZenController(protocol=tpi, **config.get('zencontrol')[0])
14
+ tpi.set_controllers([ctrl])
15
+
16
+ print("Testing ZenController queries...")
17
+ print("=" * 50)
18
+
19
+ try:
20
+ # Query controller version
21
+ version = await tpi.query_controller_version_number(ctrl)
22
+ print(f"ZenController version: {version}")
23
+
24
+ # Query controller label
25
+ controller_label = await tpi.query_controller_label(ctrl)
26
+ print(f"ZenController label: {controller_label}")
27
+
28
+ # Query controller fitting number
29
+ controller_fitting_number = await tpi.query_controller_fitting_number(ctrl)
30
+ print(f"ZenController fitting number: {controller_fitting_number}")
31
+
32
+ # Query startup status
33
+ startup_complete = await tpi.query_controller_startup_complete(ctrl)
34
+ print(f"ZenController startup complete: {startup_complete}")
35
+
36
+ # Query DALI bus status
37
+ dali_ready = await tpi.query_is_dali_ready(ctrl)
38
+ print(f"DALI bus is ready: {dali_ready}")
39
+
40
+ except Exception as e:
41
+ print(f"Error during testing: {e}")
42
+
43
+ print("=" * 50)
44
+ print("Test completed!")
45
+
46
+ if __name__ == "__main__":
47
+ run_with_keyboard_interrupt(main)
48
+
@@ -0,0 +1,48 @@
1
+ import asyncio
2
+ import yaml
3
+ from pathlib import Path
4
+ from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
5
+
6
+ async def main():
7
+ """Test DALI device queries"""
8
+ # Load configuration
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
10
+
11
+ # Create protocol and controller
12
+ async with ZenProtocol(print_traffic=False) as tpi:
13
+ ctrl = ZenController(protocol=tpi, **config.get('zencontrol')[0])
14
+ tpi.set_controllers([ctrl])
15
+
16
+ print("Testing DALI device queries...")
17
+ print("=" * 50)
18
+
19
+ try:
20
+ addresses = await tpi.query_dali_addresses_with_instances(ctrl, 0)
21
+ print(f"Addresses with instances:")
22
+
23
+ for address in addresses:
24
+ print(f" {address.number}")
25
+
26
+ label = await tpi.query_dali_device_label(address, generic_if_none=True)
27
+ print(f" label: {label}")
28
+
29
+ operating_mode = await tpi.query_operating_mode_by_address(address)
30
+ print(f" operating mode: {operating_mode}")
31
+
32
+ serial = await tpi.query_dali_serial(address)
33
+ print(f" serial: {serial}")
34
+
35
+ ean = await tpi.query_dali_ean(address)
36
+ print(f" ean: {ean}")
37
+
38
+ fitting = await tpi.query_dali_fitting_number(address)
39
+ print(f" fitting: {fitting}")
40
+
41
+ except Exception as e:
42
+ print(f"Error during testing: {e}")
43
+
44
+ print("=" * 50)
45
+ print("Test completed!")
46
+
47
+ if __name__ == "__main__":
48
+ run_with_keyboard_interrupt(main)
@@ -0,0 +1,119 @@
1
+ import asyncio
2
+ import logging
3
+ import yaml
4
+ import time
5
+ from pathlib import Path
6
+ import sys
7
+
8
+ ROOT = Path(__file__).resolve().parent.parent
9
+ if str(ROOT) not in sys.path:
10
+ sys.path.insert(0, str(ROOT))
11
+
12
+ sys.stdout.reconfigure(line_buffering=True)
13
+
14
+ from zencontrol import ZenProtocol, ZenController, ZenAddress, ZenInstance, run_with_keyboard_interrupt
15
+
16
+ CONFIG_PATH = Path(__file__).resolve().parents[2] / "tests" / "config.yaml"
17
+
18
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
19
+ logger = logging.getLogger("test_events")
20
+
21
+ # Global timing variable for event timing
22
+ timevar = None
23
+
24
+ def ms():
25
+ """Print time since last call in milliseconds"""
26
+ global timevar
27
+ timevar = timevar or time.time()
28
+ msecs = (time.time() - timevar) * 1000
29
+ timevar = time.time()
30
+ print(f"{msecs:.1f} ms")
31
+
32
+ # Event handlers — must be async; protocol awaits them
33
+ async def button_press_event(instance: ZenInstance, payload: bytes) -> None:
34
+ ms()
35
+ print(f"Button Press Event - ECD {instance.address.number} instance {instance.number}")
36
+
37
+ async def level_change_event(address: ZenAddress, arc_level: int, payload: bytes) -> None:
38
+ ms()
39
+ print(f"Level Change Event - {address.type} {address.number} arc_level {arc_level}")
40
+
41
+ async def group_level_change_event(address: ZenAddress, arc_level: int, payload: bytes) -> None:
42
+ ms()
43
+ print(f"Level Change Event Group - {address.type} {address.number} arc_level {arc_level}")
44
+
45
+ async def scene_change_event(address: ZenAddress, scene: int, active: bool, payload: bytes) -> None:
46
+ ms()
47
+ print(f"Scene Change Event - {address.type} {address.number} scene {scene}")
48
+
49
+ async def colour_change_event(address: ZenAddress, colour, payload: bytes) -> None:
50
+ ms()
51
+ print(f"Colour Change Event - {address.type} {address.number} colour {colour}")
52
+
53
+ def check_event_listener(tpi: ZenProtocol) -> None:
54
+ """Print listener health — call periodically to detect silent task death."""
55
+ task = tpi.event_task
56
+ listener = tpi.event_listener
57
+
58
+ if task is None:
59
+ print("LISTENER: event_task not started")
60
+ return
61
+ if task.done():
62
+ exc = task.exception()
63
+ if exc:
64
+ print(f"LISTENER: event_task died with {type(exc).__name__}: {exc}")
65
+ else:
66
+ print("LISTENER: event_task finished unexpectedly")
67
+ elif listener is None or not listener.is_listening():
68
+ print("LISTENER: event_task running but socket is closed")
69
+ else:
70
+ print(f"LISTENER: ok (queue size {listener._event_queue.qsize()})")
71
+
72
+ async def main():
73
+ """Test async ZenProtocol with event monitoring"""
74
+ config = yaml.safe_load(CONFIG_PATH.read_text())
75
+
76
+ async with ZenProtocol(print_traffic=True, unicast=False, logger=logger) as tpi:
77
+ ctrl = ZenController(protocol=tpi, **config.get('zencontrol')[0])
78
+ tpi.set_controllers([ctrl])
79
+
80
+ print("Testing ZenProtocol Event Monitoring...")
81
+ print("=" * 60)
82
+
83
+ tpi.set_callbacks(
84
+ button_press_callback=button_press_event,
85
+ level_change_callback=level_change_event,
86
+ group_level_change_callback=group_level_change_event,
87
+ scene_change_callback=scene_change_event,
88
+ colour_change_callback=colour_change_event
89
+ )
90
+
91
+ try:
92
+ print("Querying TPI event configuration...")
93
+ unicast_config = await tpi.query_tpi_event_unicast_address(ctrl)
94
+ print(f"TPI Event Unicast Address: {unicast_config}")
95
+
96
+ emit_state = await tpi.query_tpi_event_emit_state(ctrl)
97
+ print(f"TPI Event Emit State: {emit_state}")
98
+
99
+ except Exception as e:
100
+ print(f"Error querying event configuration: {e}")
101
+
102
+ print("\nStarting event monitoring...")
103
+ print("Press Ctrl+C to stop")
104
+ print("=" * 60)
105
+
106
+ await tpi.start_event_monitoring()
107
+ check_event_listener(tpi)
108
+
109
+ try:
110
+ while True:
111
+ await asyncio.sleep(5)
112
+ check_event_listener(tpi)
113
+ except KeyboardInterrupt:
114
+ print("\nStopping event monitoring...")
115
+ await tpi.stop_event_monitoring()
116
+ print("Event monitoring stopped.")
117
+
118
+ if __name__ == "__main__":
119
+ run_with_keyboard_interrupt(main)
examples/live/gear.py ADDED
@@ -0,0 +1,86 @@
1
+ import asyncio
2
+ import yaml
3
+ from pathlib import Path
4
+ from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
5
+
6
+ async def main():
7
+ """Test control gear queries"""
8
+ # Load configuration
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
10
+
11
+ # Create protocol and controller
12
+ async with ZenProtocol(print_traffic=False) as tpi:
13
+ ctrl = ZenController(protocol=tpi, **config.get('zencontrol')[0])
14
+ tpi.set_controllers([ctrl])
15
+
16
+ print("Testing control gear queries...")
17
+ print("=" * 50)
18
+
19
+ try:
20
+ addresses = await tpi.query_control_gear_dali_addresses(ctrl)
21
+ print(f"Control gears")
22
+
23
+
24
+ for address in addresses[:5]: # Only first 5 gears
25
+ print(f" {address.number}")
26
+
27
+ level = await tpi.dali_query_level(address)
28
+ print(f" current level: {level}")
29
+
30
+ label = await tpi.query_dali_device_label(address)
31
+ print(f" label: {label}")
32
+
33
+ groups = await tpi.query_group_membership_by_address(address)
34
+ print(f" groups: {[group.number for group in groups]}")
35
+
36
+ type = await tpi.dali_query_cg_type(address)
37
+ print(f" type: {type}")
38
+
39
+ colour = await tpi.query_dali_colour(address)
40
+ print(f" colour: {colour}")
41
+
42
+ cgtype = await tpi.query_dali_colour_features(address)
43
+ print(f" colour features: {cgtype}")
44
+
45
+ colour_temp_limits = await tpi.query_dali_colour_temp_limits(address)
46
+ print(f" colour temp limits: {colour_temp_limits}")
47
+
48
+ fitting = await tpi.query_dali_fitting_number(address)
49
+ print(f" fitting: {fitting}")
50
+
51
+ ean = await tpi.query_dali_ean(address)
52
+ print(f" ean: {ean}")
53
+
54
+ serial = await tpi.query_dali_serial(address)
55
+ print(f" serial: {serial}")
56
+
57
+ last_scene = await tpi.dali_query_last_scene(address)
58
+ print(f" last scene: {last_scene}")
59
+
60
+ last_scene_is_current = await tpi.dali_query_last_scene_is_current(address)
61
+ print(f" last scene is current: {last_scene_is_current}")
62
+
63
+ status = await tpi.dali_query_control_gear_status(address)
64
+ print(f" status: {status}")
65
+
66
+ scenes = await tpi.query_scene_numbers_by_address(address)
67
+ print(f" scenes with levels: {scenes}")
68
+
69
+ scenes = await tpi.query_colour_scene_membership_by_address(address)
70
+ print(f" scenes with colours: {scenes}")
71
+
72
+ levels = await tpi.query_scene_levels_by_address(address)
73
+ print(f" scene levels: {levels}")
74
+
75
+ scene_data = await tpi.query_scene_colours_by_address(address)
76
+ print(f" scene colour data: {scene_data}")
77
+
78
+ except Exception as e:
79
+ print(f"Error during testing: {e}")
80
+
81
+ print("=" * 50)
82
+ print("Test completed!")
83
+
84
+ if __name__ == "__main__":
85
+ run_with_keyboard_interrupt(main)
86
+
@@ -0,0 +1,53 @@
1
+ import asyncio
2
+ import yaml
3
+ from pathlib import Path
4
+ from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
5
+
6
+ async def main():
7
+ """Test group queries"""
8
+ # Load configuration
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
10
+
11
+ # Create protocol and controller
12
+ async with ZenProtocol(print_traffic=True) as tpi:
13
+ ctrl = ZenController(protocol=tpi, **config.get('zencontrol')[0])
14
+ tpi.set_controllers([ctrl])
15
+
16
+ print("Testing group queries...")
17
+ print("=" * 50)
18
+
19
+ try:
20
+ addresses = await tpi.query_group_numbers(ctrl)
21
+ print(f"Group")
22
+
23
+ for address in addresses:
24
+ print(f" {address.number}")
25
+ name = await tpi.query_group_label(address)
26
+ print(f" name: {name}")
27
+
28
+ information = await tpi.query_group_by_number(address)
29
+ print(f" information: {information}")
30
+
31
+ scenes = await tpi.query_scene_numbers_for_group(address)
32
+ print(f" scenes: {scenes}")
33
+
34
+ for scene in scenes:
35
+ label = await tpi.query_scene_label_for_group(address, scene)
36
+ print(f" Group {address.number} scene {scene} label: {label}")
37
+
38
+ gear = await tpi.query_control_gear_dali_addresses(ctrl)
39
+ print(f"Gear")
40
+ for gear in gear:
41
+ print(f" {gear.number}")
42
+ groups = await tpi.query_group_membership_by_address(gear)
43
+ for group in groups:
44
+ print(f" group: {group}")
45
+
46
+ except Exception as e:
47
+ print(f"Error during testing: {e}")
48
+
49
+ print("=" * 50)
50
+ print("Test completed!")
51
+
52
+ if __name__ == "__main__":
53
+ run_with_keyboard_interrupt(main)
@@ -0,0 +1,60 @@
1
+ import asyncio
2
+ import yaml
3
+ from pathlib import Path
4
+ from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
5
+
6
+ async def main():
7
+ """Test instance queries"""
8
+ # Load configuration
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
10
+
11
+ # Create protocol and controller
12
+ async with ZenProtocol(print_traffic=False) as tpi:
13
+ ctrl = ZenController(protocol=tpi, **config.get('zencontrol')[0])
14
+ tpi.set_controllers([ctrl])
15
+
16
+ print("Testing instance queries...")
17
+ print("=" * 50)
18
+
19
+ try:
20
+ addresses = await tpi.query_dali_addresses_with_instances(ctrl, 0)
21
+ print(f"Addresses with instances:")
22
+
23
+ for address in addresses:
24
+ print(f" {address.number}")
25
+
26
+ label = await tpi.query_dali_device_label(address)
27
+ print(f" label: {label}")
28
+
29
+ operating_mode = await tpi.query_operating_mode_by_address(address)
30
+ print(f" operating mode: {operating_mode}")
31
+
32
+ instances = await tpi.query_instances_by_address(address)
33
+ # print(f" instances: {instances}")
34
+
35
+ for instance in instances:
36
+
37
+ instance_label = await tpi.query_dali_instance_label(instance)
38
+ print(f" {instance.number} - {instance_label}")
39
+
40
+ groups = await tpi.query_instance_groups(instance)
41
+ print(f" groups: {groups}")
42
+
43
+ fitting = await tpi.query_dali_instance_fitting_number(instance)
44
+ print(f" fitting: {fitting}")
45
+
46
+ occupancy_timers = await tpi.query_occupancy_instance_timers(instance)
47
+ print(f" occupancy timers: {occupancy_timers}")
48
+
49
+ last_known_led_state = await tpi.query_last_known_dali_button_led_state(instance)
50
+ print(f" last known led state: {last_known_led_state}")
51
+
52
+ except Exception as e:
53
+ print(f"Error during testing: {e}")
54
+
55
+ print("=" * 50)
56
+ print("Test completed!")
57
+
58
+ if __name__ == "__main__":
59
+ run_with_keyboard_interrupt(main)
60
+
@@ -0,0 +1,56 @@
1
+ import asyncio
2
+ from zencontrol import ZenControl, run_with_keyboard_interrupt
3
+ import yaml
4
+ from pathlib import Path
5
+ import time
6
+
7
+ async def main():
8
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
+ zi = ZenControl(print_traffic=False)
10
+ zi.add_controller(**config.get('zencontrol')[0])
11
+ await zi.start()
12
+
13
+ timer_start = time.time()
14
+
15
+ print("Profiles")
16
+ profiles = await zi.get_profiles()
17
+ for profile in profiles:
18
+ print(f" • {profile}")
19
+
20
+ print("Lights")
21
+ lights = await zi.get_lights()
22
+ for light in lights:
23
+ print(f" • {light}")
24
+ for group in light.groups:
25
+ print(f" • {group}")
26
+
27
+ print("Groups")
28
+ groups = await zi.get_groups()
29
+ for group in groups:
30
+ print(f" • {group}")
31
+ for light in group.lights:
32
+ print(f" • {light}")
33
+
34
+ print("Buttons")
35
+ buttons = await zi.get_buttons()
36
+ for button in buttons:
37
+ print(f" • {button}")
38
+
39
+ print("Motion sensors")
40
+ motion_sensors = await zi.get_motion_sensors()
41
+ for motion_sensor in motion_sensors:
42
+ print(f" • {motion_sensor}")
43
+ print(f" = {'occupied' if motion_sensor.occupied else 'not occupied'}")
44
+
45
+ print("System variables")
46
+ system_variables = await zi.get_system_variables()
47
+ for zsv in system_variables:
48
+ print(f" • {zsv}")
49
+ value = await zsv.get_value()
50
+ print(f" = {value}")
51
+
52
+ timer_end = time.time()
53
+ print(f"Time taken: {timer_end - timer_start} seconds")
54
+
55
+ if __name__ == "__main__":
56
+ run_with_keyboard_interrupt(main)
@@ -0,0 +1,80 @@
1
+ import asyncio
2
+ from zencontrol import ZenControl, ZenProfile, ZenGroup, ZenLight, ZenButton, ZenMotionSensor, ZenSystemVariable, ZenColour, run_with_keyboard_interrupt
3
+ import yaml
4
+ from pathlib import Path
5
+ import time
6
+
7
+ async def main():
8
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
+ zi = ZenControl(print_traffic=False)
10
+ zi.add_controller(**config.get('zencontrol')[0])
11
+
12
+ # Handlers
13
+ async def _zen_on_connect() -> None:
14
+ print(f"Connected to Zen")
15
+
16
+ async def _zen_on_disconnect() -> None:
17
+ print(f"Disconnected from Zen")
18
+
19
+ async def _zen_profile_change(profile: ZenProfile) -> None:
20
+ ms()
21
+ print(f"Profile Change Event - {profile}")
22
+
23
+ async def _zen_group_change(group: ZenGroup, level: int | None = None, colour: ZenColour | None = None, scene: int | None = None, discoordinated: bool = False) -> None:
24
+ ms()
25
+ print(f"Group Change Event - {group} level {level} colour {colour} scene {scene} {'discoordinated' if discoordinated else ''}")
26
+
27
+ async def _zen_light_change(light: ZenLight, level: int | None = None, colour: ZenColour | None = None, scene: int | None = None) -> None:
28
+ ms()
29
+ print(f"Light Change Event - {light} level {level} colour {colour} scene {scene}")
30
+
31
+ async def _zen_button_press(button: ZenButton) -> None:
32
+ ms()
33
+ print(f"Button Press Event - {button}")
34
+
35
+ async def _zen_button_long_press(button: ZenButton) -> None:
36
+ ms()
37
+ print(f"Button Long Press Event - {button}")
38
+
39
+ async def _zen_motion_event(sensor: ZenMotionSensor, occupied: bool) -> None:
40
+ ms()
41
+ print(f"Motion Event - {sensor} {'occupied' if occupied else 'not occupied'}")
42
+
43
+ async def _zen_system_variable_change(system_variable: ZenSystemVariable, value: int, changed: bool, by_me: bool) -> None:
44
+ ms()
45
+ print(f"System Variable Change - {system_variable} value {value} {'changed' if changed else 'not changed'} {'by me' if by_me else 'by someone else'}")
46
+
47
+ timevar = None
48
+ def ms():
49
+ nonlocal timevar
50
+ timevar = timevar or time.time()
51
+ msecs = (time.time() - timevar) * 1000
52
+ timevar = time.time()
53
+ print(f"{msecs:.1f} ms")
54
+
55
+ # Set up event callbacks
56
+ # zi.on_connect = _zen_on_connect
57
+ # zi.on_disconnect = _zen_on_disconnect
58
+ # zi.profile_change = _zen_profile_change
59
+ zi.group_change = _zen_group_change
60
+ zi.light_change = _zen_light_change
61
+ zi.button_press = _zen_button_press
62
+ # zi.button_long_press = _zen_button_long_press
63
+ # zi.motion_event = _zen_motion_event
64
+ # zi.system_variable_change = _zen_system_variable_change
65
+
66
+ # Start event monitoring
67
+ await zi.start()
68
+
69
+ print("Event monitoring started. Press Ctrl+C to stop.")
70
+
71
+ # Loop forever
72
+ try:
73
+ while True:
74
+ await asyncio.sleep(1)
75
+ except KeyboardInterrupt:
76
+ print("\nStopping event monitoring...")
77
+ await zi.stop()
78
+
79
+ if __name__ == "__main__":
80
+ run_with_keyboard_interrupt(main)
examples/live/leds.py ADDED
@@ -0,0 +1,44 @@
1
+ import asyncio
2
+ import yaml
3
+ from pathlib import Path
4
+ from zencontrol import ZenProtocol, ZenController, ZenAddress, ZenAddressType, run_with_keyboard_interrupt
5
+
6
+ async def main():
7
+ """Test LED control queries"""
8
+ # Load configuration
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
10
+
11
+ # Create protocol and controller
12
+ async with ZenProtocol(print_traffic=False) as tpi:
13
+ ctrl = ZenController(protocol=tpi, **config.get('zencontrol')[0])
14
+ tpi.set_controllers([ctrl])
15
+
16
+ print("Testing LED control queries...")
17
+ print("=" * 50)
18
+
19
+ try:
20
+ address = ZenAddress(ctrl, ZenAddressType.ECD, 4) # Office
21
+
22
+ instances = await tpi.query_instances_by_address(address)
23
+ print(f" instances: {instances}")
24
+
25
+ for instance in instances:
26
+
27
+ instance_label = await tpi.query_dali_instance_label(instance, generic_if_none=True)
28
+ print(f" {instance.number} - {instance_label}")
29
+
30
+ last_known_led_state = await tpi.query_last_known_dali_button_led_state(instance)
31
+ print(f" last known led state: {last_known_led_state}")
32
+
33
+ set_led_state = await tpi.override_dali_button_led_state(instance, False)
34
+ print(f" set led state: {set_led_state}")
35
+
36
+ except Exception as e:
37
+ print(f"Error during testing: {e}")
38
+
39
+ print("=" * 50)
40
+ print("Test completed!")
41
+
42
+ if __name__ == "__main__":
43
+ run_with_keyboard_interrupt(main)
44
+
@@ -0,0 +1,77 @@
1
+ import asyncio
2
+ import random
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ ROOT = Path(__file__).resolve().parent.parent
7
+ if str(ROOT) not in sys.path:
8
+ sys.path.insert(0, str(ROOT))
9
+
10
+ import yaml
11
+
12
+ from zencontrol.api.models import ZenAddress, ZenController
13
+ from zencontrol.api.protocol import ZenProtocol
14
+ from zencontrol.api.types import ZenAddressType, ZenEventCode
15
+ from zencontrol.utils import run_with_keyboard_interrupt
16
+
17
+ CONFIG_PATH = Path(__file__).resolve().parents[2] / "tests" / "config.yaml"
18
+ ECG_ADDRESS = 33
19
+ TIMEOUT_SECONDS = 5.0
20
+
21
+
22
+ async def test_level_change_v2():
23
+ config = yaml.safe_load(CONFIG_PATH.read_text())
24
+ level = random.randint(100, 250)
25
+ event_received = asyncio.Event()
26
+ received: dict = {}
27
+
28
+ async with ZenProtocol(print_traffic=True) as tpi:
29
+ ctrl = ZenController(protocol=tpi, **config["zencontrol"][0])
30
+ tpi.set_controllers([ctrl])
31
+
32
+ original_process = tpi._process_zen_event
33
+
34
+ async def capture_level_change_v2(event):
35
+ if (
36
+ event.event_code == ZenEventCode.LEVEL_CHANGE_V2.value
37
+ and event.target == ECG_ADDRESS
38
+ ):
39
+ received["event"] = event
40
+ event_received.set()
41
+ await original_process(event)
42
+
43
+ tpi._process_zen_event = capture_level_change_v2
44
+
45
+ await tpi.start_event_monitoring()
46
+
47
+ try:
48
+ address = ZenAddress(ctrl, ZenAddressType.ECG, ECG_ADDRESS)
49
+ await tpi.dali_arc_level(address, level)
50
+ print(f"Set address {ECG_ADDRESS} to level {level}, waiting for LEVEL_CHANGE_V2...")
51
+ await asyncio.wait_for(event_received.wait(), timeout=TIMEOUT_SECONDS)
52
+ except asyncio.TimeoutError:
53
+ raise RuntimeError(
54
+ f"Timed out after {TIMEOUT_SECONDS}s waiting for LEVEL_CHANGE_V2 "
55
+ f"on address {ECG_ADDRESS} level {level}"
56
+ ) from None
57
+ finally:
58
+ await tpi.stop_event_monitoring()
59
+
60
+ event = received["event"]
61
+ if event.event_code != ZenEventCode.LEVEL_CHANGE_V2.value:
62
+ raise RuntimeError(f"Expected LEVEL_CHANGE_V2, got event code {event.event_code}")
63
+ if event.target != ECG_ADDRESS:
64
+ raise RuntimeError(f"Expected target {ECG_ADDRESS}, got {event.target}")
65
+
66
+ current = event.payload[0] if len(event.payload) >= 1 else None
67
+ target = event.payload[1] if len(event.payload) >= 2 else None
68
+ print(f"LEVEL_CHANGE_V2 received: address {event.target}, current {current}, target {target}")
69
+
70
+
71
+ async def main():
72
+ await test_level_change_v2()
73
+ print("Test passed.")
74
+
75
+
76
+ if __name__ == "__main__":
77
+ run_with_keyboard_interrupt(main)