zencontrol-python 0.1.1__tar.gz → 0.1.3__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.
Files changed (51) hide show
  1. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/PKG-INFO +29 -18
  2. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/README.md +26 -13
  3. zencontrol_python-0.1.1/tests/test_controller.py → zencontrol_python-0.1.3/examples/live/controller.py +2 -1
  4. zencontrol_python-0.1.1/tests/test_devices.py → zencontrol_python-0.1.3/examples/live/devices.py +2 -1
  5. zencontrol_python-0.1.1/tests/test_events.py → zencontrol_python-0.1.3/examples/live/events.py +1 -1
  6. zencontrol_python-0.1.1/tests/test_gear.py → zencontrol_python-0.1.3/examples/live/gear.py +2 -1
  7. zencontrol_python-0.1.1/tests/test_groups.py → zencontrol_python-0.1.3/examples/live/groups.py +2 -1
  8. zencontrol_python-0.1.1/tests/test_instances.py → zencontrol_python-0.1.3/examples/live/instances.py +2 -1
  9. zencontrol_python-0.1.1/tests/test_interface.py → zencontrol_python-0.1.3/examples/live/interface.py +2 -1
  10. zencontrol_python-0.1.1/tests/test_interface_events.py → zencontrol_python-0.1.3/examples/live/interface_events.py +4 -4
  11. zencontrol_python-0.1.1/tests/test_leds.py → zencontrol_python-0.1.3/examples/live/leds.py +2 -1
  12. zencontrol_python-0.1.1/tests/test_level_change_v2.py → zencontrol_python-0.1.3/examples/live/level_change_v2.py +1 -1
  13. zencontrol_python-0.1.1/tests/test_multicast.py → zencontrol_python-0.1.3/examples/live/multicast.py +2 -1
  14. zencontrol_python-0.1.1/tests/test_profiles.py → zencontrol_python-0.1.3/examples/live/profiles.py +2 -1
  15. zencontrol_python-0.1.1/tests/test_variables.py → zencontrol_python-0.1.3/examples/live/variables.py +2 -1
  16. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/examples/mqtt_bridge.py +7 -37
  17. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/pyproject.toml +17 -6
  18. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/tests/test_entity_scope.py +7 -0
  19. zencontrol_python-0.1.3/tests/test_sim_events.py +206 -0
  20. zencontrol_python-0.1.3/tests/test_sim_interface.py +341 -0
  21. zencontrol_python-0.1.3/tests/test_sim_protocol.py +524 -0
  22. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/tests/test_wire_resilience.py +118 -0
  23. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/__init__.py +4 -2
  24. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/api/models.py +127 -98
  25. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/api/protocol.py +199 -150
  26. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/api/types.py +8 -6
  27. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/exceptions.py +13 -2
  28. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/interface/interface.py +177 -159
  29. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/io/command.py +82 -58
  30. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/io/event.py +13 -14
  31. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/utils.py +4 -2
  32. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol_python.egg-info/PKG-INFO +29 -18
  33. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol_python.egg-info/SOURCES.txt +18 -15
  34. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/LICENSE +0 -0
  35. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/examples/__init__.py +0 -0
  36. /zencontrol_python-0.1.1/tests/test_multicast_raw.py → /zencontrol_python-0.1.3/examples/live/multicast_raw.py +0 -0
  37. /zencontrol_python-0.1.1/tests/test_tpi_event_listener.py → /zencontrol_python-0.1.3/examples/live/tpi_event_listener.py +0 -0
  38. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/setup.cfg +0 -0
  39. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/tests/test_discovery.py +0 -0
  40. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/tests/test_event_listener_disconnect.py +0 -0
  41. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/tests/test_lifecycle.py +0 -0
  42. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/tests/test_reconnect.py +0 -0
  43. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/tests/test_sub_labels.py +0 -0
  44. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/api/__init__.py +0 -0
  45. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/interface/__init__.py +0 -0
  46. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/io/__init__.py +0 -0
  47. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol/py.typed +0 -0
  48. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol_python.egg-info/dependency_links.txt +0 -0
  49. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol_python.egg-info/entry_points.txt +0 -0
  50. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/zencontrol_python.egg-info/requires.txt +0 -0
  51. {zencontrol_python-0.1.1 → zencontrol_python-0.1.3}/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.1
