zencontrol-python 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/dump_simulator_config.py +7 -22
- examples/live/interface_events.py +2 -3
- examples/mqtt_bridge.py +7 -7
- zencontrol/__init__.py +1 -1
- zencontrol/api/models.py +118 -93
- zencontrol/api/protocol.py +146 -130
- zencontrol/api/types.py +3 -3
- zencontrol/exceptions.py +2 -3
- zencontrol/interface/interface.py +232 -175
- zencontrol/io/command.py +23 -23
- zencontrol/io/event.py +13 -14
- zencontrol/utils.py +4 -2
- {zencontrol_python-0.1.2.dist-info → zencontrol_python-0.1.4.dist-info}/METADATA +28 -16
- {zencontrol_python-0.1.2.dist-info → zencontrol_python-0.1.4.dist-info}/RECORD +18 -18
- {zencontrol_python-0.1.2.dist-info → zencontrol_python-0.1.4.dist-info}/WHEEL +0 -0
- {zencontrol_python-0.1.2.dist-info → zencontrol_python-0.1.4.dist-info}/entry_points.txt +0 -0
- {zencontrol_python-0.1.2.dist-info → zencontrol_python-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {zencontrol_python-0.1.2.dist-info → zencontrol_python-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
examples/mqtt_bridge.py
CHANGED
|
@@ -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())
|
zencontrol/__init__.py
CHANGED
|
@@ -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
|
zencontrol/api/models.py
CHANGED
|
@@ -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
|