zencontrol-python 0.1.6__tar.gz → 0.1.7__tar.gz
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.
- {zencontrol_python-0.1.6/zencontrol_python.egg-info → zencontrol_python-0.1.7}/PKG-INFO +7 -4
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/level_change_v2.py +1 -1
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/mqtt_bridge.py +7 -1
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/pyproject.toml +15 -24
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_discovery.py +5 -4
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_lifecycle.py +1 -1
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_sim_events.py +4 -5
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_sim_interface.py +103 -18
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_sim_protocol.py +103 -25
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_wire_resilience.py +17 -8
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/__init__.py +34 -14
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/api/__init__.py +15 -2
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/api/models.py +27 -12
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/api/protocol.py +70 -45
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/api/types.py +2 -2
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/exceptions.py +1 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/interface/interface.py +78 -71
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/io/__init__.py +9 -2
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/io/command.py +20 -16
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/io/event.py +24 -22
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/utils.py +2 -2
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7/zencontrol_python.egg-info}/PKG-INFO +7 -4
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol_python.egg-info/requires.txt +6 -2
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/LICENSE +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/README.md +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/__init__.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/controller.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/devices.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/events.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/gear.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/groups.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/instances.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/interface.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/interface_events.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/leds.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/multicast.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/multicast_raw.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/profiles.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/tpi_event_listener.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/examples/live/variables.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/setup.cfg +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_absolute_input.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_entity_scope.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_event_keepalive.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_event_listener_disconnect.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_reconnect.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/tests/test_sub_labels.py +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/interface/__init__.py +4 -4
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol/py.typed +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol_python.egg-info/SOURCES.txt +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol_python.egg-info/dependency_links.txt +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol_python.egg-info/entry_points.txt +0 -0
- {zencontrol_python-0.1.6 → zencontrol_python-0.1.7}/zencontrol_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zencontrol-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Python implementation of the Zencontrol TPI Advanced protocol for DALI lighting controllers.
|
|
5
5
|
Author: Simon Wright
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,17 +19,20 @@ Classifier: Typing :: Typed
|
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
-
|
|
22
|
+
Provides-Extra: colorama
|
|
23
|
+
Requires-Dist: colorama>=0.4.6; extra == "colorama"
|
|
23
24
|
Provides-Extra: dev
|
|
24
25
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
25
26
|
Requires-Dist: pytest-asyncio>=0.18.0; extra == "dev"
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
Requires-Dist: flake8>=4.0; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
28
|
Requires-Dist: mypy>=0.950; extra == "dev"
|
|
29
|
+
Requires-Dist: types-colorama; extra == "dev"
|
|
29
30
|
Requires-Dist: build>=1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: colorama>=0.4.6; extra == "dev"
|
|
30
32
|
Provides-Extra: mqtt
|
|
31
33
|
Requires-Dist: aiomqtt>=2.1.0; extra == "mqtt"
|
|
32
34
|
Requires-Dist: PyYAML>=6.0; extra == "mqtt"
|
|
35
|
+
Requires-Dist: colorama>=0.4.6; extra == "mqtt"
|
|
33
36
|
Dynamic: license-file
|
|
34
37
|
|
|
35
38
|
# zencontrol-python
|
|
@@ -9,7 +9,7 @@ if str(ROOT) not in sys.path:
|
|
|
9
9
|
|
|
10
10
|
import yaml
|
|
11
11
|
|
|
12
|
-
from zencontrol
|
|
12
|
+
from zencontrol import ZenAddress, ZenController
|
|
13
13
|
from zencontrol.api.protocol import ZenProtocol
|
|
14
14
|
from zencontrol.api.types import ZenAddressType, ZenEventCode
|
|
15
15
|
from zencontrol.utils import run_with_keyboard_interrupt
|
|
@@ -10,12 +10,18 @@ import zencontrol
|
|
|
10
10
|
from zencontrol import ZenController, ZenProtocol, ZenClient, ZenColour, ZenColourType, ZenProfile, ZenLight, ZenGroup, ZenButton, ZenMotionSensor, ZenSystemVariable, ZenTimeoutError, ZenAddressType
|
|
11
11
|
from zencontrol.api.types import Const as ApiConst
|
|
12
12
|
import aiomqtt
|
|
13
|
-
from colorama import Fore, Back, Style
|
|
14
13
|
import logging
|
|
15
14
|
from logging.handlers import RotatingFileHandler
|
|
16
15
|
import math
|
|
17
16
|
import pickle
|
|
18
17
|
import traceback
|
|
18
|
+
try:
|
|
19
|
+
from colorama import Fore, Style
|
|
20
|
+
except ImportError:
|
|
21
|
+
class _NoColor:
|
|
22
|
+
def __getattr__(self, _name: str) -> str:
|
|
23
|
+
return ""
|
|
24
|
+
Fore = Style = _NoColor() # type: ignore[assignment]
|
|
19
25
|
|
|
20
26
|
class RateLimiter:
|
|
21
27
|
"""Rate limiter to control concurrent coroutine execution"""
|
|
@@ -24,22 +24,25 @@ classifiers = [
|
|
|
24
24
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
25
|
"Typing :: Typed",
|
|
26
26
|
]
|
|
27
|
-
dependencies = [
|
|
28
|
-
"colorama>=0.4.6",
|
|
29
|
-
]
|
|
27
|
+
dependencies = []
|
|
30
28
|
|
|
31
29
|
[project.optional-dependencies]
|
|
30
|
+
colorama = [
|
|
31
|
+
"colorama>=0.4.6",
|
|
32
|
+
]
|
|
32
33
|
dev = [
|
|
33
34
|
"pytest>=6.0",
|
|
34
35
|
"pytest-asyncio>=0.18.0",
|
|
35
|
-
"
|
|
36
|
-
"flake8>=4.0",
|
|
36
|
+
"ruff",
|
|
37
37
|
"mypy>=0.950",
|
|
38
|
+
"types-colorama",
|
|
38
39
|
"build>=1.0",
|
|
40
|
+
"colorama>=0.4.6",
|
|
39
41
|
]
|
|
40
42
|
mqtt = [
|
|
41
43
|
"aiomqtt>=2.1.0",
|
|
42
44
|
"PyYAML>=6.0",
|
|
45
|
+
"colorama>=0.4.6",
|
|
43
46
|
]
|
|
44
47
|
# Simulator integration tests (tests/test_sim_*.py) need zencontrol-simulator
|
|
45
48
|
# (pip install -e ../zencontrol-simulator, or a sibling ../zencontrol-simulator
|
|
@@ -63,24 +66,6 @@ include = ["zencontrol*", "examples*"]
|
|
|
63
66
|
[tool.setuptools.package-data]
|
|
64
67
|
zencontrol = ["py.typed"]
|
|
65
68
|
|
|
66
|
-
[tool.black]
|
|
67
|
-
line-length = 88
|
|
68
|
-
target-version = ['py314']
|
|
69
|
-
include = '\.pyi?$'
|
|
70
|
-
extend-exclude = '''
|
|
71
|
-
/(
|
|
72
|
-
# directories
|
|
73
|
-
\.eggs
|
|
74
|
-
| \.git
|
|
75
|
-
| \.hg
|
|
76
|
-
| \.mypy_cache
|
|
77
|
-
| \.tox
|
|
78
|
-
| \.venv
|
|
79
|
-
| build
|
|
80
|
-
| dist
|
|
81
|
-
)/
|
|
82
|
-
'''
|
|
83
|
-
|
|
84
69
|
[tool.mypy]
|
|
85
70
|
python_version = "3.14"
|
|
86
71
|
warn_return_any = true
|
|
@@ -109,7 +94,13 @@ markers = [
|
|
|
109
94
|
|
|
110
95
|
[tool.ruff]
|
|
111
96
|
target-version = "py314"
|
|
112
|
-
line-length =
|
|
97
|
+
line-length = 130
|
|
98
|
+
exclude = ["examples"]
|
|
113
99
|
|
|
114
100
|
[tool.ruff.lint]
|
|
115
101
|
select = ["ASYNC", "B", "E", "F", "I", "UP"]
|
|
102
|
+
ignore = [
|
|
103
|
+
"ASYNC109", # Timeout parameters are part of the public async API.
|
|
104
|
+
"E501", # Preserve existing protocol logging and payload formatting.
|
|
105
|
+
"E701", # Allow readable single-line conditionals.
|
|
106
|
+
]
|
|
@@ -6,7 +6,8 @@ from unittest.mock import AsyncMock, patch
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
from zencontrol
|
|
9
|
+
from zencontrol import ZenController
|
|
10
|
+
from zencontrol.api.models import DiscoveredController
|
|
10
11
|
from zencontrol.api.protocol import ZenProtocol
|
|
11
12
|
from zencontrol.io.event import ZenEvent
|
|
12
13
|
|
|
@@ -76,7 +77,7 @@ async def test_second_packet_from_same_mac_or_ip_is_ignored() -> None:
|
|
|
76
77
|
async def test_registered_controller_is_not_discovered() -> None:
|
|
77
78
|
protocol = ZenProtocol()
|
|
78
79
|
ctrl = ZenController(
|
|
79
|
-
id=
|
|
80
|
+
id=1,
|
|
80
81
|
name="known",
|
|
81
82
|
label="Known",
|
|
82
83
|
host="192.168.1.50",
|
|
@@ -105,7 +106,7 @@ async def test_registering_controller_forgets_identified() -> None:
|
|
|
105
106
|
assert len(protocol.identified_controllers) == 1
|
|
106
107
|
|
|
107
108
|
ctrl = ZenController(
|
|
108
|
-
id=
|
|
109
|
+
id=1,
|
|
109
110
|
name="kitchen",
|
|
110
111
|
label="Kitchen",
|
|
111
112
|
host="192.168.1.50",
|
|
@@ -136,7 +137,7 @@ async def test_label_query_failure_still_remembers_controller() -> None:
|
|
|
136
137
|
async def test_new_controller_discovered_while_one_is_registered() -> None:
|
|
137
138
|
protocol = ZenProtocol()
|
|
138
139
|
known = ZenController(
|
|
139
|
-
id=
|
|
140
|
+
id=1,
|
|
140
141
|
name="known",
|
|
141
142
|
label="Known",
|
|
142
143
|
host="192.168.1.10",
|
|
@@ -67,7 +67,7 @@ async def test_aclose_closes_clients_and_clears_instances() -> None:
|
|
|
67
67
|
ctrl.client = fake_client
|
|
68
68
|
|
|
69
69
|
address = ZenAddress(controller=ctrl, type=ZenAddressType.ECG, number=1)
|
|
70
|
-
|
|
70
|
+
ZenLight(protocol=zen.protocol, address=address)
|
|
71
71
|
assert "ctrl-a 1" in zen.protocol.entity_registry.lights
|
|
72
72
|
assert "ctrl-a" in zen.protocol.entity_registry.controllers
|
|
73
73
|
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
+
from helpers import LEGACY_ACK, wait_until
|
|
6
7
|
|
|
7
8
|
from zencontrol import ZenColour, ZenColourType
|
|
8
9
|
|
|
9
|
-
from helpers import wait_until
|
|
10
|
-
|
|
11
10
|
pytestmark = pytest.mark.simulator
|
|
12
11
|
|
|
13
12
|
|
|
@@ -26,7 +25,7 @@ async def test_scene_and_colour_events_via_protocol(live_sim):
|
|
|
26
25
|
p.set_callbacks(scene_change_callback=on_scene, colour_change_callback=on_colour)
|
|
27
26
|
await p.start_event_monitoring()
|
|
28
27
|
|
|
29
|
-
assert await p.dali_scene(live_sim.ecg(0), 1) is
|
|
28
|
+
assert await p.dali_scene(live_sim.ecg(0), 1) is LEGACY_ACK
|
|
30
29
|
await wait_until(
|
|
31
30
|
lambda: any(t == "ECG" and n == 0 and s == 1 for t, n, s in scenes),
|
|
32
31
|
message="expected scene-change event for ECG 0 → scene 1",
|
|
@@ -61,7 +60,7 @@ async def test_group_level_event_via_protocol(live_sim):
|
|
|
61
60
|
)
|
|
62
61
|
await p.start_event_monitoring()
|
|
63
62
|
|
|
64
|
-
assert await p.dali_arc_level(live_sim.group(0), 44) is
|
|
63
|
+
assert await p.dali_arc_level(live_sim.group(0), 44) is LEGACY_ACK
|
|
65
64
|
await wait_until(
|
|
66
65
|
lambda: any(n == 0 and level == 44 for n, level in group_events),
|
|
67
66
|
message="expected group level-change event for group 0 → 44",
|
|
@@ -83,7 +82,7 @@ async def test_member_events_on_group_scene(live_sim):
|
|
|
83
82
|
p.set_callbacks(scene_change_callback=on_scene, level_change_callback=on_level)
|
|
84
83
|
await p.start_event_monitoring()
|
|
85
84
|
|
|
86
|
-
assert await p.dali_scene(live_sim.group(0), 1) is
|
|
85
|
+
assert await p.dali_scene(live_sim.group(0), 1) is LEGACY_ACK
|
|
87
86
|
await wait_until(
|
|
88
87
|
lambda: (
|
|
89
88
|
any(t == "GROUP" and n == 0 and s == 1 for t, n, s in scenes)
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
+
from helpers import LEGACY_ACK, wait_until
|
|
6
7
|
|
|
7
8
|
from zencontrol import ZenColour, ZenColourType
|
|
8
9
|
|
|
9
|
-
from helpers import wait_until
|
|
10
|
-
|
|
11
10
|
pytestmark = pytest.mark.simulator
|
|
12
11
|
|
|
13
12
|
|
|
@@ -65,11 +64,11 @@ async def test_light_set_and_query(live_zen):
|
|
|
65
64
|
lights = {lt.address.number: lt for lt in await zen.get_lights()}
|
|
66
65
|
light = lights[1]
|
|
67
66
|
|
|
68
|
-
assert await light.set(level=50, fade=True) is
|
|
67
|
+
assert await light.set(level=50, fade=True) is LEGACY_ACK
|
|
69
68
|
assert await zen.protocol.dali_query_level(light.address) == 50
|
|
70
69
|
assert live_sim.world.lights[1].level == 50
|
|
71
70
|
|
|
72
|
-
assert await light.off(fade=False) is
|
|
71
|
+
assert await light.off(fade=False) is LEGACY_ACK
|
|
73
72
|
assert await zen.protocol.dali_query_level(light.address) == 0
|
|
74
73
|
|
|
75
74
|
|
|
@@ -99,7 +98,7 @@ async def test_group_scene_and_profile_switch(live_zen):
|
|
|
99
98
|
|
|
100
99
|
groups = await zen.get_groups()
|
|
101
100
|
group = next(g for g in groups if g.address.number == 0)
|
|
102
|
-
assert await group.set_scene(1) is
|
|
101
|
+
assert await group.set_scene(1) is LEGACY_ACK
|
|
103
102
|
assert live_sim.world.groups[0].last_scene == 1
|
|
104
103
|
assert live_sim.world.lights[0].level == 80
|
|
105
104
|
assert live_sim.world.lights[1].level == 100
|
|
@@ -176,7 +175,7 @@ async def test_start_receives_injected_and_control_events(live_zen):
|
|
|
176
175
|
await zen.get_profiles()
|
|
177
176
|
|
|
178
177
|
lights = {lt.address.number: lt for lt in await zen.get_lights()}
|
|
179
|
-
assert await lights[1].set(level=66) is
|
|
178
|
+
assert await lights[1].set(level=66) is LEGACY_ACK
|
|
180
179
|
await wait_until(
|
|
181
180
|
lambda: any(n == 1 and level == 66 for n, level, _, _ in light_events),
|
|
182
181
|
message="expected light_change for ECG 1 → 66",
|
|
@@ -190,7 +189,7 @@ async def test_start_receives_injected_and_control_events(live_zen):
|
|
|
190
189
|
)
|
|
191
190
|
|
|
192
191
|
groups = {g.address.number: g for g in await zen.get_groups()}
|
|
193
|
-
assert await groups[0].set(level=55) is
|
|
192
|
+
assert await groups[0].set(level=55) is LEGACY_ACK
|
|
194
193
|
await wait_until(
|
|
195
194
|
lambda: any(n == 0 and level == 55 for n, level, _, _ in group_events),
|
|
196
195
|
message="expected group_change for group 0 → 55",
|
|
@@ -260,14 +259,14 @@ async def test_light_scene_and_on_off_via_interface(live_zen):
|
|
|
260
259
|
lights = {lt.address.number: lt for lt in await zen.get_lights()}
|
|
261
260
|
light = lights[0]
|
|
262
261
|
|
|
263
|
-
assert await light.set_scene(1) is
|
|
262
|
+
assert await light.set_scene(1) is LEGACY_ACK
|
|
264
263
|
assert live_sim.world.lights[0].level == 80
|
|
265
264
|
assert await zen.protocol.dali_query_last_scene(light.address) == 1
|
|
266
265
|
|
|
267
|
-
assert await light.set(level=120) is
|
|
268
|
-
assert await light.off(fade=False) is
|
|
266
|
+
assert await light.set(level=120) is LEGACY_ACK
|
|
267
|
+
assert await light.off(fade=False) is LEGACY_ACK
|
|
269
268
|
assert live_sim.world.lights[0].level == 0
|
|
270
|
-
assert await light.on(fade=False) is
|
|
269
|
+
assert await light.on(fade=False) is LEGACY_ACK
|
|
271
270
|
assert live_sim.world.lights[0].level == 120
|
|
272
271
|
|
|
273
272
|
|
|
@@ -280,11 +279,11 @@ async def test_group_scene_by_label_and_level(live_zen):
|
|
|
280
279
|
group = groups[0]
|
|
281
280
|
|
|
282
281
|
assert group.get_scene_number_from_label("Relax") == 1
|
|
283
|
-
assert await group.set_scene("Relax") is
|
|
282
|
+
assert await group.set_scene("Relax") is LEGACY_ACK
|
|
284
283
|
assert live_sim.world.groups[0].last_scene == 1
|
|
285
284
|
assert live_sim.world.lights[0].level == 80
|
|
286
285
|
|
|
287
|
-
assert await group.set(level=40) is
|
|
286
|
+
assert await group.set(level=40) is LEGACY_ACK
|
|
288
287
|
assert live_sim.world.lights[0].level == 40
|
|
289
288
|
assert live_sim.world.lights[1].level == 40
|
|
290
289
|
|
|
@@ -313,15 +312,15 @@ async def test_light_steps_recall_and_inhibit_via_interface(live_zen):
|
|
|
313
312
|
live_sim.world.lights[1].max_level = 200
|
|
314
313
|
live_sim.world.lights[1].min_level = 5
|
|
315
314
|
|
|
316
|
-
assert await light.set(level=10) is
|
|
317
|
-
assert await light.dali_up() is
|
|
315
|
+
assert await light.set(level=10) is LEGACY_ACK
|
|
316
|
+
assert await light.dali_up() is LEGACY_ACK
|
|
318
317
|
assert live_sim.world.lights[1].level == 11
|
|
319
|
-
assert await light.dali_down() is
|
|
318
|
+
assert await light.dali_down() is LEGACY_ACK
|
|
320
319
|
assert live_sim.world.lights[1].level == 10
|
|
321
320
|
|
|
322
|
-
assert await light.dali_recall_max() is
|
|
321
|
+
assert await light.dali_recall_max() is LEGACY_ACK
|
|
323
322
|
assert live_sim.world.lights[1].level == 200
|
|
324
|
-
assert await light.dali_recall_min() is
|
|
323
|
+
assert await light.dali_recall_min() is LEGACY_ACK
|
|
325
324
|
assert live_sim.world.lights[1].level == 5
|
|
326
325
|
|
|
327
326
|
assert await light.dali_inhibit(True) is True
|
|
@@ -350,3 +349,89 @@ async def test_button_and_sensor_interview_fields(live_zen):
|
|
|
350
349
|
# Entrance 6-button pad should contribute multiple button instances
|
|
351
350
|
entrance = [b for b in buttons if b.label == "Entrance 6-Button"]
|
|
352
351
|
assert len(entrance) == 6
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@pytest.mark.asyncio
|
|
355
|
+
async def test_light_fade_step_and_refresh_via_interface(live_zen):
|
|
356
|
+
zen, live_sim = live_zen
|
|
357
|
+
ctrl = zen.controllers[0]
|
|
358
|
+
await ctrl.interview()
|
|
359
|
+
lights = {lt.address.number: lt for lt in await zen.get_lights()}
|
|
360
|
+
light = lights[1]
|
|
361
|
+
|
|
362
|
+
assert await light.set(level=0, fade=False) is True
|
|
363
|
+
assert await light.dali_on_step_up() is LEGACY_ACK
|
|
364
|
+
assert live_sim.world.lights[1].level >= 1
|
|
365
|
+
|
|
366
|
+
assert await light.set(level=50, fade=False) is True
|
|
367
|
+
assert await light.dali_custom_fade(100, 5) is True
|
|
368
|
+
assert live_sim.world.lights[1].status & 0x10
|
|
369
|
+
assert await light.dali_stop_fade() is True
|
|
370
|
+
assert not (live_sim.world.lights[1].status & 0x10)
|
|
371
|
+
|
|
372
|
+
live_sim.world.lights[1].last_active_level = 88
|
|
373
|
+
assert await light.dali_go_to_last_active_level() is LEGACY_ACK
|
|
374
|
+
assert live_sim.world.lights[1].level == 88
|
|
375
|
+
|
|
376
|
+
assert await light.set(level=40, fade=False) is True
|
|
377
|
+
assert await light.dali_step_down_off() is LEGACY_ACK
|
|
378
|
+
assert live_sim.world.lights[1].level == 39
|
|
379
|
+
|
|
380
|
+
assert await light.dali_enable_dapc_sequence() is None
|
|
381
|
+
assert await light.dali_off() is LEGACY_ACK
|
|
382
|
+
assert live_sim.world.lights[1].level == 0
|
|
383
|
+
|
|
384
|
+
# Mutate controller under the entity, then refresh entity state.
|
|
385
|
+
assert await zen.protocol.dali_arc_level(light.address, 123) is LEGACY_ACK
|
|
386
|
+
light.level = None
|
|
387
|
+
await light.refresh_state_from_controller()
|
|
388
|
+
assert light.level == 123
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@pytest.mark.asyncio
|
|
392
|
+
async def test_sysvar_get_value_and_refresh(live_zen):
|
|
393
|
+
zen, live_sim = live_zen
|
|
394
|
+
ctrl = zen.controllers[0]
|
|
395
|
+
await ctrl.interview()
|
|
396
|
+
assert await ctrl.is_dali_ready() is True
|
|
397
|
+
|
|
398
|
+
sysvars = await zen.get_system_variables(give_up_after=5)
|
|
399
|
+
svar = next(v for v in sysvars if v.id == 0)
|
|
400
|
+
svar._value = None
|
|
401
|
+
live_sim.world.system_variables[0].value = 55
|
|
402
|
+
assert await svar.get_value() == 55
|
|
403
|
+
|
|
404
|
+
live_sim.world.system_variables[0].value = 66
|
|
405
|
+
await svar.refresh_state_from_controller()
|
|
406
|
+
assert svar.value == 66
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@pytest.mark.asyncio
|
|
410
|
+
async def test_motion_refresh_and_group_discoordination(live_zen):
|
|
411
|
+
zen, live_sim = live_zen
|
|
412
|
+
ctrl = zen.controllers[0]
|
|
413
|
+
await ctrl.interview()
|
|
414
|
+
|
|
415
|
+
sensors = await zen.get_motion_sensors()
|
|
416
|
+
porch = next(s for s in sensors if s.label == "Porch Sensor")
|
|
417
|
+
world_inst = live_sim.world.instance(10, 0)
|
|
418
|
+
assert world_inst is not None and world_inst.timers is not None
|
|
419
|
+
world_inst.timers.last_motion_at = __import__("time").time() - 5
|
|
420
|
+
assert await porch.refresh_state_from_controller() is True
|
|
421
|
+
assert porch.last_detect is not None
|
|
422
|
+
assert porch.hold_time == 60
|
|
423
|
+
|
|
424
|
+
groups = {g.address.number: g for g in await zen.get_groups()}
|
|
425
|
+
group = groups[0]
|
|
426
|
+
group.level = 40
|
|
427
|
+
group.scene = 1
|
|
428
|
+
disco: list = []
|
|
429
|
+
|
|
430
|
+
async def on_group(*, group, discoordinated=False, **kwargs):
|
|
431
|
+
if discoordinated:
|
|
432
|
+
disco.append(group.address.number)
|
|
433
|
+
|
|
434
|
+
zen.group_change = on_group
|
|
435
|
+
await group.declare_discoordination()
|
|
436
|
+
assert group.level is None and group.scene is None
|
|
437
|
+
assert 0 in disco
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
+
from helpers import LEGACY_ACK, wait_until
|
|
6
7
|
|
|
7
8
|
from zencontrol import ZenColour, ZenColourType, ZenEventMask, ZenEventMode
|
|
8
9
|
|
|
9
|
-
from helpers import wait_until
|
|
10
|
-
|
|
11
10
|
pytestmark = pytest.mark.simulator
|
|
12
11
|
|
|
13
12
|
|
|
@@ -73,13 +72,13 @@ async def test_arc_level_off_and_on(live_sim):
|
|
|
73
72
|
p = live_sim.protocol
|
|
74
73
|
addr = live_sim.ecg(1)
|
|
75
74
|
|
|
76
|
-
assert await p.dali_arc_level(addr, 77) is
|
|
75
|
+
assert await p.dali_arc_level(addr, 77) is LEGACY_ACK
|
|
77
76
|
assert await p.dali_query_level(addr) == 77
|
|
78
77
|
assert live_sim.world.lights[1].level == 77
|
|
79
78
|
|
|
80
|
-
assert await p.dali_off(addr) is
|
|
79
|
+
assert await p.dali_off(addr) is LEGACY_ACK
|
|
81
80
|
assert await p.dali_query_level(addr) == 0
|
|
82
|
-
assert await p.dali_on_step_up(addr) is
|
|
81
|
+
assert await p.dali_on_step_up(addr) is LEGACY_ACK
|
|
83
82
|
assert await p.dali_query_level(addr) == live_sim.world.lights[1].min_level
|
|
84
83
|
|
|
85
84
|
|
|
@@ -88,7 +87,7 @@ async def test_group_arc_and_mixed_level_query(live_sim):
|
|
|
88
87
|
p = live_sim.protocol
|
|
89
88
|
g0 = live_sim.group(0)
|
|
90
89
|
|
|
91
|
-
assert await p.dali_arc_level(g0, 40) is
|
|
90
|
+
assert await p.dali_arc_level(g0, 40) is LEGACY_ACK
|
|
92
91
|
assert await p.dali_query_level(g0) == 40
|
|
93
92
|
assert live_sim.world.lights[0].level == 40
|
|
94
93
|
assert live_sim.world.lights[1].level == 40
|
|
@@ -103,7 +102,7 @@ async def test_scene_recall_and_queries(live_sim):
|
|
|
103
102
|
p = live_sim.protocol
|
|
104
103
|
addr = live_sim.ecg(0)
|
|
105
104
|
|
|
106
|
-
assert await p.dali_scene(addr, 1) is
|
|
105
|
+
assert await p.dali_scene(addr, 1) is LEGACY_ACK
|
|
107
106
|
assert await p.dali_query_last_scene(addr) == 1
|
|
108
107
|
assert await p.dali_query_last_scene_is_current(addr) is True
|
|
109
108
|
assert await p.dali_query_level(addr) == 80
|
|
@@ -123,7 +122,7 @@ async def test_group_scene_recall(live_sim):
|
|
|
123
122
|
assert 0 in scenes and 1 in scenes
|
|
124
123
|
assert await p.query_scene_label_for_group(g0, 1) == "Relax"
|
|
125
124
|
|
|
126
|
-
assert await p.dali_scene(g0, 1) is
|
|
125
|
+
assert await p.dali_scene(g0, 1) is LEGACY_ACK
|
|
127
126
|
assert live_sim.world.groups[0].last_scene == 1
|
|
128
127
|
assert live_sim.world.lights[0].level == 80
|
|
129
128
|
assert live_sim.world.lights[1].level == 100
|
|
@@ -197,7 +196,7 @@ async def test_level_change_event_via_protocol(live_sim):
|
|
|
197
196
|
|
|
198
197
|
p.set_callbacks(level_change_callback=on_level)
|
|
199
198
|
await p.start_event_monitoring()
|
|
200
|
-
assert await p.dali_arc_level(live_sim.ecg(1), 55) is
|
|
199
|
+
assert await p.dali_arc_level(live_sim.ecg(1), 55) is LEGACY_ACK
|
|
201
200
|
await wait_until(
|
|
202
201
|
lambda: any(n == 1 and level == 55 for n, level in events),
|
|
203
202
|
message="expected level-change event for ECG 1 → 55",
|
|
@@ -288,19 +287,19 @@ async def test_step_up_down_and_step_down_off(live_sim):
|
|
|
288
287
|
addr = live_sim.ecg(1)
|
|
289
288
|
|
|
290
289
|
await p.dali_arc_level(addr, 10)
|
|
291
|
-
assert await p.dali_up(addr) is
|
|
290
|
+
assert await p.dali_up(addr) is LEGACY_ACK
|
|
292
291
|
assert await p.dali_query_level(addr) == 11
|
|
293
|
-
assert await p.dali_down(addr) is
|
|
292
|
+
assert await p.dali_down(addr) is LEGACY_ACK
|
|
294
293
|
assert await p.dali_query_level(addr) == 10
|
|
295
294
|
|
|
296
295
|
await p.dali_arc_level(addr, 1)
|
|
297
|
-
assert await p.dali_down(addr) is
|
|
296
|
+
assert await p.dali_down(addr) is LEGACY_ACK # stay at min
|
|
298
297
|
assert await p.dali_query_level(addr) == 1
|
|
299
|
-
assert await p.dali_step_down_off(addr) is
|
|
298
|
+
assert await p.dali_step_down_off(addr) is LEGACY_ACK
|
|
300
299
|
assert await p.dali_query_level(addr) == 0
|
|
301
300
|
|
|
302
301
|
# UP must not ignite from off
|
|
303
|
-
assert await p.dali_up(addr) is
|
|
302
|
+
assert await p.dali_up(addr) is LEGACY_ACK
|
|
304
303
|
assert await p.dali_query_level(addr) == 0
|
|
305
304
|
|
|
306
305
|
|
|
@@ -311,14 +310,14 @@ async def test_recall_max_min_and_last_active(live_sim):
|
|
|
311
310
|
live_sim.world.lights[1].max_level = 200
|
|
312
311
|
live_sim.world.lights[1].min_level = 5
|
|
313
312
|
|
|
314
|
-
assert await p.dali_recall_max(addr) is
|
|
313
|
+
assert await p.dali_recall_max(addr) is LEGACY_ACK
|
|
315
314
|
assert await p.dali_query_level(addr) == 200
|
|
316
|
-
assert await p.dali_recall_min(addr) is
|
|
315
|
+
assert await p.dali_recall_min(addr) is LEGACY_ACK
|
|
317
316
|
assert await p.dali_query_level(addr) == 5
|
|
318
317
|
|
|
319
318
|
await p.dali_arc_level(addr, 88)
|
|
320
319
|
await p.dali_off(addr)
|
|
321
|
-
assert await p.dali_go_to_last_active_level(addr) is
|
|
320
|
+
assert await p.dali_go_to_last_active_level(addr) is LEGACY_ACK
|
|
322
321
|
assert await p.dali_query_level(addr) == 88
|
|
323
322
|
|
|
324
323
|
|
|
@@ -425,7 +424,7 @@ async def test_colour_scenes_include_8_11(live_sim):
|
|
|
425
424
|
assert levels[8] == 160
|
|
426
425
|
assert levels[9] == 40
|
|
427
426
|
|
|
428
|
-
assert await p.dali_scene(addr, 8) is
|
|
427
|
+
assert await p.dali_scene(addr, 8) is LEGACY_ACK
|
|
429
428
|
assert await p.dali_query_level(addr) == 160
|
|
430
429
|
queried = await p.query_dali_colour(addr)
|
|
431
430
|
assert queried is not None and queried.kelvin == 4500
|
|
@@ -448,13 +447,13 @@ async def test_broadcast_arc_off_scene_and_colour(live_sim):
|
|
|
448
447
|
p = live_sim.protocol
|
|
449
448
|
bcast = live_sim.broadcast()
|
|
450
449
|
|
|
451
|
-
assert await p.dali_arc_level(bcast, 33) is
|
|
450
|
+
assert await p.dali_arc_level(bcast, 33) is LEGACY_ACK
|
|
452
451
|
assert all(lt.level == 33 for lt in live_sim.world.lights.values())
|
|
453
452
|
|
|
454
|
-
assert await p.dali_off(bcast) is
|
|
453
|
+
assert await p.dali_off(bcast) is LEGACY_ACK
|
|
455
454
|
assert all(lt.level == 0 for lt in live_sim.world.lights.values())
|
|
456
455
|
|
|
457
|
-
assert await p.dali_scene(bcast, 0) is
|
|
456
|
+
assert await p.dali_scene(bcast, 0) is LEGACY_ACK
|
|
458
457
|
assert live_sim.world.lights[0].level == 180
|
|
459
458
|
assert live_sim.world.groups[0].last_scene == 0
|
|
460
459
|
|
|
@@ -469,7 +468,7 @@ async def test_broadcast_arc_off_scene_and_colour(live_sim):
|
|
|
469
468
|
async def test_group_last_scene_and_status(live_sim):
|
|
470
469
|
p = live_sim.protocol
|
|
471
470
|
g0 = live_sim.group(0)
|
|
472
|
-
assert await p.dali_scene(g0, 1) is
|
|
471
|
+
assert await p.dali_scene(g0, 1) is LEGACY_ACK
|
|
473
472
|
assert await p.dali_query_last_scene(g0) == 1
|
|
474
473
|
assert await p.dali_query_last_scene_is_current(g0) is True
|
|
475
474
|
|
|
@@ -540,9 +539,7 @@ async def test_readiness_flags_and_unknown_sysvars(live_sim):
|
|
|
540
539
|
live_sim.world.startup_complete = False
|
|
541
540
|
assert await p.query_controller_startup_complete(c) is not True
|
|
542
541
|
live_sim.world.startup_complete = True
|
|
543
|
-
live_sim.world.dali_ready = False
|
|
544
|
-
assert await p.query_is_dali_ready(c) is not True
|
|
545
|
-
live_sim.world.dali_ready = True
|
|
542
|
+
live_sim.world.dali_ready = False # simulator ignores — DALI always ready
|
|
546
543
|
assert await p.query_is_dali_ready(c) is True
|
|
547
544
|
|
|
548
545
|
assert await p.query_system_variable_name(c, 99) is None
|
|
@@ -557,3 +554,84 @@ async def test_return_to_scheduled_profile(live_sim):
|
|
|
557
554
|
assert await p.change_profile_number(c, 3) is True
|
|
558
555
|
assert await p.return_to_scheduled_profile(c) is True
|
|
559
556
|
assert await p.query_current_profile_number(c) == 1
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@pytest.mark.asyncio
|
|
560
|
+
async def test_dali_query_fade_running(live_sim):
|
|
561
|
+
p = live_sim.protocol
|
|
562
|
+
addr = live_sim.ecg(1)
|
|
563
|
+
await p.dali_arc_level(addr, 0)
|
|
564
|
+
assert await p.dali_query_fade_running(addr) is False
|
|
565
|
+
assert await p.dali_custom_fade(addr, 100, 5) is True
|
|
566
|
+
p.cache.clear()
|
|
567
|
+
assert await p.dali_query_fade_running(addr) is True
|
|
568
|
+
assert await p.dali_stop_fade(addr) is True
|
|
569
|
+
p.cache.clear()
|
|
570
|
+
assert await p.dali_query_fade_running(addr) is False
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@pytest.mark.asyncio
|
|
574
|
+
async def test_operating_mode_and_button_led_stubs(live_sim):
|
|
575
|
+
p = live_sim.protocol
|
|
576
|
+
assert await p.query_operating_mode_by_address(live_sim.ecg(0)) == 0
|
|
577
|
+
assert await p.query_operating_mode_by_address(live_sim.ecd(0)) == 0
|
|
578
|
+
|
|
579
|
+
button = live_sim.instance(0, 0)
|
|
580
|
+
assert await p.override_dali_button_led_state(button, True) is True
|
|
581
|
+
# Sim stub always reports last-known LED off.
|
|
582
|
+
assert await p.query_last_known_dali_button_led_state(button) is False
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
@pytest.mark.asyncio
|
|
586
|
+
async def test_query_instance_groups(live_sim):
|
|
587
|
+
p = live_sim.protocol
|
|
588
|
+
groups = await p.query_instance_groups(live_sim.instance(0, 0))
|
|
589
|
+
assert groups == (0, 1, None)
|
|
590
|
+
motion = await p.query_instance_groups(live_sim.instance(0, 2, type_code=3))
|
|
591
|
+
assert motion == (0, None, None)
|
|
592
|
+
# Unconfigured instance → all None
|
|
593
|
+
unset = await p.query_instance_groups(live_sim.instance(1, 0))
|
|
594
|
+
assert unset == (None, None, None)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
@pytest.mark.asyncio
|
|
598
|
+
async def test_query_profile_information(live_sim):
|
|
599
|
+
"""PDF: header + records; bit0=disabled, bits1–2=priority (0 scheduled, 1+, …)."""
|
|
600
|
+
p, c = live_sim.protocol, live_sim.controller
|
|
601
|
+
result = await p.query_profile_information(c)
|
|
602
|
+
assert result is not None
|
|
603
|
+
state, profiles = result
|
|
604
|
+
assert state["current_active_profile"] == 1
|
|
605
|
+
assert state["last_scheduled_profile"] == 1
|
|
606
|
+
assert state["last_overridden_profile_utc"].timestamp() == 0x22334455
|
|
607
|
+
assert state["last_scheduled_profile_utc"].timestamp() == 0x44556677
|
|
608
|
+
assert set(profiles) == {1, 2, 3}
|
|
609
|
+
# Assert protocol fields (enabled/priority); labels are library niceties.
|
|
610
|
+
assert profiles[1]["enabled"] is True and profiles[1]["priority"] == 0
|
|
611
|
+
assert profiles[2]["enabled"] is True and profiles[2]["priority"] == 1
|
|
612
|
+
assert profiles[3]["enabled"] is False and profiles[3]["priority"] == 0
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
@pytest.mark.asyncio
|
|
616
|
+
async def test_colour_only_preserves_level(live_sim):
|
|
617
|
+
p = live_sim.protocol
|
|
618
|
+
addr = live_sim.ecg(0)
|
|
619
|
+
assert await p.dali_arc_level(addr, 77) is LEGACY_ACK
|
|
620
|
+
colour = ZenColour(type=ZenColourType.TC, kelvin=4200)
|
|
621
|
+
assert await p.dali_colour(addr, colour, level=255) is True
|
|
622
|
+
assert await p.dali_query_level(addr) == 77
|
|
623
|
+
assert live_sim.world.lights[0].level == 77
|
|
624
|
+
queried = await p.query_dali_colour(addr)
|
|
625
|
+
assert queried is not None and queried.kelvin == 4200
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@pytest.mark.asyncio
|
|
629
|
+
async def test_clear_tpi_event_unicast_address(live_sim):
|
|
630
|
+
"""Sim extension: omit IP/port (zeros) clears unicast targeting."""
|
|
631
|
+
p, c = live_sim.protocol, live_sim.controller
|
|
632
|
+
await p.set_tpi_event_unicast_address(c, ipaddr="127.0.0.1", port=6970)
|
|
633
|
+
await p.set_tpi_event_unicast_address(c)
|
|
634
|
+
info = await p.query_tpi_event_unicast_address(c)
|
|
635
|
+
assert info is not None
|
|
636
|
+
assert info["port"] == 0
|
|
637
|
+
assert info["ip"] == "0.0.0.0"
|