zencontrol-python 0.1.2__tar.gz → 0.1.4__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.2 → zencontrol_python-0.1.4}/PKG-INFO +28 -16
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/README.md +25 -11
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/dump_simulator_config.py +7 -22
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/interface_events.py +2 -3
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/mqtt_bridge.py +7 -7
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/pyproject.toml +11 -6
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/tests/test_lifecycle.py +28 -1
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/__init__.py +1 -1
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/api/models.py +118 -93
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/api/protocol.py +146 -130
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/api/types.py +3 -3
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/exceptions.py +2 -3
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/interface/interface.py +232 -175
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/io/command.py +23 -23
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/io/event.py +13 -14
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/utils.py +4 -2
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol_python.egg-info/PKG-INFO +28 -16
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/LICENSE +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/__init__.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/controller.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/devices.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/events.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/gear.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/groups.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/instances.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/interface.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/leds.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/level_change_v2.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/multicast.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/multicast_raw.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/profiles.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/tpi_event_listener.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/examples/live/variables.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/setup.cfg +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/tests/test_discovery.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/tests/test_entity_scope.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/tests/test_event_listener_disconnect.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/tests/test_reconnect.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/tests/test_sim_events.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/tests/test_sim_interface.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/tests/test_sim_protocol.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/tests/test_sub_labels.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/tests/test_wire_resilience.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/api/__init__.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/interface/__init__.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/io/__init__.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol/py.typed +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol_python.egg-info/SOURCES.txt +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol_python.egg-info/dependency_links.txt +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol_python.egg-info/entry_points.txt +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/zencontrol_python.egg-info/requires.txt +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.4}/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.4
|
|
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
|
|
@@ -12,13 +12,11 @@ Classifier: Development Status :: 3 - Alpha
|
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
16
|
Classifier: Topic :: Home Automation
|
|
19
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
18
|
Classifier: Typing :: Typed
|
|
21
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.14
|
|
22
20
|
Description-Content-Type: text/markdown
|
|
23
21
|
License-File: LICENSE
|
|
24
22
|
Requires-Dist: colorama>=0.4.6
|
|
@@ -46,9 +44,33 @@ This library has now undergone validation in multiple environments. There is an
|
|
|
46
44
|
[zencontrol-simulator](https://github.com/sjwright/zencontrol-simulator), a nearly feature-complete simulator of zencontrol hardware.
|
|
47
45
|
A practical demonstration is [zencontrol-homeassistant](https://github.com/sjwright/zencontrol-homeassistant), a comprehensive Home Assistant integration.
|
|
48
46
|
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
Beyond basic lighting control, this library supports:
|
|
50
|
+
|
|
51
|
+
* **Broad command surface** — inhibit, custom fade, step/up/down helpers, colour scene membership queries, EAN/serial, and most related TPI Advanced commands
|
|
52
|
+
* **Object-based entity model** — Optional. Expresses lights, groups, profiles, buttons, motion sensors, and system variables as rich objects with interview/discovery helpers
|
|
53
|
+
* **UDP transport resilience** — request retries and queue-failure backoff
|
|
54
|
+
* **Multicast controller discovery** — find controllers on the LAN without a preconfigured host
|
|
55
|
+
* **Button events** — discovery of control-device button instances, plus press and long-press event callbacks
|
|
56
|
+
* **Event filtering** — configure which TPI events the controller emits
|
|
57
|
+
* **System variables** — labelled SV discovery, read/write, and change events
|
|
58
|
+
* **Profiles** — query, change, and return to the scheduled profile
|
|
59
|
+
* **Simulator-backed tests** — protocol path exercised against [zencontrol-simulator](https://github.com/sjwright/zencontrol-simulator)
|
|
60
|
+
|
|
61
|
+
## Known limitations
|
|
62
|
+
|
|
63
|
+
* RGB+ and XY colour commands have not been tested with hardware
|
|
64
|
+
* Numerical (absolute) instances have not been tested with hardware
|
|
65
|
+
|
|
66
|
+
## Out of scope
|
|
67
|
+
|
|
68
|
+
* Any commands involving DMX, Control4, or virtual instances (I don't have licenses for any of these so I couldn't test them even if I wanted to, but the scaffolding is there if anyone wishes to add support)
|
|
69
|
+
* Any commands described in the documentation as "legacy" (they aren't useful)
|
|
70
|
+
|
|
49
71
|
## Requirements
|
|
50
72
|
|
|
51
|
-
* Python 3.
|
|
73
|
+
* Python 3.14 (or later)
|
|
52
74
|
* Controller firmware 2.2.130 or later is strongly recommended (minimum 2.2.11 required)
|
|
53
75
|
|
|
54
76
|
## Install
|
|
@@ -70,16 +92,6 @@ pytest -m "not simulator"
|
|
|
70
92
|
pytest
|
|
71
93
|
```
|
|
72
94
|
|
|
73
|
-
## Limitations
|
|
74
|
-
|
|
75
|
-
* RGB+ and XY colour commands are not tested (I don't have any compatible lights)
|
|
76
|
-
* Numerical (absolute) instances are not tested (I don't have any such ECDs)
|
|
77
|
-
|
|
78
|
-
## Out of scope
|
|
79
|
-
|
|
80
|
-
* Any commands involving DMX, Control4, or virtual instances (I don't have licenses for any of these so I couldn't test them even if I wanted to, but the scaffolding is there if anyone wishes to add support)
|
|
81
|
-
* Any commands described in the documentation as "legacy" (they aren't useful)
|
|
82
|
-
|
|
83
95
|
## TPI Advanced wishlist
|
|
84
96
|
|
|
85
97
|
* Command to return a controller's MAC address used for multicast packets _(There are other ways to get or infer the MAC access, but they're unreliable.)_
|
|
@@ -10,9 +10,33 @@ This library has now undergone validation in multiple environments. There is an
|
|
|
10
10
|
[zencontrol-simulator](https://github.com/sjwright/zencontrol-simulator), a nearly feature-complete simulator of zencontrol hardware.
|
|
11
11
|
A practical demonstration is [zencontrol-homeassistant](https://github.com/sjwright/zencontrol-homeassistant), a comprehensive Home Assistant integration.
|
|
12
12
|
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
Beyond basic lighting control, this library supports:
|
|
16
|
+
|
|
17
|
+
* **Broad command surface** — inhibit, custom fade, step/up/down helpers, colour scene membership queries, EAN/serial, and most related TPI Advanced commands
|
|
18
|
+
* **Object-based entity model** — Optional. Expresses lights, groups, profiles, buttons, motion sensors, and system variables as rich objects with interview/discovery helpers
|
|
19
|
+
* **UDP transport resilience** — request retries and queue-failure backoff
|
|
20
|
+
* **Multicast controller discovery** — find controllers on the LAN without a preconfigured host
|
|
21
|
+
* **Button events** — discovery of control-device button instances, plus press and long-press event callbacks
|
|
22
|
+
* **Event filtering** — configure which TPI events the controller emits
|
|
23
|
+
* **System variables** — labelled SV discovery, read/write, and change events
|
|
24
|
+
* **Profiles** — query, change, and return to the scheduled profile
|
|
25
|
+
* **Simulator-backed tests** — protocol path exercised against [zencontrol-simulator](https://github.com/sjwright/zencontrol-simulator)
|
|
26
|
+
|
|
27
|
+
## Known limitations
|
|
28
|
+
|
|
29
|
+
* RGB+ and XY colour commands have not been tested with hardware
|
|
30
|
+
* Numerical (absolute) instances have not been tested with hardware
|
|
31
|
+
|
|
32
|
+
## Out of scope
|
|
33
|
+
|
|
34
|
+
* Any commands involving DMX, Control4, or virtual instances (I don't have licenses for any of these so I couldn't test them even if I wanted to, but the scaffolding is there if anyone wishes to add support)
|
|
35
|
+
* Any commands described in the documentation as "legacy" (they aren't useful)
|
|
36
|
+
|
|
13
37
|
## Requirements
|
|
14
38
|
|
|
15
|
-
* Python 3.
|
|
39
|
+
* Python 3.14 (or later)
|
|
16
40
|
* Controller firmware 2.2.130 or later is strongly recommended (minimum 2.2.11 required)
|
|
17
41
|
|
|
18
42
|
## Install
|
|
@@ -34,16 +58,6 @@ pytest -m "not simulator"
|
|
|
34
58
|
pytest
|
|
35
59
|
```
|
|
36
60
|
|
|
37
|
-
## Limitations
|
|
38
|
-
|
|
39
|
-
* RGB+ and XY colour commands are not tested (I don't have any compatible lights)
|
|
40
|
-
* Numerical (absolute) instances are not tested (I don't have any such ECDs)
|
|
41
|
-
|
|
42
|
-
## Out of scope
|
|
43
|
-
|
|
44
|
-
* Any commands involving DMX, Control4, or virtual instances (I don't have licenses for any of these so I couldn't test them even if I wanted to, but the scaffolding is there if anyone wishes to add support)
|
|
45
|
-
* Any commands described in the documentation as "legacy" (they aren't useful)
|
|
46
|
-
|
|
47
61
|
## TPI Advanced wishlist
|
|
48
62
|
|
|
49
63
|
* Command to return a controller's MAC address used for multicast packets _(There are other ways to get or infer the MAC access, but they're unreliable.)_
|
|
@@ -11,14 +11,12 @@ Example:
|
|
|
11
11
|
-o ../zencontrol-simulator/config.from-live.yaml
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
14
|
import argparse
|
|
17
15
|
import asyncio
|
|
18
16
|
import logging
|
|
19
17
|
import sys
|
|
20
18
|
from pathlib import Path
|
|
21
|
-
from typing import Any
|
|
19
|
+
from typing import Any
|
|
22
20
|
|
|
23
21
|
import yaml
|
|
24
22
|
|
|
@@ -42,12 +40,10 @@ INSTANCE_TYPE_NAMES = {
|
|
|
42
40
|
ZenInstanceType.GENERAL_SENSOR: "general_sensor",
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
|
|
46
43
|
def _hex_int(value: int) -> str:
|
|
47
44
|
return f"0x{value:X}"
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
def _colour_dict(colour: Optional[ZenColour]) -> Optional[dict[str, Any]]:
|
|
46
|
+
def _colour_dict(colour: ZenColour | None) -> dict[str, Any] | None:
|
|
51
47
|
if colour is None or colour.type is None:
|
|
52
48
|
return None
|
|
53
49
|
if colour.type == ZenColourType.TC:
|
|
@@ -66,32 +62,28 @@ def _colour_dict(colour: Optional[ZenColour]) -> Optional[dict[str, Any]]:
|
|
|
66
62
|
return {"type": "xy", "x": colour.x, "y": colour.y}
|
|
67
63
|
return None
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
out: list[Optional[int]] = [None] * 12
|
|
65
|
+
def _scene_levels(levels: list[int | None] | None) -> list[int | None]:
|
|
66
|
+
out: list[int | None] = [None] * 12
|
|
72
67
|
if not levels:
|
|
73
68
|
return out
|
|
74
69
|
for i, level in enumerate(levels[:12]):
|
|
75
70
|
out[i] = None if level is None else int(level)
|
|
76
71
|
return out
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
out: list[Optional[dict[str, Any]]] = [None] * 12
|
|
73
|
+
def _scene_colours(colours: list[ZenColour | None] | None) -> list[dict[str, Any] | None]:
|
|
74
|
+
out: list[dict[str, Any] | None] = [None] * 12
|
|
81
75
|
if not colours:
|
|
82
76
|
return out
|
|
83
77
|
for i, colour in enumerate(colours[:12]):
|
|
84
78
|
out[i] = _colour_dict(colour)
|
|
85
79
|
return out
|
|
86
80
|
|
|
87
|
-
|
|
88
|
-
async def _raw_byte(tpi: ZenProtocol, controller: ZenController, command: int, address: int = 0) -> Optional[int]:
|
|
81
|
+
async def _raw_byte(tpi: ZenProtocol, controller: ZenController, command: int, address: int = 0) -> int | None:
|
|
89
82
|
response = await tpi._send_basic(controller, command, address)
|
|
90
83
|
if response and len(response) >= 1:
|
|
91
84
|
return int(response[0])
|
|
92
85
|
return None
|
|
93
86
|
|
|
94
|
-
|
|
95
87
|
async def dump_controller(tpi: ZenProtocol, controller: ZenController) -> dict[str, Any]:
|
|
96
88
|
LOGGER.info("Querying controller %s (%s:%s)", controller.mac, controller.host, controller.port)
|
|
97
89
|
|
|
@@ -343,15 +335,12 @@ async def dump_controller(tpi: ZenProtocol, controller: ZenController) -> dict[s
|
|
|
343
335
|
|
|
344
336
|
return world
|
|
345
337
|
|
|
346
|
-
|
|
347
338
|
class _HexInt(int):
|
|
348
339
|
"""YAML representable int that dumps as 0x…."""
|
|
349
340
|
|
|
350
|
-
|
|
351
341
|
def _represent_hex_int(dumper: yaml.Dumper, data: _HexInt) -> Any:
|
|
352
342
|
return dumper.represent_scalar("tag:yaml.org,2002:int", f"0x{int(data):X}")
|
|
353
343
|
|
|
354
|
-
|
|
355
344
|
def _prepare_for_yaml(obj: Any) -> Any:
|
|
356
345
|
"""Convert serial hex strings back to ints tagged for hex dump where useful."""
|
|
357
346
|
if isinstance(obj, dict):
|
|
@@ -370,7 +359,6 @@ def _prepare_for_yaml(obj: Any) -> Any:
|
|
|
370
359
|
return [_prepare_for_yaml(x) for x in obj]
|
|
371
360
|
return obj
|
|
372
361
|
|
|
373
|
-
|
|
374
362
|
def write_yaml(path: Path, world: dict[str, Any]) -> None:
|
|
375
363
|
class Dumper(yaml.SafeDumper):
|
|
376
364
|
pass
|
|
@@ -395,7 +383,6 @@ def write_yaml(path: Path, world: dict[str, Any]) -> None:
|
|
|
395
383
|
width=100,
|
|
396
384
|
)
|
|
397
385
|
|
|
398
|
-
|
|
399
386
|
def build_parser() -> argparse.ArgumentParser:
|
|
400
387
|
example_dir = Path(__file__).resolve().parent
|
|
401
388
|
default_config = example_dir / "config.yaml"
|
|
@@ -431,7 +418,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
431
418
|
)
|
|
432
419
|
return parser
|
|
433
420
|
|
|
434
|
-
|
|
435
421
|
async def main(argv: list[str] | None = None) -> None:
|
|
436
422
|
args = build_parser().parse_args(argv)
|
|
437
423
|
logging.basicConfig(
|
|
@@ -484,6 +470,5 @@ async def main(argv: list[str] | None = None) -> None:
|
|
|
484
470
|
len(world["system_variables"]),
|
|
485
471
|
)
|
|
486
472
|
|
|
487
|
-
|
|
488
473
|
if __name__ == "__main__":
|
|
489
474
|
run_with_keyboard_interrupt(main)
|
|
@@ -3,7 +3,6 @@ from zencontrol import ZenControl, ZenProfile, ZenGroup, ZenLight, ZenButton, Ze
|
|
|
3
3
|
import yaml
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
import time
|
|
6
|
-
from typing import Optional
|
|
7
6
|
|
|
8
7
|
async def main():
|
|
9
8
|
config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
|
|
@@ -21,11 +20,11 @@ async def main():
|
|
|
21
20
|
ms()
|
|
22
21
|
print(f"Profile Change Event - {profile}")
|
|
23
22
|
|
|
24
|
-
async def _zen_group_change(group: ZenGroup, level:
|
|
23
|
+
async def _zen_group_change(group: ZenGroup, level: int | None = None, colour: ZenColour | None = None, scene: int | None = None, discoordinated: bool = False) -> None:
|
|
25
24
|
ms()
|
|
26
25
|
print(f"Group Change Event - {group} level {level} colour {colour} scene {scene} {'discoordinated' if discoordinated else ''}")
|
|
27
26
|
|
|
28
|
-
async def _zen_light_change(light: ZenLight, level:
|
|
27
|
+
async def _zen_light_change(light: ZenLight, level: int | None = None, colour: ZenColour | None = None, scene: int | None = None) -> None:
|
|
29
28
|
ms()
|
|
30
29
|
print(f"Light Change Event - {light} level {level} colour {colour} scene {scene}")
|
|
31
30
|
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import yaml
|
|
6
6
|
import re
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Any
|
|
9
9
|
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
|
|
@@ -814,9 +814,9 @@ class ZenMQTTBridge:
|
|
|
814
814
|
async def _mqtt_light_change(self, light: ZenLight|ZenGroup, payload: dict[str, Any]) -> None:
|
|
815
815
|
addr = light.address
|
|
816
816
|
ctrl = addr.controller
|
|
817
|
-
state:
|
|
818
|
-
brightness:
|
|
819
|
-
mireds:
|
|
817
|
+
state: str | None = payload.get("state", None)
|
|
818
|
+
brightness: int | None = payload.get("brightness", None)
|
|
819
|
+
mireds: int | None = payload.get("color_temp", None)
|
|
820
820
|
|
|
821
821
|
# If brightness or temperature is set
|
|
822
822
|
if brightness is not None or mireds is not None:
|
|
@@ -835,7 +835,7 @@ class ZenMQTTBridge:
|
|
|
835
835
|
self.logger.info(f"♥️💡 Command from HA: {ctrl.name} turning gear {addr.number} ON")
|
|
836
836
|
await light.on()
|
|
837
837
|
|
|
838
|
-
async def _zen_light_change(self, light: ZenLight, level:
|
|
838
|
+
async def _zen_light_change(self, light: ZenLight, level: int | None = None, colour: ZenColour | None = None, scene: int | None = None) -> None:
|
|
839
839
|
typestr = "group" if light.address.type == ZenAddressType.GROUP else "light"
|
|
840
840
|
emoji = "👥" if light.address.type == ZenAddressType.GROUP else "💡"
|
|
841
841
|
self.logger.info(f"🩵{emoji} Event from Zen: {typestr} {light.address.number} level {level if level is not None else '--'} colour {colour if colour is not None else '--'} scene {scene if scene is not None else '--'}")
|
|
@@ -930,7 +930,7 @@ class ZenMQTTBridge:
|
|
|
930
930
|
|
|
931
931
|
# mqtt group light change calls _mqtt_light_change
|
|
932
932
|
|
|
933
|
-
async def _zen_group_change(self, group: ZenGroup, level:
|
|
933
|
+
async def _zen_group_change(self, group: ZenGroup, level: int | None = None, colour: ZenColour | None = None, scene: int | None = None, discoordinated: bool | None = None) -> None:
|
|
934
934
|
select_mqtt_topic = group.client_data.get("select", {}).get('mqtt_topic', None)
|
|
935
935
|
|
|
936
936
|
# Get the scene label for the ID from the group
|
|
@@ -1109,4 +1109,4 @@ async def main():
|
|
|
1109
1109
|
await bridge.stop()
|
|
1110
1110
|
|
|
1111
1111
|
if __name__ == "__main__":
|
|
1112
|
-
asyncio.run(main())
|
|
1112
|
+
asyncio.run(main())
|
|
@@ -7,7 +7,7 @@ name = "zencontrol-python"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "Python implementation of the Zencontrol TPI Advanced protocol for DALI lighting controllers."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [
|
|
@@ -19,9 +19,7 @@ classifiers = [
|
|
|
19
19
|
"Intended Audience :: Developers",
|
|
20
20
|
"Operating System :: OS Independent",
|
|
21
21
|
"Programming Language :: Python :: 3",
|
|
22
|
-
"Programming Language :: Python :: 3.
|
|
23
|
-
"Programming Language :: Python :: 3.12",
|
|
24
|
-
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
25
23
|
"Topic :: Home Automation",
|
|
26
24
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
25
|
"Typing :: Typed",
|
|
@@ -67,7 +65,7 @@ zencontrol = ["py.typed"]
|
|
|
67
65
|
|
|
68
66
|
[tool.black]
|
|
69
67
|
line-length = 88
|
|
70
|
-
target-version = ['
|
|
68
|
+
target-version = ['py314']
|
|
71
69
|
include = '\.pyi?$'
|
|
72
70
|
extend-exclude = '''
|
|
73
71
|
/(
|
|
@@ -84,7 +82,7 @@ extend-exclude = '''
|
|
|
84
82
|
'''
|
|
85
83
|
|
|
86
84
|
[tool.mypy]
|
|
87
|
-
python_version = "3.
|
|
85
|
+
python_version = "3.14"
|
|
88
86
|
warn_return_any = true
|
|
89
87
|
warn_unused_configs = true
|
|
90
88
|
disallow_untyped_defs = true
|
|
@@ -108,3 +106,10 @@ asyncio_mode = "auto"
|
|
|
108
106
|
markers = [
|
|
109
107
|
"simulator: integration tests that start zencontrol-simulator (installed or sibling ../zencontrol-simulator checkout)",
|
|
110
108
|
]
|
|
109
|
+
|
|
110
|
+
[tool.ruff]
|
|
111
|
+
target-version = "py314"
|
|
112
|
+
line-length = 88
|
|
113
|
+
|
|
114
|
+
[tool.ruff.lint]
|
|
115
|
+
select = ["ASYNC", "B", "E", "F", "I", "UP"]
|
|
@@ -11,7 +11,6 @@ from zencontrol.api.models import ZenAddress
|
|
|
11
11
|
from zencontrol.api.types import ZenAddressType
|
|
12
12
|
from zencontrol.interface.interface import (
|
|
13
13
|
ZenControl,
|
|
14
|
-
ZenController,
|
|
15
14
|
ZenLight,
|
|
16
15
|
)
|
|
17
16
|
|
|
@@ -24,6 +23,34 @@ async def test_zencontrol_async_context_manager_calls_aclose() -> None:
|
|
|
24
23
|
aclose.assert_awaited_once()
|
|
25
24
|
|
|
26
25
|
|
|
26
|
+
@pytest.mark.asyncio
|
|
27
|
+
async def test_remove_controller_closes_client_and_purges_cache() -> None:
|
|
28
|
+
zen = ZenControl()
|
|
29
|
+
ctrl_a = zen.add_controller(
|
|
30
|
+
id=1, name="ctrl-a", label="A", host="127.0.0.1", port=5108
|
|
31
|
+
)
|
|
32
|
+
ctrl_b = zen.add_controller(
|
|
33
|
+
id=2, name="ctrl-b", label="B", host="127.0.0.2", port=5108
|
|
34
|
+
)
|
|
35
|
+
fake_client = MagicMock()
|
|
36
|
+
fake_client.close = AsyncMock()
|
|
37
|
+
ctrl_a.client = fake_client
|
|
38
|
+
address = ZenAddress(controller=ctrl_a, type=ZenAddressType.ECG, number=1)
|
|
39
|
+
ZenLight(protocol=zen.protocol, address=address)
|
|
40
|
+
assert "ctrl-a 1" in zen.protocol.entity_registry.lights
|
|
41
|
+
|
|
42
|
+
await zen.remove_controller(ctrl_a)
|
|
43
|
+
|
|
44
|
+
fake_client.close.assert_awaited()
|
|
45
|
+
assert ctrl_a.client is None
|
|
46
|
+
assert zen.controllers == [ctrl_b]
|
|
47
|
+
assert "ctrl-a" not in zen.protocol.entity_registry.controllers
|
|
48
|
+
assert "ctrl-a 1" not in zen.protocol.entity_registry.lights
|
|
49
|
+
assert "ctrl-b" in zen.protocol.entity_registry.controllers
|
|
50
|
+
|
|
51
|
+
await zen.aclose()
|
|
52
|
+
|
|
53
|
+
|
|
27
54
|
@pytest.mark.asyncio
|
|
28
55
|
async def test_aclose_closes_clients_and_clears_instances() -> None:
|
|
29
56
|
zen = ZenControl()
|
|
@@ -58,7 +58,7 @@ from .exceptions import ZenError, ZenTimeoutError, ZenResponseError, ZenConnecti
|
|
|
58
58
|
# Utilities
|
|
59
59
|
from .utils import run_with_keyboard_interrupt
|
|
60
60
|
|
|
61
|
-
__version__ = "0.1.
|
|
61
|
+
__version__ = "0.1.4"
|
|
62
62
|
__author__ = "Simon Wright"
|
|
63
63
|
|
|
64
64
|
# Public API - these are the main classes users should import
|