zencontrol-python 0.1.2__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.
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/PKG-INFO +4 -6
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/README.md +1 -1
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/interface_events.py +2 -3
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/mqtt_bridge.py +7 -7
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/pyproject.toml +11 -6
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/__init__.py +1 -1
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/api/models.py +118 -93
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/api/protocol.py +127 -130
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/api/types.py +3 -3
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/exceptions.py +2 -3
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/interface/interface.py +160 -157
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/io/command.py +23 -23
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/io/event.py +13 -14
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/utils.py +4 -2
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol_python.egg-info/PKG-INFO +4 -6
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol_python.egg-info/SOURCES.txt +0 -1
- zencontrol_python-0.1.2/examples/dump_simulator_config.py +0 -489
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/LICENSE +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/__init__.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/controller.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/devices.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/events.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/gear.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/groups.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/instances.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/interface.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/leds.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/level_change_v2.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/multicast.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/multicast_raw.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/profiles.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/tpi_event_listener.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/examples/live/variables.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/setup.cfg +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/tests/test_discovery.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/tests/test_entity_scope.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/tests/test_event_listener_disconnect.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/tests/test_lifecycle.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/tests/test_reconnect.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/tests/test_sim_events.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/tests/test_sim_interface.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/tests/test_sim_protocol.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/tests/test_sub_labels.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/tests/test_wire_resilience.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/api/__init__.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/interface/__init__.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/io/__init__.py +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol/py.typed +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol_python.egg-info/dependency_links.txt +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol_python.egg-info/entry_points.txt +0 -0
- {zencontrol_python-0.1.2 → zencontrol_python-0.1.3}/zencontrol_python.egg-info/requires.txt +0 -0
- {zencontrol_python-0.1.2 → 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.
|
|
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.
|
|
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
|
|
@@ -48,7 +46,7 @@ A practical demonstration is [zencontrol-homeassistant](https://github.com/sjwri
|
|
|
48
46
|
|
|
49
47
|
## Requirements
|
|
50
48
|
|
|
51
|
-
* Python 3.
|
|
49
|
+
* Python 3.14 (or later)
|
|
52
50
|
* Controller firmware 2.2.130 or later is strongly recommended (minimum 2.2.11 required)
|
|
53
51
|
|
|
54
52
|
## Install
|
|
@@ -12,7 +12,7 @@ A practical demonstration is [zencontrol-homeassistant](https://github.com/sjwri
|
|
|
12
12
|
|
|
13
13
|
## Requirements
|
|
14
14
|
|
|
15
|
-
* Python 3.
|
|
15
|
+
* Python 3.14 (or later)
|
|
16
16
|
* Controller firmware 2.2.130 or later is strongly recommended (minimum 2.2.11 required)
|
|
17
17
|
|
|
18
18
|
## Install
|
|
@@ -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"]
|
|
@@ -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.3"
|
|
62
62
|
__author__ = "Simon Wright"
|
|
63
63
|
|
|
64
64
|
# Public API - these are the main classes users should import
|
|
@@ -12,7 +12,7 @@ import socket
|
|
|
12
12
|
import struct
|
|
13
13
|
import time
|
|
14
14
|
from dataclasses import dataclass, field
|
|
15
|
-
from typing import Any,
|
|
15
|
+
from typing import Any, Self
|
|
16
16
|
|
|
17
17
|
from ..io import ZenClient
|
|
18
18
|
from .types import ZenAddressType, ZenInstanceType, ZenColourType, Const
|
|
@@ -21,13 +21,13 @@ from .types import ZenAddressType, ZenInstanceType, ZenColourType, Const
|
|
|
21
21
|
DEFAULT_CONTROLLER_PORT = 5108
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
@dataclass(frozen=True)
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
25
|
class DiscoveredController:
|
|
26
26
|
"""A controller identified from multicast events (not yet registered)."""
|
|
27
27
|
|
|
28
28
|
host: str
|
|
29
29
|
mac: str
|
|
30
|
-
label:
|
|
30
|
+
label: str | None = None
|
|
31
31
|
port: int = DEFAULT_CONTROLLER_PORT
|
|
32
32
|
|
|
33
33
|
|
|
@@ -43,17 +43,17 @@ class ZenController:
|
|
|
43
43
|
label: str
|
|
44
44
|
host: str
|
|
45
45
|
port: int
|
|
46
|
-
mac:
|
|
47
|
-
mac_bytes:
|
|
46
|
+
mac: str | None = None
|
|
47
|
+
mac_bytes: bytes | None = field(init=False, default=None)
|
|
48
48
|
# Any avoids an import cycle with protocol.py (which imports this module).
|
|
49
|
-
protocol:
|
|
50
|
-
version:
|
|
49
|
+
protocol: Any | None = None
|
|
50
|
+
version: str | None = None
|
|
51
51
|
startup_complete: bool = False
|
|
52
52
|
dali_ready: bool = False
|
|
53
53
|
filtering: bool = False
|
|
54
54
|
last_seen: float = field(default_factory=time.time)
|
|
55
|
-
client:
|
|
56
|
-
_ip:
|
|
55
|
+
client: ZenClient | None = None
|
|
56
|
+
_ip: str | None = field(init=False, repr=False, default=None)
|
|
57
57
|
|
|
58
58
|
def __post_init__(self):
|
|
59
59
|
self._update_mac_bytes(self.mac)
|
|
@@ -64,7 +64,7 @@ class ZenController:
|
|
|
64
64
|
if name == "mac" and "mac_bytes" in self.__dict__:
|
|
65
65
|
self._update_mac_bytes(value if isinstance(value, str) or value is None else None)
|
|
66
66
|
|
|
67
|
-
def _update_mac_bytes(self, value:
|
|
67
|
+
def _update_mac_bytes(self, value: str | None) -> None:
|
|
68
68
|
"""Update mac_bytes from a MAC string (or clear it)."""
|
|
69
69
|
if value is not None:
|
|
70
70
|
try:
|
|
@@ -101,14 +101,14 @@ class ZenController:
|
|
|
101
101
|
return self.ip
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
@dataclass
|
|
104
|
+
@dataclass(slots=True)
|
|
105
105
|
class ZenAddress:
|
|
106
106
|
"""Represents a DALI address"""
|
|
107
107
|
controller: ZenController
|
|
108
108
|
type: ZenAddressType
|
|
109
109
|
number: int
|
|
110
|
-
label:
|
|
111
|
-
serial:
|
|
110
|
+
label: str | None = field(default=None, init=False)
|
|
111
|
+
serial: str | None = field(default=None, init=False)
|
|
112
112
|
|
|
113
113
|
@classmethod
|
|
114
114
|
def broadcast(cls, controller: ZenController) -> Self:
|
|
@@ -168,14 +168,14 @@ class ZenAddress:
|
|
|
168
168
|
raise ValueError(f"Group address must be 0-15, got {self.number}")
|
|
169
169
|
|
|
170
170
|
|
|
171
|
-
@dataclass
|
|
171
|
+
@dataclass(slots=True)
|
|
172
172
|
class ZenInstance:
|
|
173
173
|
"""Represents a DALI ECD instance"""
|
|
174
174
|
address: ZenAddress
|
|
175
175
|
type: ZenInstanceType
|
|
176
176
|
number: int
|
|
177
|
-
active:
|
|
178
|
-
error:
|
|
177
|
+
active: bool | None = None
|
|
178
|
+
error: bool | None = None
|
|
179
179
|
def __post_init__(self):
|
|
180
180
|
if not 0 <= self.number < Const.MAX_INSTANCE:
|
|
181
181
|
raise ValueError(f"Instance number must be between 0 and {Const.MAX_INSTANCE-1}, received {self.number}")
|
|
@@ -185,100 +185,125 @@ class ZenInstance:
|
|
|
185
185
|
return f"{self.address.entity_id_string()}_{self.number}"
|
|
186
186
|
|
|
187
187
|
|
|
188
|
-
@dataclass
|
|
188
|
+
@dataclass(slots=True)
|
|
189
189
|
class ZenColour:
|
|
190
190
|
"""Represents a DALI color"""
|
|
191
|
-
type:
|
|
192
|
-
kelvin:
|
|
193
|
-
r:
|
|
194
|
-
g:
|
|
195
|
-
b:
|
|
196
|
-
w:
|
|
197
|
-
a:
|
|
198
|
-
f:
|
|
199
|
-
x:
|
|
200
|
-
y:
|
|
191
|
+
type: ZenColourType | None = None
|
|
192
|
+
kelvin: int | None = None
|
|
193
|
+
r: int | None = None
|
|
194
|
+
g: int | None = None
|
|
195
|
+
b: int | None = None
|
|
196
|
+
w: int | None = None
|
|
197
|
+
a: int | None = None
|
|
198
|
+
f: int | None = None
|
|
199
|
+
x: int | None = None
|
|
200
|
+
y: int | None = None
|
|
201
201
|
|
|
202
202
|
@classmethod
|
|
203
|
-
def from_bytes(cls,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
203
|
+
def from_bytes(cls, data: bytes) -> Self | None:
|
|
204
|
+
match list(data):
|
|
205
|
+
case [ZenColourType.RGBWAF.value, r, g, b, w, a, f]:
|
|
206
|
+
return cls(type=ZenColourType.RGBWAF, r=r, g=g, b=b, w=w, a=a, f=f)
|
|
207
|
+
case [ZenColourType.TC.value, hi, lo] | [ZenColourType.TC.value, hi, lo, *_]:
|
|
208
|
+
if len(data) not in (3, 7):
|
|
209
|
+
return None
|
|
210
|
+
return cls(type=ZenColourType.TC, kelvin=(hi << 8) | lo)
|
|
211
|
+
case [ZenColourType.XY.value, xh, xl, yh, yl] | [ZenColourType.XY.value, xh, xl, yh, yl, *_]:
|
|
212
|
+
if len(data) not in (5, 7):
|
|
213
|
+
return None
|
|
214
|
+
return cls(type=ZenColourType.XY, x=(xh << 8) | xl, y=(yh << 8) | yl)
|
|
215
|
+
case _:
|
|
216
|
+
return None
|
|
216
217
|
|
|
217
218
|
def __post_init__(self):
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
219
|
+
match self.type:
|
|
220
|
+
case ZenColourType.TC:
|
|
221
|
+
kelvin = self.kelvin
|
|
222
|
+
if kelvin is None:
|
|
223
|
+
raise ValueError("Kelvin is required for TC colour type")
|
|
224
|
+
if not Const.MIN_KELVIN <= kelvin <= Const.MAX_KELVIN:
|
|
225
|
+
logging.getLogger(__name__).warning(
|
|
226
|
+
"Kelvin %s out of range [%s, %s]; clamping",
|
|
227
|
+
kelvin, Const.MIN_KELVIN, Const.MAX_KELVIN,
|
|
228
|
+
)
|
|
229
|
+
self.kelvin = max(Const.MIN_KELVIN, min(Const.MAX_KELVIN, kelvin))
|
|
230
|
+
case ZenColourType.RGBWAF:
|
|
231
|
+
r, g, b = self.r, self.g, self.b
|
|
232
|
+
if r is None or not 0 <= r <= 255:
|
|
233
|
+
raise ValueError(f"R must be between 0 and 255, received {self.r}")
|
|
234
|
+
if g is None or not 0 <= g <= 255:
|
|
235
|
+
raise ValueError(f"G must be between 0 and 255, received {self.g}")
|
|
236
|
+
if b is None or not 0 <= b <= 255:
|
|
237
|
+
raise ValueError(f"B must be between 0 and 255, received {self.b}")
|
|
238
|
+
if self.w is not None and not 0 <= self.w <= 255:
|
|
239
|
+
raise ValueError(f"W must be between 0 and 255, received {self.w}")
|
|
240
|
+
if self.a is not None and not 0 <= self.a <= 255:
|
|
241
|
+
raise ValueError(f"A must be between 0 and 255, received {self.a}")
|
|
242
|
+
if self.f is not None and not 0 <= self.f <= 255:
|
|
243
|
+
raise ValueError(f"F must be between 0 and 255, received {self.f}")
|
|
244
|
+
case ZenColourType.XY:
|
|
245
|
+
x, y = self.x, self.y
|
|
246
|
+
if x is None or not 0 <= x <= 65535:
|
|
247
|
+
raise ValueError(f"X must be between 0 and 65535, received {self.x}")
|
|
248
|
+
if y is None or not 0 <= y <= 65535:
|
|
249
|
+
raise ValueError(f"Y must be between 0 and 65535, received {self.y}")
|
|
250
|
+
case _:
|
|
251
|
+
pass
|
|
250
252
|
|
|
251
253
|
def __repr__(self) -> str:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return
|
|
254
|
+
match self.type:
|
|
255
|
+
case ZenColourType.TC:
|
|
256
|
+
return f"ZenColour(kelvin={self.kelvin})"
|
|
257
|
+
case ZenColourType.RGBWAF:
|
|
258
|
+
return f"ZenColour(r={self.r}, g={self.g}, b={self.b}, w={self.w}, a={self.a}, f={self.f})"
|
|
259
|
+
case ZenColourType.XY:
|
|
260
|
+
return f"ZenColour(x={self.x}, y={self.y})"
|
|
261
|
+
case _:
|
|
262
|
+
return f"ZenColour(type={self.type})"
|
|
263
|
+
|
|
264
|
+
def __eq__(self, other: object) -> bool:
|
|
265
|
+
if not isinstance(other, ZenColour):
|
|
266
|
+
return NotImplemented
|
|
267
|
+
return (
|
|
268
|
+
self.type == other.type
|
|
269
|
+
and self.kelvin == other.kelvin
|
|
270
|
+
and self.r == other.r
|
|
271
|
+
and self.g == other.g
|
|
272
|
+
and self.b == other.b
|
|
273
|
+
and self.w == other.w
|
|
274
|
+
and self.a == other.a
|
|
275
|
+
and self.f == other.f
|
|
276
|
+
and self.x == other.x
|
|
277
|
+
and self.y == other.y
|
|
278
|
+
)
|
|
265
279
|
|
|
266
280
|
def to_bytes(self, level: int = 255) -> bytes:
|
|
267
281
|
"""Encode colour data as returned by QUERY_DALI_COLOUR (no address or arc level)."""
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
282
|
+
match self.type:
|
|
283
|
+
case ZenColourType.TC:
|
|
284
|
+
return struct.pack(">BH", 0x20, self.kelvin)
|
|
285
|
+
case ZenColourType.RGBWAF:
|
|
286
|
+
return struct.pack(
|
|
287
|
+
"BBBBBBB",
|
|
288
|
+
0x80,
|
|
289
|
+
self.r,
|
|
290
|
+
self.g,
|
|
291
|
+
self.b,
|
|
292
|
+
self.w if self.w is not None else 0,
|
|
293
|
+
self.a if self.a is not None else 0,
|
|
294
|
+
self.f if self.f is not None else 0,
|
|
295
|
+
)
|
|
296
|
+
case ZenColourType.XY:
|
|
297
|
+
return struct.pack(">BHH", 0x10, self.x, self.y)
|
|
298
|
+
case _:
|
|
299
|
+
return b""
|
|
275
300
|
|
|
276
301
|
def command_payload(self) -> bytes:
|
|
277
302
|
"""Colour type and channel bytes for DALI_COLOUR (follows address and arc level)."""
|
|
278
303
|
return self.to_bytes()
|
|
279
304
|
|
|
280
305
|
|
|
281
|
-
@dataclass
|
|
306
|
+
@dataclass(slots=True)
|
|
282
307
|
class ZenProfile:
|
|
283
308
|
"""Represents a DALI profile"""
|
|
284
309
|
controller: ZenController
|