3
+ Version: 0.1.3
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.11
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.11
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
@@ -40,33 +38,46 @@ This is an implementation of the **Zencontrol TPI Advanced** protocol, written i
40
38
 
41
39
  - zencontrol.io: Implementation of the raw TPI Advanced UDP packet specification;
42
40
  - zencontrol.api: Implementation of most TPI Advanced API commands and events;
43
- - zencontrol.interface: An opinionated abstraction layer suitable for integration into smart building control software. It provides methods, objects, and callbacks for managing lights, groups, profiles, buttons, motion sensors, and system variables. This code is still undergoing significant refinement.
41
+ - zencontrol.interface: An opinionated abstraction layer suitable for integration into smart building control software. It provides methods, objects, and callbacks for managing lights, groups, profiles, buttons, motion sensors, and system variables.
44
42
 
45
- Built on top of this is an example MQTT bridge for Home Assistant. See [examples/mqtt_bridge.md](examples/mqtt_bridge.md).
43
+ This library has now undergone validation in multiple environments. There is an extensive test suite, some of which uses
44
+ [zencontrol-simulator](https://github.com/sjwright/zencontrol-simulator), a nearly feature-complete simulator of zencontrol hardware.
45
+ A practical demonstration is [zencontrol-homeassistant](https://github.com/sjwright/zencontrol-homeassistant), a comprehensive Home Assistant integration.
46
46
 
47
47
  ## Requirements
48
48
 
49
- * Python 3.11 (or later)
50
- * Controller firmware 2.2.11 (or later)
49
+ * Python 3.14 (or later)
50
+ * Controller firmware 2.2.130 or later is strongly recommended (minimum 2.2.11 required)
51
51
 
52
52
  ## Install
53
53
 
54
- Refer to zencontrol-tpi project for now.
55
- For integrators, the library is also published on PyPI.
54
+ This library is available on PyPI.
55
+
56
+ ## Testing
57
+
58
+ Integration tests start [zencontrol-simulator](https://github.com/sjwright/zencontrol-simulator) on an ephemeral local port and exercise a real UDP TPI protocol path. Either install the simulator, or check it out as a sibling directory (`../zencontrol-simulator`); tests will pick it up automatically. Note that PyYAML is a simulator dependency.
59
+
60
+ ```bash
61
+ pip install -e ".[dev]"
62
+ pip install PyYAML
63
+ # optional if not using a sibling checkout:
64
+ # pip install -e ../zencontrol-simulator
65
+ pytest -m simulator
66
+ pytest -m "not simulator"
67
+ # or run everything:
68
+ pytest
69
+ ```
56
70
 
57
71
  ## Limitations
58
72
 
59
- Implemented but untested:
60
-
61
- * Dealing with multiple controllers (I only have one controller)
62
- * RGB+ and XY colour commands (I don't have any compatible lights)
63
- * Numerical (absolute) instances (I don't have any such ECDs)
64
- * Event filtering (I haven't tested it)
73
+ * RGB+ and XY colour commands are not tested (I don't have any compatible lights)
74
+ * Numerical (absolute) instances are not tested (I don't have any such ECDs)
65
75
 
66
- Not implemented:
76
+ ## Out of scope
67
77
 
68
78
  * 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
79
  * Any commands described in the documentation as "legacy" (they aren't useful)
80
+
70
81
  ## TPI Advanced wishlist
71
82
 
72
83
  * 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.)_
@@ -4,33 +4,46 @@ This is an implementation of the **Zencontrol TPI Advanced** protocol, written i
4
4
 
5
5
  - zencontrol.io: Implementation of the raw TPI Advanced UDP packet specification;
6
6
  - zencontrol.api: Implementation of most TPI Advanced API commands and events;
7
- - zencontrol.interface: An opinionated abstraction layer suitable for integration into smart building control software. It provides methods, objects, and callbacks for managing lights, groups, profiles, buttons, motion sensors, and system variables. This code is still undergoing significant refinement.
7
+ - zencontrol.interface: An opinionated abstraction layer suitable for integration into smart building control software. It provides methods, objects, and callbacks for managing lights, groups, profiles, buttons, motion sensors, and system variables.
8
8
 
9
- Built on top of this is an example MQTT bridge for Home Assistant. See [examples/mqtt_bridge.md](examples/mqtt_bridge.md).
9
+ This library has now undergone validation in multiple environments. There is an extensive test suite, some of which uses
10
+ [zencontrol-simulator](https://github.com/sjwright/zencontrol-simulator), a nearly feature-complete simulator of zencontrol hardware.
11
+ A practical demonstration is [zencontrol-homeassistant](https://github.com/sjwright/zencontrol-homeassistant), a comprehensive Home Assistant integration.
10
12
 
11
13
  ## Requirements
12
14
 
13
- * Python 3.11 (or later)
14
- * Controller firmware 2.2.11 (or later)
15
+ * Python 3.14 (or later)
16
+ * Controller firmware 2.2.130 or later is strongly recommended (minimum 2.2.11 required)
15
17
 
16
18
  ## Install
17
19
 
18
- Refer to zencontrol-tpi project for now.
19
- For integrators, the library is also published on PyPI.
20
+ This library is available on PyPI.
21
+
22
+ ## Testing
23
+
24
+ Integration tests start [zencontrol-simulator](https://github.com/sjwright/zencontrol-simulator) on an ephemeral local port and exercise a real UDP TPI protocol path. Either install the simulator, or check it out as a sibling directory (`../zencontrol-simulator`); tests will pick it up automatically. Note that PyYAML is a simulator dependency.
25
+
26
+ ```bash
27
+ pip install -e ".[dev]"
28
+ pip install PyYAML
29
+ # optional if not using a sibling checkout:
30
+ # pip install -e ../zencontrol-simulator
31
+ pytest -m simulator
32
+ pytest -m "not simulator"
33
+ # or run everything:
34
+ pytest
35
+ ```
20
36
 
21
37
  ## Limitations
22
38
 
23
- Implemented but untested:
24
-
25
- * Dealing with multiple controllers (I only have one controller)
26
- * RGB+ and XY colour commands (I don't have any compatible lights)
27
- * Numerical (absolute) instances (I don't have any such ECDs)
28
- * Event filtering (I haven't tested it)
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)
29
41
 
30
- Not implemented:
42
+ ## Out of scope
31
43
 
32
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)
33
45
  * Any commands described in the documentation as "legacy" (they aren't useful)
46
+
34
47
  ## TPI Advanced wishlist
35
48
 
36
49
  * 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.)_
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import yaml
3
+ from pathlib import Path
3
4
  from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
4
5
 
5
6
  async def main():
6
7
  """Test the async ZenProtocol with controller queries"""
7
8
  # Load configuration
8
- config = yaml.safe_load(open("tests/config.yaml"))
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
10
 
10
11
  # Create protocol and controller
11
12
  async with ZenProtocol(print_traffic=True) as tpi:
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import yaml
3
+ from pathlib import Path
3
4
  from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
4
5
 
5
6
  async def main():
6
7
  """Test DALI device queries"""
7
8
  # Load configuration
8
- config = yaml.safe_load(open("tests/config.yaml"))
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
10
 
10
11
  # Create protocol and controller
11
12
  async with ZenProtocol(print_traffic=False) as tpi:
@@ -13,7 +13,7 @@ sys.stdout.reconfigure(line_buffering=True)
13
13
 
14
14
  from zencontrol import ZenProtocol, ZenController, ZenAddress, ZenInstance, run_with_keyboard_interrupt
15
15
 
16
- CONFIG_PATH = Path(__file__).parent / "config.yaml"
16
+ CONFIG_PATH = Path(__file__).resolve().parents[2] / "tests" / "config.yaml"
17
17
 
18
18
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
19
19
  logger = logging.getLogger("test_events")
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import yaml
3
+ from pathlib import Path
3
4
  from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
4
5
 
5
6
  async def main():
6
7
  """Test control gear queries"""
7
8
  # Load configuration
8
- config = yaml.safe_load(open("tests/config.yaml"))
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
10
 
10
11
  # Create protocol and controller
11
12
  async with ZenProtocol(print_traffic=False) as tpi:
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import yaml
3
+ from pathlib import Path
3
4
  from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
4
5
 
5
6
  async def main():
6
7
  """Test group queries"""
7
8
  # Load configuration
8
- config = yaml.safe_load(open("tests/config.yaml"))
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
10
 
10
11
  # Create protocol and controller
11
12
  async with ZenProtocol(print_traffic=True) as tpi:
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import yaml
3
+ from pathlib import Path
3
4
  from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
4
5
 
5
6
  async def main():
6
7
  """Test instance queries"""
7
8
  # Load configuration
8
- config = yaml.safe_load(open("tests/config.yaml"))
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
10
 
10
11
  # Create protocol and controller
11
12
  async with ZenProtocol(print_traffic=False) as tpi:
@@ -1,10 +1,11 @@
1
1
  import asyncio
2
2
  from zencontrol import ZenControl, run_with_keyboard_interrupt
3
3
  import yaml
4
+ from pathlib import Path
4
5
  import time
5
6
 
6
7
  async def main():
7
- config = yaml.safe_load(open("tests/config.yaml"))
8
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
8
9
  zi = ZenControl(print_traffic=False)
9
10
  zi.add_controller(**config.get('zencontrol')[0])
10
11
  await zi.start()
@@ -1,11 +1,11 @@
1
1
  import asyncio
2
2
  from zencontrol import ZenControl, ZenProfile, ZenGroup, ZenLight, ZenButton, ZenMotionSensor, ZenSystemVariable, ZenColour, run_with_keyboard_interrupt
3
3
  import yaml
4
+ from pathlib import Path
4
5
  import time
5
- from typing import Optional
6
6
 
7
7
  async def main():
8
- config = yaml.safe_load(open("tests/config.yaml"))
8
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
9
  zi = ZenControl(print_traffic=False)
10
10
  zi.add_controller(**config.get('zencontrol')[0])
11
11
 
@@ -20,11 +20,11 @@ async def main():
20
20
  ms()
21
21
  print(f"Profile Change Event - {profile}")
22
22
 
23
- async def _zen_group_change(group: ZenGroup, level: Optional[int] = None, colour: Optional[ZenColour] = None, scene: Optional[int] = None, discoordinated: bool = False) -> None:
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
24
  ms()
25
25
  print(f"Group Change Event - {group} level {level} colour {colour} scene {scene} {'discoordinated' if discoordinated else ''}")
26
26
 
27
- async def _zen_light_change(light: ZenLight, level: Optional[int] = None, colour: Optional[ZenColour] = None, scene: Optional[int] = None) -> None:
27
+ async def _zen_light_change(light: ZenLight, level: int | None = None, colour: ZenColour | None = None, scene: int | None = None) -> None:
28
28
  ms()
29
29
  print(f"Light Change Event - {light} level {level} colour {colour} scene {scene}")
30
30
 
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import yaml
3
+ from pathlib import Path
3
4
  from zencontrol import ZenProtocol, ZenController, ZenAddress, ZenAddressType, run_with_keyboard_interrupt
4
5
 
5
6
  async def main():
6
7
  """Test LED control queries"""
7
8
  # Load configuration
8
- config = yaml.safe_load(open("tests/config.yaml"))
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
10
 
10
11
  # Create protocol and controller
11
12
  async with ZenProtocol(print_traffic=False) as tpi:
@@ -14,7 +14,7 @@ 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
16
16
 
17
- CONFIG_PATH = Path(__file__).parent / "config.yaml"
17
+ CONFIG_PATH = Path(__file__).resolve().parents[2] / "tests" / "config.yaml"
18
18
  ECG_ADDRESS = 33
19
19
  TIMEOUT_SECONDS = 5.0
20
20
 
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import yaml
3
+ from pathlib import Path
3
4
  from zencontrol import ZenProtocol, ZenController, ZenAddress, ZenInstance, ZenEventMode, run_with_keyboard_interrupt
4
5
 
5
6
  async def main():
6
7
  """Test multicast event monitoring"""
7
8
  # Load configuration
8
- config = yaml.safe_load(open("tests/config.yaml"))
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
10
 
10
11
  # Create protocol and controller
11
12
  async with ZenProtocol(print_traffic=True, unicast=False) as tpi:
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import yaml
3
+ from pathlib import Path
3
4
  from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
4
5
 
5
6
  async def main():
6
7
  """Test profile queries"""
7
8
  # Load configuration
8
- config = yaml.safe_load(open("tests/config.yaml"))
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
10
 
10
11
  # Create protocol and controller
11
12
  async with ZenProtocol(print_traffic=False) as tpi:
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import yaml
3
+ from pathlib import Path
3
4
  from zencontrol import ZenProtocol, ZenController, run_with_keyboard_interrupt
4
5
 
5
6
  async def main():
6
7
  """Test system variable queries and sets"""
7
8
  # Load configuration
8
- config = yaml.safe_load(open("tests/config.yaml"))
9
+ config = yaml.safe_load(open(Path(__file__).resolve().parents[2] / "tests" / "config.yaml"))
9
10
 
10
11
  # Create protocol and controller
11
12
  async with ZenProtocol(print_traffic=True) as tpi:
@@ -5,7 +5,7 @@ import json
5
5
  import yaml
6
6
  import re
7
7
  from pathlib import Path
8
- from typing import Optional, Any
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
@@ -157,36 +157,6 @@ class ZenMQTTBridge:
157
157
  pickle.dump(self.zen.cache, f)
158
158
 
159
159
 
160
-
161
- """
162
- controllers:
163
- - id: 1
164
- name: zen1
165
- label: Zencontrol 1
166
- groups:
167
- - id: 0
168
- name: All Lights
169
- hide_group: True
170
- hide_members: True
171
- - id: 1
172
- name: Ground
173
- lights:
174
- - ecg: 14
175
- name: Lounge NW
176
- room: Downstairs
177
- groups:
178
- - 15
179
- - ecg: 21
180
- name: Guest NE
181
- room: Guest Bedroom
182
- groups:
183
- - 15
184
- - ecg: 42
185
- name: Garage
186
- override_name: Garage Downlights
187
- room: Garage
188
- """
189
-
190
160
  clist = []
191
161
  for c in sorted(self.control, key=lambda x: x.id):
192
162
  c: ZenController
@@ -844,9 +814,9 @@ class ZenMQTTBridge:
844
814
  async def _mqtt_light_change(self, light: ZenLight|ZenGroup, payload: dict[str, Any]) -> None:
845
815
  addr = light.address
846
816
  ctrl = addr.controller
847
- state: Optional[str] = payload.get("state", None)
848
- brightness: Optional[int] = payload.get("brightness", None)
849
- mireds: Optional[int] = payload.get("color_temp", None)
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)
850
820
 
851
821
  # If brightness or temperature is set
852
822
  if brightness is not None or mireds is not None:
@@ -865,7 +835,7 @@ class ZenMQTTBridge:
865
835
  self.logger.info(f"♥️💡 Command from HA: {ctrl.name} turning gear {addr.number} ON")
866
836
  await light.on()
867
837
 
868
- async def _zen_light_change(self, light: ZenLight, level: Optional[int] = None, colour: Optional[ZenColour] = None, scene: Optional[int] = None) -> None:
838
+ async def _zen_light_change(self, light: ZenLight, level: int | None = None, colour: ZenColour | None = None, scene: int | None = None) -> None:
869
839
  typestr = "group" if light.address.type == ZenAddressType.GROUP else "light"
870
840
  emoji = "👥" if light.address.type == ZenAddressType.GROUP else "💡"
871
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 '--'}")
@@ -960,7 +930,7 @@ class ZenMQTTBridge:
960
930
 
961
931
  # mqtt group light change calls _mqtt_light_change
962
932
 
963
- async def _zen_group_change(self, group: ZenGroup, level: Optional[int] = None, colour: Optional[ZenColour] = None, scene: Optional[int] = None, discoordinated: Optional[bool] = None) -> None:
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:
964
934
  select_mqtt_topic = group.client_data.get("select", {}).get('mqtt_topic', None)
965
935
 
966
936
  # Get the scene label for the ID from the group
@@ -1139,4 +1109,4 @@ async def main():
1139
1109
  await bridge.stop()
1140
1110
 
1141
1111
  if __name__ == "__main__":
1142
- 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.11"
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.11",
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",
@@ -43,6 +41,9 @@ mqtt = [
43
41
  "aiomqtt>=2.1.0",
44
42
  "PyYAML>=6.0",
45
43
  ]
44
+ # Simulator integration tests (tests/test_sim_*.py) need zencontrol-simulator
45
+ # (pip install -e ../zencontrol-simulator, or a sibling ../zencontrol-simulator
46
+ # checkout) plus PyYAML.
46
47
 
47
48
  [project.scripts]
48
49
  zencontrol-mqtt = "examples.mqtt_bridge:main"
@@ -64,7 +65,7 @@ zencontrol = ["py.typed"]
64
65
 
65
66
  [tool.black]
66
67
  line-length = 88
67
- target-version = ['py311']
68
+ target-version = ['py314']
68
69
  include = '\.pyi?$'
69
70
  extend-exclude = '''
70
71
  /(
@@ -81,7 +82,7 @@ extend-exclude = '''
81
82
  '''
82
83
 
83
84
  [tool.mypy]
84
- python_version = "3.11"
85
+ python_version = "3.14"
85
86
  warn_return_any = true
86
87
  warn_unused_configs = true
87
88
  disallow_untyped_defs = true
@@ -102,3 +103,13 @@ python_classes = ["Test*"]
102
103
  python_functions = ["test_*"]
103
104
  addopts = "-v --tb=short"
104
105
  asyncio_mode = "auto"
106
+ markers = [
107
+ "simulator: integration tests that start zencontrol-simulator (installed or sibling ../zencontrol-simulator checkout)",
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"]
@@ -77,6 +77,9 @@ def test_controller_same_name_same_protocol_is_singleton() -> None:
77
77
  host="127.0.0.1",
78
78
  port=5108,
79
79
  )
80
+ a.client = object() # type: ignore[assignment]
81
+ a.version = "1.2.3"
82
+ a.startup_complete = True
80
83
  b = ZenController(
81
84
  protocol=zen.protocol,
82
85
  id=2,
@@ -88,4 +91,8 @@ def test_controller_same_name_same_protocol_is_singleton() -> None:
88
91
  assert a is b
89
92
  assert a.label == "Second"
90
93
  assert a.host == "10.0.0.1"
94
+ # Re-construct must not wipe live transport / interview state
95
+ assert a.client is not None
96
+ assert a.version == "1.2.3"
97
+ assert a.startup_complete is True
91
98
  zen.clear_entity_caches()