zencontrol-python 0.1.0__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/__init__.py +0 -0
- examples/mqtt_bridge.py +1142 -0
- zencontrol/__init__.py +108 -0
- zencontrol/api/__init__.py +30 -0
- zencontrol/api/models.py +273 -0
- zencontrol/api/protocol.py +1700 -0
- zencontrol/api/types.py +203 -0
- zencontrol/exceptions.py +30 -0
- zencontrol/interface/__init__.py +33 -0
- zencontrol/interface/interface.py +1579 -0
- zencontrol/io/__init__.py +24 -0
- zencontrol/io/command.py +387 -0
- zencontrol/io/event.py +316 -0
- zencontrol/py.typed +0 -0
- zencontrol/utils.py +67 -0
- zencontrol_python-0.1.0.dist-info/METADATA +79 -0
- zencontrol_python-0.1.0.dist-info/RECORD +21 -0
- zencontrol_python-0.1.0.dist-info/WHEEL +5 -0
- zencontrol_python-0.1.0.dist-info/entry_points.txt +2 -0
- zencontrol_python-0.1.0.dist-info/licenses/LICENSE +504 -0
- zencontrol_python-0.1.0.dist-info/top_level.txt +2 -0
zencontrol/api/types.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API-level type definitions.
|
|
3
|
+
|
|
4
|
+
This module contains types and enums that belong to the API layer:
|
|
5
|
+
- DALI address types, instance types, color types
|
|
6
|
+
- Event masks and modes used by the TPI protocol
|
|
7
|
+
- Constants used by the API layer
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Optional, Self
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ZenAddressType(Enum):
|
|
16
|
+
BROADCAST = 0
|
|
17
|
+
ECG = 1 # Control Gear
|
|
18
|
+
ECD = 2 # Control Device
|
|
19
|
+
GROUP = 3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ZenInstanceType(Enum):
|
|
23
|
+
PUSH_BUTTON = 0x01
|
|
24
|
+
ABSOLUTE_INPUT = 0x02
|
|
25
|
+
OCCUPANCY_SENSOR = 0x03
|
|
26
|
+
LIGHT_SENSOR = 0x04
|
|
27
|
+
GENERAL_SENSOR = 0x06
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ZenColourType(Enum):
|
|
31
|
+
XY = 0x10
|
|
32
|
+
TC = 0x20 # Tunable White
|
|
33
|
+
RGBWAF = 0x80
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ZenErrorCode(Enum):
|
|
37
|
+
CHECKSUM = 0x01
|
|
38
|
+
SHORT_CIRCUIT = 0x02
|
|
39
|
+
RECEIVE_ERROR = 0x03
|
|
40
|
+
UNKNOWN_CMD = 0x04
|
|
41
|
+
PAID_FEATURE = 0xB0
|
|
42
|
+
INVALID_ARGS = 0xB1
|
|
43
|
+
CMD_REFUSED = 0xB2
|
|
44
|
+
QUEUE_FAILURE = 0xB3
|
|
45
|
+
RESPONSE_UNAVAIL = 0xB4
|
|
46
|
+
OTHER_DALI_ERROR = 0xB5
|
|
47
|
+
MAX_LIMIT = 0xB6
|
|
48
|
+
UNEXPECTED_RESULT = 0xB7
|
|
49
|
+
UNKNOWN_TARGET = 0xB8
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ZenEventMode:
|
|
54
|
+
enabled: bool = False
|
|
55
|
+
filtering: bool = False
|
|
56
|
+
unicast: bool = False
|
|
57
|
+
multicast: bool = False
|
|
58
|
+
|
|
59
|
+
def bitmask(self) -> int:
|
|
60
|
+
mode_flag = 0x00
|
|
61
|
+
if self.enabled: mode_flag |= 0x01
|
|
62
|
+
if self.filtering: mode_flag |= 0x02
|
|
63
|
+
if self.unicast: mode_flag |= 0x40
|
|
64
|
+
if not self.multicast: mode_flag |= 0x80
|
|
65
|
+
return mode_flag
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_byte(cls, mode_flag: int) -> Self:
|
|
69
|
+
return cls(
|
|
70
|
+
enabled = (mode_flag & 0x01) != 0,
|
|
71
|
+
filtering = (mode_flag & 0x02) != 0,
|
|
72
|
+
unicast = (mode_flag & 0x40) != 0,
|
|
73
|
+
multicast = (mode_flag & 0x80) == 0
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ZenEventCode(Enum):
|
|
78
|
+
BUTTON_PRESS = 0x00
|
|
79
|
+
BUTTON_HOLD = 0x01
|
|
80
|
+
ABSOLUTE_INPUT = 0x02
|
|
81
|
+
LEVEL_CHANGE = 0x03
|
|
82
|
+
GROUP_LEVEL_CHANGE = 0x04
|
|
83
|
+
SCENE_CHANGE = 0x05
|
|
84
|
+
IS_OCCUPIED = 0x06
|
|
85
|
+
SYSTEM_VARIABLE_CHANGE = 0x07
|
|
86
|
+
COLOUR_CHANGE = 0x08
|
|
87
|
+
PROFILE_CHANGE = 0x09
|
|
88
|
+
GROUP_OCCUPIED = 0x0A
|
|
89
|
+
LEVEL_CHANGE_V2 = 0x0B
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class ZenEventMask:
|
|
94
|
+
button_press: bool = False
|
|
95
|
+
button_hold: bool = False
|
|
96
|
+
absolute_input: bool = False
|
|
97
|
+
level_change: bool = False
|
|
98
|
+
group_level_change: bool = False
|
|
99
|
+
scene_change: bool = False
|
|
100
|
+
is_occupied: bool = False
|
|
101
|
+
system_variable_change: bool = False
|
|
102
|
+
colour_change: bool = False
|
|
103
|
+
profile_change: bool = False
|
|
104
|
+
group_occupied: bool = False
|
|
105
|
+
level_change_v2: bool = False
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def all_events(cls):
|
|
109
|
+
# Exclude deprecated level_change / group_level_change — use level_change_v2 (spec V2.001.121+)
|
|
110
|
+
return cls(
|
|
111
|
+
button_press = True,
|
|
112
|
+
button_hold = True,
|
|
113
|
+
absolute_input = True,
|
|
114
|
+
scene_change = True,
|
|
115
|
+
is_occupied = True,
|
|
116
|
+
system_variable_change = True,
|
|
117
|
+
colour_change = True,
|
|
118
|
+
profile_change = True,
|
|
119
|
+
group_occupied = True,
|
|
120
|
+
level_change_v2 = True
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_upper_lower(cls, upper: int, lower: int) -> Self:
|
|
125
|
+
return cls.from_double_byte((upper << 8) | lower)
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def from_double_byte(cls, event_mask: int) -> Self:
|
|
129
|
+
return cls(
|
|
130
|
+
button_press = (event_mask & (1 << ZenEventCode.BUTTON_PRESS.value)) != 0,
|
|
131
|
+
button_hold = (event_mask & (1 << ZenEventCode.BUTTON_HOLD.value)) != 0,
|
|
132
|
+
absolute_input = (event_mask & (1 << ZenEventCode.ABSOLUTE_INPUT.value)) != 0,
|
|
133
|
+
level_change = (event_mask & (1 << ZenEventCode.LEVEL_CHANGE.value)) != 0,
|
|
134
|
+
group_level_change = (event_mask & (1 << ZenEventCode.GROUP_LEVEL_CHANGE.value)) != 0,
|
|
135
|
+
scene_change = (event_mask & (1 << ZenEventCode.SCENE_CHANGE.value)) != 0,
|
|
136
|
+
is_occupied = (event_mask & (1 << ZenEventCode.IS_OCCUPIED.value)) != 0,
|
|
137
|
+
system_variable_change = (event_mask & (1 << ZenEventCode.SYSTEM_VARIABLE_CHANGE.value)) != 0,
|
|
138
|
+
colour_change = (event_mask & (1 << ZenEventCode.COLOUR_CHANGE.value)) != 0,
|
|
139
|
+
profile_change = (event_mask & (1 << ZenEventCode.PROFILE_CHANGE.value)) != 0,
|
|
140
|
+
group_occupied = (event_mask & (1 << ZenEventCode.GROUP_OCCUPIED.value)) != 0,
|
|
141
|
+
level_change_v2 = (event_mask & (1 << ZenEventCode.LEVEL_CHANGE_V2.value)) != 0
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def bitmask(self) -> int:
|
|
145
|
+
event_mask = 0x00
|
|
146
|
+
if self.button_press: event_mask |= (1 << ZenEventCode.BUTTON_PRESS.value)
|
|
147
|
+
if self.button_hold: event_mask |= (1 << ZenEventCode.BUTTON_HOLD.value)
|
|
148
|
+
if self.absolute_input: event_mask |= (1 << ZenEventCode.ABSOLUTE_INPUT.value)
|
|
149
|
+
if self.level_change: event_mask |= (1 << ZenEventCode.LEVEL_CHANGE.value)
|
|
150
|
+
if self.group_level_change: event_mask |= (1 << ZenEventCode.GROUP_LEVEL_CHANGE.value)
|
|
151
|
+
if self.scene_change: event_mask |= (1 << ZenEventCode.SCENE_CHANGE.value)
|
|
152
|
+
if self.is_occupied: event_mask |= (1 << ZenEventCode.IS_OCCUPIED.value)
|
|
153
|
+
if self.system_variable_change: event_mask |= (1 << ZenEventCode.SYSTEM_VARIABLE_CHANGE.value)
|
|
154
|
+
if self.colour_change: event_mask |= (1 << ZenEventCode.COLOUR_CHANGE.value)
|
|
155
|
+
if self.profile_change: event_mask |= (1 << ZenEventCode.PROFILE_CHANGE.value)
|
|
156
|
+
if self.group_occupied: event_mask |= (1 << ZenEventCode.GROUP_OCCUPIED.value)
|
|
157
|
+
if self.level_change_v2: event_mask |= (1 << ZenEventCode.LEVEL_CHANGE_V2.value)
|
|
158
|
+
return event_mask
|
|
159
|
+
|
|
160
|
+
def upper(self) -> int:
|
|
161
|
+
return (self.bitmask() >> 8) & 0xFF
|
|
162
|
+
|
|
163
|
+
def lower(self) -> int:
|
|
164
|
+
return self.bitmask() & 0xFF
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# API-level constants
|
|
168
|
+
class Const:
|
|
169
|
+
"""API-level constants"""
|
|
170
|
+
# UDP protocol - use zen_protocol constants
|
|
171
|
+
RESPONSE_TIMEOUT = 3.0 # Default timeout from ClientConst
|
|
172
|
+
|
|
173
|
+
# DALI limits
|
|
174
|
+
MAX_ECG = 64 # 0-63
|
|
175
|
+
MAX_ECD = 64 # 0-63
|
|
176
|
+
MAX_INSTANCE = 32 # 0-31
|
|
177
|
+
MAX_GROUP = 16 # 0-15
|
|
178
|
+
MAX_SCENE = 12 # 0-11
|
|
179
|
+
MAX_SYSVAR = 148 # 0-147
|
|
180
|
+
MAX_LEVEL = 254 # 255 is mask value (i.e. no change)
|
|
181
|
+
MIN_KELVIN = 1000
|
|
182
|
+
MAX_KELVIN = 20000
|
|
183
|
+
|
|
184
|
+
# Color temperature defaults (only used if query_dali_colour_temp_limits fails)
|
|
185
|
+
DEFAULT_WARMEST_TEMP = 2700
|
|
186
|
+
DEFAULT_COOLEST_TEMP = 6500
|
|
187
|
+
|
|
188
|
+
# RGB channel counts
|
|
189
|
+
RGB_CHANNELS = 3
|
|
190
|
+
RGBW_CHANNELS = 4
|
|
191
|
+
RGBWW_CHANNELS = 5
|
|
192
|
+
|
|
193
|
+
# Button press constants
|
|
194
|
+
LONG_PRESS_COUNT = 2
|
|
195
|
+
DEFAULT_HOLD_TIME = 60
|
|
196
|
+
|
|
197
|
+
# Cache
|
|
198
|
+
CACHE_TIMEOUT = 1*60*60 # 1 hour
|
|
199
|
+
|
|
200
|
+
# Event-listener reconnect (ZenControl supervisor)
|
|
201
|
+
RECONNECT_MIN_DELAY = 1.0
|
|
202
|
+
RECONNECT_MAX_DELAY = 30.0
|
|
203
|
+
RECONNECT_HEALTHY_SECONDS = 60.0
|
zencontrol/exceptions.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ZenControl library exceptions.
|
|
3
|
+
|
|
4
|
+
This module defines all custom exceptions used throughout the library.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ZenError(Exception):
|
|
9
|
+
"""Base exception for Zen protocol errors"""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ZenTimeoutError(ZenError):
|
|
14
|
+
"""Raised when a command times out"""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ZenResponseError(ZenError):
|
|
19
|
+
"""Raised when receiving an invalid response"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ZenConnectionError(ZenError):
|
|
24
|
+
"""Raised when connection to controller fails"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ZenConfigurationError(ZenError):
|
|
29
|
+
"""Raised when configuration is invalid"""
|
|
30
|
+
pass
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level interface models and client.
|
|
3
|
+
|
|
4
|
+
This module contains models that belong to the zen_interface layer:
|
|
5
|
+
- ZenControl (main client for high-level usage)
|
|
6
|
+
- ZenLight, ZenGroup, ZenButton, etc. (high-level Pythonic objects)
|
|
7
|
+
- Business logic and convenience methods
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .interface import (
|
|
11
|
+
ZenControl,
|
|
12
|
+
ZenController,
|
|
13
|
+
ZenProfile,
|
|
14
|
+
ZenLight,
|
|
15
|
+
ZenGroup,
|
|
16
|
+
ZenButton,
|
|
17
|
+
ZenMotionSensor,
|
|
18
|
+
ZenSystemVariable,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# High-level client
|
|
23
|
+
"ZenControl",
|
|
24
|
+
|
|
25
|
+
# High-level models
|
|
26
|
+
"ZenController",
|
|
27
|
+
"ZenProfile",
|
|
28
|
+
"ZenLight",
|
|
29
|
+
"ZenGroup",
|
|
30
|
+
"ZenButton",
|
|
31
|
+
"ZenMotionSensor",
|
|
32
|
+
"ZenSystemVariable",
|
|
33
|
+
]
|