pyvlx 0.2.27__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.
- pyvlx/__init__.py +21 -0
- pyvlx/api/__init__.py +23 -0
- pyvlx/api/activate_scene.py +63 -0
- pyvlx/api/api_event.py +73 -0
- pyvlx/api/command_send.py +85 -0
- pyvlx/api/factory_default.py +34 -0
- pyvlx/api/frame_creation.py +202 -0
- pyvlx/api/frames/__init__.py +76 -0
- pyvlx/api/frames/alias_array.py +45 -0
- pyvlx/api/frames/frame.py +56 -0
- pyvlx/api/frames/frame_activate_scene.py +92 -0
- pyvlx/api/frames/frame_activation_log_updated.py +14 -0
- pyvlx/api/frames/frame_command_send.py +280 -0
- pyvlx/api/frames/frame_discover_nodes.py +64 -0
- pyvlx/api/frames/frame_error_notification.py +42 -0
- pyvlx/api/frames/frame_facory_default.py +32 -0
- pyvlx/api/frames/frame_get_all_nodes_information.py +218 -0
- pyvlx/api/frames/frame_get_limitation.py +127 -0
- pyvlx/api/frames/frame_get_local_time.py +38 -0
- pyvlx/api/frames/frame_get_network_setup.py +64 -0
- pyvlx/api/frames/frame_get_node_information.py +223 -0
- pyvlx/api/frames/frame_get_protocol_version.py +53 -0
- pyvlx/api/frames/frame_get_scene_list.py +82 -0
- pyvlx/api/frames/frame_get_state.py +47 -0
- pyvlx/api/frames/frame_get_version.py +72 -0
- pyvlx/api/frames/frame_helper.py +40 -0
- pyvlx/api/frames/frame_house_status_monitor_disable_cfm.py +14 -0
- pyvlx/api/frames/frame_house_status_monitor_disable_req.py +14 -0
- pyvlx/api/frames/frame_house_status_monitor_enable_cfm.py +14 -0
- pyvlx/api/frames/frame_house_status_monitor_enable_req.py +14 -0
- pyvlx/api/frames/frame_leave_learn_state.py +41 -0
- pyvlx/api/frames/frame_node_information_changed.py +57 -0
- pyvlx/api/frames/frame_node_state_position_changed_notification.py +84 -0
- pyvlx/api/frames/frame_password_change.py +114 -0
- pyvlx/api/frames/frame_password_enter.py +70 -0
- pyvlx/api/frames/frame_reboot.py +32 -0
- pyvlx/api/frames/frame_set_node_name.py +73 -0
- pyvlx/api/frames/frame_set_utc.py +45 -0
- pyvlx/api/frames/frame_status_request.py +212 -0
- pyvlx/api/get_all_nodes_information.py +46 -0
- pyvlx/api/get_limitation.py +64 -0
- pyvlx/api/get_local_time.py +34 -0
- pyvlx/api/get_network_setup.py +34 -0
- pyvlx/api/get_node_information.py +42 -0
- pyvlx/api/get_protocol_version.py +40 -0
- pyvlx/api/get_scene_list.py +49 -0
- pyvlx/api/get_state.py +43 -0
- pyvlx/api/get_version.py +34 -0
- pyvlx/api/house_status_monitor.py +52 -0
- pyvlx/api/leave_learn_state.py +33 -0
- pyvlx/api/password_enter.py +39 -0
- pyvlx/api/reboot.py +33 -0
- pyvlx/api/session_id.py +20 -0
- pyvlx/api/set_node_name.py +32 -0
- pyvlx/api/set_utc.py +31 -0
- pyvlx/api/status_request.py +48 -0
- pyvlx/config.py +54 -0
- pyvlx/connection.py +182 -0
- pyvlx/const.py +685 -0
- pyvlx/dataobjects.py +161 -0
- pyvlx/discovery.py +100 -0
- pyvlx/exception.py +26 -0
- pyvlx/heartbeat.py +79 -0
- pyvlx/klf200gateway.py +167 -0
- pyvlx/lightening_device.py +102 -0
- pyvlx/log.py +4 -0
- pyvlx/node.py +74 -0
- pyvlx/node_helper.py +165 -0
- pyvlx/node_updater.py +162 -0
- pyvlx/nodes.py +99 -0
- pyvlx/on_off_switch.py +44 -0
- pyvlx/opening_device.py +644 -0
- pyvlx/parameter.py +357 -0
- pyvlx/py.typed +0 -0
- pyvlx/pyvlx.py +124 -0
- pyvlx/scene.py +53 -0
- pyvlx/scenes.py +60 -0
- pyvlx/slip.py +48 -0
- pyvlx/string_helper.py +20 -0
- pyvlx-0.2.27.dist-info/METADATA +122 -0
- pyvlx-0.2.27.dist-info/RECORD +84 -0
- pyvlx-0.2.27.dist-info/WHEEL +5 -0
- pyvlx-0.2.27.dist-info/licenses/LICENSE +165 -0
- pyvlx-0.2.27.dist-info/top_level.txt +1 -0
pyvlx/parameter.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Module for Position class."""
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from .exception import PyVLXException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Parameter:
|
|
8
|
+
"""General object for storing parameters."""
|
|
9
|
+
|
|
10
|
+
UNKNOWN_VALUE = 0xF7FF # F7 FF
|
|
11
|
+
CURRENT = 0xD200 # D2 00
|
|
12
|
+
MAX = 0xC800 # C8 00
|
|
13
|
+
MIN = 0x0000 # 00 00
|
|
14
|
+
ON = 0x0000 # 00 00
|
|
15
|
+
OFF = 0xC800 # C8 00
|
|
16
|
+
TARGET = 0xD100 # D1 00
|
|
17
|
+
IGNORE = 0xD400 # D4 00
|
|
18
|
+
DUAL_SHUTTER_CURTAINS = 0xD808 # D8 08
|
|
19
|
+
|
|
20
|
+
def __init__(self, raw: Optional[bytes] = None):
|
|
21
|
+
"""Initialize Parameter class."""
|
|
22
|
+
self.raw = self.from_int(Position.UNKNOWN_VALUE)
|
|
23
|
+
if raw is not None:
|
|
24
|
+
self.raw = self.from_raw(raw)
|
|
25
|
+
|
|
26
|
+
def __bytes__(self) -> bytes:
|
|
27
|
+
"""Convert object in byte representation."""
|
|
28
|
+
return self.raw
|
|
29
|
+
|
|
30
|
+
def from_parameter(self, parameter: "Parameter") -> None:
|
|
31
|
+
"""Set internal raw state from parameter."""
|
|
32
|
+
if not isinstance(parameter, Parameter):
|
|
33
|
+
raise PyVLXException("parameter::from_parameter_wrong_object")
|
|
34
|
+
self.raw = parameter.raw
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def from_int(value: int) -> bytes:
|
|
38
|
+
"""Create raw out of position value."""
|
|
39
|
+
if not isinstance(value, int):
|
|
40
|
+
raise PyVLXException("value_has_to_be_int")
|
|
41
|
+
if not Parameter.is_valid_int(value):
|
|
42
|
+
raise PyVLXException("value_out_of_range")
|
|
43
|
+
return bytes([value >> 8 & 255, value & 255])
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def to_int(raw: bytes) -> int:
|
|
47
|
+
"""Create int position value out of raw."""
|
|
48
|
+
return raw[0] * 256 + raw[1]
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def is_valid_int(value: int) -> bool:
|
|
52
|
+
"""Test if value can be rendered out of int."""
|
|
53
|
+
if 0 <= value <= Parameter.MAX: # This includes ON and OFF
|
|
54
|
+
return True
|
|
55
|
+
valid_values = {
|
|
56
|
+
Parameter.UNKNOWN_VALUE,
|
|
57
|
+
Parameter.IGNORE,
|
|
58
|
+
Parameter.CURRENT,
|
|
59
|
+
Parameter.TARGET,
|
|
60
|
+
Parameter.DUAL_SHUTTER_CURTAINS,
|
|
61
|
+
}
|
|
62
|
+
if value in valid_values:
|
|
63
|
+
return True
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def from_raw(raw: bytes) -> bytes:
|
|
68
|
+
"""Test if raw packets are valid for initialization of Position."""
|
|
69
|
+
if not isinstance(raw, bytes):
|
|
70
|
+
raise PyVLXException("Position::raw_must_be_bytes")
|
|
71
|
+
if len(raw) != 2:
|
|
72
|
+
raise PyVLXException("Position::raw_must_be_two_bytes")
|
|
73
|
+
if (
|
|
74
|
+
raw != Position.from_int(Position.CURRENT)
|
|
75
|
+
and raw != Position.from_int(Position.IGNORE)
|
|
76
|
+
and raw != Position.from_int(Position.TARGET)
|
|
77
|
+
and raw != Position.from_int(Position.UNKNOWN_VALUE)
|
|
78
|
+
and Position.to_int(raw) > Position.MAX
|
|
79
|
+
):
|
|
80
|
+
return Position.from_int(Position.UNKNOWN_VALUE)
|
|
81
|
+
return raw
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def from_percent(percent: int) -> bytes:
|
|
85
|
+
"""Create raw value out of percent position."""
|
|
86
|
+
if not isinstance(percent, int):
|
|
87
|
+
raise PyVLXException("Position::percent_has_to_be_int")
|
|
88
|
+
if percent < 0:
|
|
89
|
+
raise PyVLXException("Position::percent_has_to_be_positive")
|
|
90
|
+
if percent > 100:
|
|
91
|
+
raise PyVLXException("Position::percent_out_of_range")
|
|
92
|
+
return bytes([percent * 2, 0])
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def to_percent(raw: bytes) -> int:
|
|
96
|
+
"""Create percent position value out of raw."""
|
|
97
|
+
# The first byte has the vlue from 0 to 200. Ignoring the second one.
|
|
98
|
+
# Adding 0.5 allows a slight tolerance for devices (e.g. Velux SML) that
|
|
99
|
+
# do not return exactly 51200 as final position when closed.
|
|
100
|
+
return int(raw[0] / 2 + 0.5)
|
|
101
|
+
|
|
102
|
+
def __eq__(self, other: object) -> bool:
|
|
103
|
+
"""Equal operator."""
|
|
104
|
+
if not isinstance(other, Parameter):
|
|
105
|
+
return NotImplemented
|
|
106
|
+
return self.raw == other.raw
|
|
107
|
+
|
|
108
|
+
def __str__(self) -> str:
|
|
109
|
+
"""Return string representation of object."""
|
|
110
|
+
if self.raw == self.from_int(Position.UNKNOWN_VALUE):
|
|
111
|
+
return "UNKNOWN"
|
|
112
|
+
if self.raw == self.from_int(Position.CURRENT):
|
|
113
|
+
return "CURRENT"
|
|
114
|
+
if self.raw == self.from_int(Position.TARGET):
|
|
115
|
+
return "TARGET"
|
|
116
|
+
if self.raw == self.from_int(Position.IGNORE):
|
|
117
|
+
return "IGNORE"
|
|
118
|
+
if self.raw == self.from_int(Position.DUAL_SHUTTER_CURTAINS):
|
|
119
|
+
return "DUAL"
|
|
120
|
+
return "{} %".format(int(self.to_percent(self.raw)))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class SwitchParameter(Parameter):
|
|
124
|
+
"""Class for storing On or Off values."""
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self, parameter: Optional[Parameter] = None, state: Optional[int] = None
|
|
128
|
+
):
|
|
129
|
+
"""Initialize Parameter class."""
|
|
130
|
+
super().__init__()
|
|
131
|
+
if parameter is not None:
|
|
132
|
+
self.from_parameter(parameter)
|
|
133
|
+
elif state is not None:
|
|
134
|
+
self.state = state
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def state(self) -> int:
|
|
138
|
+
"""Position property."""
|
|
139
|
+
return self.to_int(self.raw)
|
|
140
|
+
|
|
141
|
+
@state.setter
|
|
142
|
+
def state(self, state: int) -> None:
|
|
143
|
+
"""Setter of internal raw via state."""
|
|
144
|
+
self.raw = self.from_int(state)
|
|
145
|
+
|
|
146
|
+
def set_on(self) -> None:
|
|
147
|
+
"""Set parameter to 'on' state."""
|
|
148
|
+
self.raw = self.from_int(Parameter.ON)
|
|
149
|
+
|
|
150
|
+
def set_off(self) -> None:
|
|
151
|
+
"""Set parameter to 'off' state."""
|
|
152
|
+
self.raw = self.from_int(Parameter.OFF)
|
|
153
|
+
|
|
154
|
+
def is_on(self) -> bool:
|
|
155
|
+
"""Return True if parameter is in 'on' state."""
|
|
156
|
+
return self.raw == self.from_int(Parameter.ON)
|
|
157
|
+
|
|
158
|
+
def is_off(self) -> bool:
|
|
159
|
+
"""Return True if parameter is in 'off' state."""
|
|
160
|
+
return self.raw == self.from_int(Parameter.OFF)
|
|
161
|
+
|
|
162
|
+
def __str__(self) -> str:
|
|
163
|
+
"""Return string representation of object."""
|
|
164
|
+
if self.raw == self.from_int(Parameter.ON):
|
|
165
|
+
return "ON"
|
|
166
|
+
if self.raw == self.from_int(Parameter.OFF):
|
|
167
|
+
return "OFF"
|
|
168
|
+
return "UNKNOWN"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class SwitchParameterOn(SwitchParameter):
|
|
172
|
+
"""Switch Parameter in switched 'on' state."""
|
|
173
|
+
|
|
174
|
+
def __init__(self) -> None:
|
|
175
|
+
"""Initialize SwitchParameterOn class."""
|
|
176
|
+
super().__init__(state=Parameter.ON)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class SwitchParameterOff(SwitchParameter):
|
|
180
|
+
"""Switch Parameter in switched 'off' state."""
|
|
181
|
+
|
|
182
|
+
def __init__(self) -> None:
|
|
183
|
+
"""Initialize SwitchParameterOff class."""
|
|
184
|
+
super().__init__(state=Parameter.OFF)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Position(Parameter):
|
|
188
|
+
"""Class for storing a position."""
|
|
189
|
+
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
parameter: Optional[Parameter] = None,
|
|
193
|
+
position: Optional[int] = None,
|
|
194
|
+
position_percent: Optional[int] = None,
|
|
195
|
+
):
|
|
196
|
+
"""Initialize Position class."""
|
|
197
|
+
super().__init__()
|
|
198
|
+
if parameter is not None:
|
|
199
|
+
self.from_parameter(parameter)
|
|
200
|
+
elif position is not None:
|
|
201
|
+
self.position = position
|
|
202
|
+
elif position_percent is not None:
|
|
203
|
+
self.position_percent = position_percent
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def known(self) -> bool:
|
|
207
|
+
"""Known property, true if position is not in an unknown position."""
|
|
208
|
+
return self.raw != self.from_int(Position.UNKNOWN_VALUE)
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def open(self) -> bool:
|
|
212
|
+
"""Return true if position is set to fully open."""
|
|
213
|
+
return self.raw == self.from_int(Position.MIN)
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def closed(self) -> bool:
|
|
217
|
+
"""Return true if position is set to fully closed."""
|
|
218
|
+
# Consider closed even if raw is not exactly 51200 (tolerance for devices like Velux SML)
|
|
219
|
+
return self.to_percent(self.raw) == self.to_percent(self.from_int(Position.MAX))
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def position(self) -> int:
|
|
223
|
+
"""Position property."""
|
|
224
|
+
return self.to_int(self.raw)
|
|
225
|
+
|
|
226
|
+
@position.setter
|
|
227
|
+
def position(self, position: int) -> None:
|
|
228
|
+
"""Setter of internal raw via position."""
|
|
229
|
+
self.raw = self.from_int(position)
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def position_percent(self) -> int:
|
|
233
|
+
"""Position percent property."""
|
|
234
|
+
# unclear why it returns a <property object> here
|
|
235
|
+
return int(self.to_percent(self.raw))
|
|
236
|
+
|
|
237
|
+
@position_percent.setter
|
|
238
|
+
def position_percent(self, position_percent: int) -> None:
|
|
239
|
+
"""Setter of internal raw via percent position."""
|
|
240
|
+
self.raw = self.from_percent(percent=position_percent)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class UnknownPosition(Position):
|
|
244
|
+
"""Unknown position."""
|
|
245
|
+
|
|
246
|
+
def __init__(self) -> None:
|
|
247
|
+
"""Initialize UnknownPosition class."""
|
|
248
|
+
super().__init__(position=Position.UNKNOWN_VALUE)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class CurrentPosition(Position):
|
|
252
|
+
"""Current position, used to stop devices."""
|
|
253
|
+
|
|
254
|
+
def __init__(self) -> None:
|
|
255
|
+
"""Initialize CurrentPosition class."""
|
|
256
|
+
super().__init__(position=Position.CURRENT)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class TargetPosition(Position):
|
|
260
|
+
"""Class for using a target position."""
|
|
261
|
+
|
|
262
|
+
def __init__(self) -> None:
|
|
263
|
+
"""Initialize TargetPosition class."""
|
|
264
|
+
super().__init__(position=Position.TARGET)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class IgnorePosition(Position):
|
|
268
|
+
"""The Ignore is used where a parameter in the frame is to be ignored."""
|
|
269
|
+
|
|
270
|
+
def __init__(self) -> None:
|
|
271
|
+
"""Initialize CurrentPosition class."""
|
|
272
|
+
super().__init__(position=Position.IGNORE)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class Intensity(Parameter):
|
|
276
|
+
"""Class for storing an intensity."""
|
|
277
|
+
|
|
278
|
+
def __init__(
|
|
279
|
+
self,
|
|
280
|
+
parameter: Optional[Parameter] = None,
|
|
281
|
+
intensity: Optional[int] = None,
|
|
282
|
+
intensity_percent: Optional[int] = None,
|
|
283
|
+
):
|
|
284
|
+
"""Initialize Intensity class."""
|
|
285
|
+
super().__init__()
|
|
286
|
+
if parameter is not None:
|
|
287
|
+
self.from_parameter(parameter)
|
|
288
|
+
elif intensity is not None:
|
|
289
|
+
self.intensity = intensity
|
|
290
|
+
elif intensity_percent is not None:
|
|
291
|
+
self.intensity_percent = intensity_percent
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def known(self) -> bool:
|
|
295
|
+
"""Known property, true if intensity is not in an unknown intensity."""
|
|
296
|
+
return self.raw != self.from_int(Intensity.UNKNOWN_VALUE)
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def on(self) -> bool: # pylint: disable=invalid-name
|
|
300
|
+
"""Return true if intensity is set to fully turn on."""
|
|
301
|
+
return self.raw == self.from_int(Intensity.MIN)
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def off(self) -> bool:
|
|
305
|
+
"""Return true if intensity is set to fully turn off."""
|
|
306
|
+
return self.raw == bytes([self.MAX >> 8 & 255, self.MAX & 255])
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def intensity(self) -> int:
|
|
310
|
+
"""Intensity property."""
|
|
311
|
+
return self.to_int(self.raw)
|
|
312
|
+
|
|
313
|
+
@intensity.setter
|
|
314
|
+
def intensity(self, intensity: int) -> None:
|
|
315
|
+
"""Setter of internal raw via intensity."""
|
|
316
|
+
self.raw = self.from_int(intensity)
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def intensity_percent(self) -> int:
|
|
320
|
+
"""Intensity percent property."""
|
|
321
|
+
# unclear why it returns a <property object> here
|
|
322
|
+
return int(self.to_percent(self.raw))
|
|
323
|
+
|
|
324
|
+
@intensity_percent.setter
|
|
325
|
+
def intensity_percent(self, intensity_percent: int) -> None:
|
|
326
|
+
"""Setter of internal raw via percent intensity."""
|
|
327
|
+
self.raw = self.from_percent(percent=intensity_percent)
|
|
328
|
+
|
|
329
|
+
def __str__(self) -> str:
|
|
330
|
+
"""Return string representation of object."""
|
|
331
|
+
if self.raw == self.from_int(Intensity.UNKNOWN_VALUE):
|
|
332
|
+
return "UNKNOWN"
|
|
333
|
+
return "{} %".format(self.intensity_percent)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class UnknownIntensity(Intensity):
|
|
337
|
+
"""Unknown intensity."""
|
|
338
|
+
|
|
339
|
+
def __init__(self) -> None:
|
|
340
|
+
"""Initialize UnknownIntensity class."""
|
|
341
|
+
super().__init__(intensity=Intensity.UNKNOWN_VALUE)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class CurrentIntensity(Intensity):
|
|
345
|
+
"""Current intensity, used to stop devices."""
|
|
346
|
+
|
|
347
|
+
def __init__(self) -> None:
|
|
348
|
+
"""Initialize CurrentIntensity class."""
|
|
349
|
+
super().__init__(intensity=Intensity.CURRENT)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class DualRollerShutterPosition(Position):
|
|
353
|
+
"""Position to be provided when addressing the upper or lower curtain of a dual roller shutter by using FP1 or FP2."""
|
|
354
|
+
|
|
355
|
+
def __init__(self) -> None:
|
|
356
|
+
"""Initialize CurrentPosition class."""
|
|
357
|
+
super().__init__(position=Position.DUAL_SHUTTER_CURTAINS)
|
pyvlx/py.typed
ADDED
|
File without changes
|
pyvlx/pyvlx.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for PyVLX object.
|
|
3
|
+
|
|
4
|
+
PyVLX is an asynchronous library for connecting to
|
|
5
|
+
a VELUX KLF 200 device for controlling window openers
|
|
6
|
+
and roller shutters.
|
|
7
|
+
"""
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .api import get_limitation
|
|
12
|
+
from .api.frames import FrameBase
|
|
13
|
+
from .config import Config
|
|
14
|
+
from .connection import Connection
|
|
15
|
+
from .exception import PyVLXException
|
|
16
|
+
from .heartbeat import Heartbeat
|
|
17
|
+
from .klf200gateway import Klf200Gateway
|
|
18
|
+
from .log import PYVLXLOG
|
|
19
|
+
from .node_updater import NodeUpdater
|
|
20
|
+
from .nodes import Nodes
|
|
21
|
+
from .scenes import Scenes
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PyVLX:
|
|
25
|
+
"""Class for PyVLX."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
path: Optional[str] = None,
|
|
30
|
+
host: Optional[str] = None,
|
|
31
|
+
password: Optional[str] = None,
|
|
32
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
33
|
+
heartbeat_interval: int = 30,
|
|
34
|
+
heartbeat_load_all_states: bool = True,
|
|
35
|
+
):
|
|
36
|
+
"""Initialize PyVLX class."""
|
|
37
|
+
self.loop = loop or asyncio.get_event_loop()
|
|
38
|
+
self.config = Config(self, path, host, password)
|
|
39
|
+
self.connection = Connection(loop=self.loop, config=self.config)
|
|
40
|
+
self.heartbeat = Heartbeat(
|
|
41
|
+
pyvlx=self,
|
|
42
|
+
interval=heartbeat_interval,
|
|
43
|
+
load_all_states=heartbeat_load_all_states,
|
|
44
|
+
)
|
|
45
|
+
self.node_updater = NodeUpdater(pyvlx=self)
|
|
46
|
+
self.nodes = Nodes(self)
|
|
47
|
+
self.connection.register_frame_received_cb(self.node_updater.process_frame)
|
|
48
|
+
|
|
49
|
+
self.scenes = Scenes(self)
|
|
50
|
+
self.version = None
|
|
51
|
+
self.protocol_version = None
|
|
52
|
+
self.klf200 = Klf200Gateway(pyvlx=self)
|
|
53
|
+
self.api_call_semaphore = asyncio.Semaphore(1) # Limit parallel commands
|
|
54
|
+
|
|
55
|
+
async def connect(self) -> None:
|
|
56
|
+
"""Connect to KLF 200."""
|
|
57
|
+
PYVLXLOG.debug("Connecting to KLF 200")
|
|
58
|
+
await self.connection.connect()
|
|
59
|
+
assert self.config.password is not None
|
|
60
|
+
await self.klf200.password_enter(password=self.config.password)
|
|
61
|
+
await self.klf200.get_version()
|
|
62
|
+
await self.klf200.get_protocol_version()
|
|
63
|
+
PYVLXLOG.debug(
|
|
64
|
+
"Connected to: %s, %s",
|
|
65
|
+
str(self.klf200.version),
|
|
66
|
+
str(self.klf200.protocol_version),
|
|
67
|
+
)
|
|
68
|
+
await self.klf200.house_status_monitor_disable(pyvlx=self)
|
|
69
|
+
await self.klf200.get_state()
|
|
70
|
+
await self.klf200.set_utc()
|
|
71
|
+
await self.klf200.get_network_setup()
|
|
72
|
+
await self.klf200.house_status_monitor_enable(pyvlx=self)
|
|
73
|
+
self.heartbeat.start()
|
|
74
|
+
|
|
75
|
+
async def reboot_gateway(self) -> None:
|
|
76
|
+
"""For Compatibility: Reboot the KLF 200."""
|
|
77
|
+
if not self.get_connected():
|
|
78
|
+
PYVLXLOG.warning("KLF 200 reboot initiated, but gateway is not connected")
|
|
79
|
+
else:
|
|
80
|
+
PYVLXLOG.warning("KLF 200 reboot initiated")
|
|
81
|
+
await self.klf200.reboot()
|
|
82
|
+
|
|
83
|
+
def get_connected(self) -> bool:
|
|
84
|
+
"""Return whether the gateway is currently connected."""
|
|
85
|
+
return self.connection.connected
|
|
86
|
+
|
|
87
|
+
async def check_connected(self) -> None:
|
|
88
|
+
"""Check we're connected, and if not, connect."""
|
|
89
|
+
if not self.connection.connected:
|
|
90
|
+
await self.connect()
|
|
91
|
+
|
|
92
|
+
async def send_frame(self, frame: FrameBase) -> None:
|
|
93
|
+
"""Send frame to API via connection."""
|
|
94
|
+
await self.check_connected()
|
|
95
|
+
self.connection.write(frame)
|
|
96
|
+
|
|
97
|
+
async def disconnect(self) -> None:
|
|
98
|
+
"""Disconnect from KLF 200."""
|
|
99
|
+
await self.heartbeat.stop()
|
|
100
|
+
if self.connection.connected:
|
|
101
|
+
try:
|
|
102
|
+
# If the connection will be closed while house status monitor is enabled, a reconnection will fail on SSL handshake.
|
|
103
|
+
if self.klf200.house_status_monitor_enabled:
|
|
104
|
+
await self.klf200.house_status_monitor_disable(pyvlx=self, timeout=5)
|
|
105
|
+
# Reboot KLF200 when disconnecting to avoid unresponsive KLF200.
|
|
106
|
+
await self.klf200.reboot()
|
|
107
|
+
except (OSError, PyVLXException):
|
|
108
|
+
PYVLXLOG.exception("Error during disconnect preparations")
|
|
109
|
+
self.connection.disconnect()
|
|
110
|
+
if self.connection.tasks:
|
|
111
|
+
await asyncio.gather(*self.connection.tasks) # Wait for all tasks to finish
|
|
112
|
+
|
|
113
|
+
async def load_nodes(self, node_id: Optional[int] = None) -> None:
|
|
114
|
+
"""Load devices from KLF 200, if no node_id is specified all nodes are loaded."""
|
|
115
|
+
await self.nodes.load(node_id)
|
|
116
|
+
|
|
117
|
+
async def load_scenes(self) -> None:
|
|
118
|
+
"""Load scenes from KLF 200."""
|
|
119
|
+
await self.scenes.load()
|
|
120
|
+
|
|
121
|
+
async def get_limitation(self, node_id: int) -> None:
|
|
122
|
+
"""Return limitation."""
|
|
123
|
+
limit = get_limitation.GetLimitation(self, node_id)
|
|
124
|
+
await limit.do_api_call()
|
pyvlx/scene.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Module for scene."""
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
from .api import ActivateScene
|
|
5
|
+
from .exception import PyVLXException
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pyvlx import PyVLX
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Scene:
|
|
12
|
+
"""Object for scene."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, pyvlx: "PyVLX", scene_id: int, name: str):
|
|
15
|
+
"""Initialize Scene object.
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
* pyvlx: PyVLX object
|
|
19
|
+
* scene_id: internal id for addressing scenes.
|
|
20
|
+
Provided by KLF 200 device
|
|
21
|
+
* name: scene name
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
self.pyvlx = pyvlx
|
|
25
|
+
self.scene_id = scene_id
|
|
26
|
+
self.name = name
|
|
27
|
+
|
|
28
|
+
async def run(self, wait_for_completion: bool = True) -> None:
|
|
29
|
+
"""Run scene.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
* wait_for_completion: If set, function will return
|
|
33
|
+
after device has reached target position.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
activate_scene = ActivateScene(
|
|
37
|
+
pyvlx=self.pyvlx,
|
|
38
|
+
wait_for_completion=wait_for_completion,
|
|
39
|
+
scene_id=self.scene_id,
|
|
40
|
+
)
|
|
41
|
+
await activate_scene.do_api_call()
|
|
42
|
+
if not activate_scene.success:
|
|
43
|
+
raise PyVLXException("Unable to activate scene")
|
|
44
|
+
|
|
45
|
+
def __str__(self) -> str:
|
|
46
|
+
"""Return object as readable string."""
|
|
47
|
+
return '<{} name="{}" id="{}"/>'.format(
|
|
48
|
+
type(self).__name__, self.name, self.scene_id
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def __eq__(self, other: Any) -> bool:
|
|
52
|
+
"""Equal operator."""
|
|
53
|
+
return self.__dict__ == other.__dict__
|
pyvlx/scenes.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Module for storing and accessing scene list."""
|
|
2
|
+
from typing import TYPE_CHECKING, Iterator, List, Union
|
|
3
|
+
|
|
4
|
+
from .api import GetSceneList
|
|
5
|
+
from .exception import PyVLXException
|
|
6
|
+
from .scene import Scene
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pyvlx import PyVLX
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Scenes:
|
|
13
|
+
"""Class for storing and accessing ."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, pyvlx: "PyVLX"):
|
|
16
|
+
"""Initialize Scenes class."""
|
|
17
|
+
self.pyvlx = pyvlx
|
|
18
|
+
self.__scenes: List[Scene] = []
|
|
19
|
+
|
|
20
|
+
def __iter__(self) -> Iterator[Scene]:
|
|
21
|
+
"""Iterate."""
|
|
22
|
+
yield from self.__scenes
|
|
23
|
+
|
|
24
|
+
def __getitem__(self, key: Union[str, int]) -> Scene:
|
|
25
|
+
"""Return scene by name or by index."""
|
|
26
|
+
if isinstance(key, int):
|
|
27
|
+
for scene in self.__scenes:
|
|
28
|
+
if scene.scene_id == key:
|
|
29
|
+
return scene
|
|
30
|
+
for scene in self.__scenes:
|
|
31
|
+
if scene.name == key:
|
|
32
|
+
return scene
|
|
33
|
+
raise KeyError
|
|
34
|
+
|
|
35
|
+
def __len__(self) -> int:
|
|
36
|
+
"""Return number of scenes."""
|
|
37
|
+
return len(self.__scenes)
|
|
38
|
+
|
|
39
|
+
def add(self, scene: Scene) -> None:
|
|
40
|
+
"""Add scene, replace existing scene if scene with scene_id is present."""
|
|
41
|
+
if not isinstance(scene, Scene):
|
|
42
|
+
raise TypeError()
|
|
43
|
+
for i, j in enumerate(self.__scenes):
|
|
44
|
+
if j.scene_id == scene.scene_id:
|
|
45
|
+
self.__scenes[i] = scene
|
|
46
|
+
return
|
|
47
|
+
self.__scenes.append(scene)
|
|
48
|
+
|
|
49
|
+
def clear(self) -> None:
|
|
50
|
+
"""Clear internal scenes array."""
|
|
51
|
+
self.__scenes = []
|
|
52
|
+
|
|
53
|
+
async def load(self) -> None:
|
|
54
|
+
"""Load scenes from KLF 200."""
|
|
55
|
+
get_scene_list = GetSceneList(pyvlx=self.pyvlx)
|
|
56
|
+
await get_scene_list.do_api_call()
|
|
57
|
+
if not get_scene_list.success:
|
|
58
|
+
raise PyVLXException("Unable to retrieve scene information")
|
|
59
|
+
for scene in get_scene_list.scenes:
|
|
60
|
+
self.add(Scene(pyvlx=self.pyvlx, scene_id=scene[0], name=scene[1]))
|
pyvlx/slip.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Module for Serial Line Internet Protocol (SLIP)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
|
|
5
|
+
SLIP_END = 0xC0
|
|
6
|
+
SLIP_ESC = 0xDB
|
|
7
|
+
SLIP_ESC_END = 0xDC
|
|
8
|
+
SLIP_ESC_ESC = 0xDD
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_slip(raw: bytes) -> bool:
|
|
12
|
+
"""Check if raw is a SLIP packet."""
|
|
13
|
+
if len(raw) < 2:
|
|
14
|
+
return False
|
|
15
|
+
return raw[0] == SLIP_END and SLIP_END in raw[1:]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def decode(raw: bytes) -> bytes:
|
|
19
|
+
"""Decode SLIP message."""
|
|
20
|
+
return raw.replace(bytes([SLIP_ESC, SLIP_ESC_END]), bytes([SLIP_END])).replace(
|
|
21
|
+
bytes([SLIP_ESC, SLIP_ESC_ESC]), bytes([SLIP_ESC])
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def encode(raw: bytes) -> bytes:
|
|
26
|
+
"""Encode SLIP message."""
|
|
27
|
+
return raw.replace(bytes([SLIP_ESC]), bytes([SLIP_ESC, SLIP_ESC_ESC])).replace(
|
|
28
|
+
bytes([SLIP_END]), bytes([SLIP_ESC, SLIP_ESC_END])
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_next_slip(raw: bytes) -> Tuple[Optional[bytes], bytes]:
|
|
33
|
+
"""
|
|
34
|
+
Get the next slip packet from raw data.
|
|
35
|
+
|
|
36
|
+
Returns the extracted packet plus the raw data with the remaining data stream.
|
|
37
|
+
"""
|
|
38
|
+
if not is_slip(raw):
|
|
39
|
+
return None, raw
|
|
40
|
+
length = raw[1:].index(SLIP_END)
|
|
41
|
+
slip_packet = decode(raw[1 : length + 1])
|
|
42
|
+
new_raw = raw[length + 2 :]
|
|
43
|
+
return slip_packet, new_raw
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def slip_pack(raw: bytes) -> bytes:
|
|
47
|
+
"""Pack raw message to complete slip message."""
|
|
48
|
+
return bytes([SLIP_END]) + encode(raw) + bytes([SLIP_END])
|
pyvlx/string_helper.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Module for string encoding, decoding."""
|
|
2
|
+
from .exception import PyVLXException
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def string_to_bytes(string: str, size: int) -> bytes:
|
|
6
|
+
"""Convert string to bytes add padding."""
|
|
7
|
+
if len(string) > size:
|
|
8
|
+
raise PyVLXException("string_to_bytes::string_to_large")
|
|
9
|
+
encoded = bytes(string, encoding="utf-8")
|
|
10
|
+
return encoded + bytes(size - len(encoded))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def bytes_to_string(raw: bytes) -> str:
|
|
14
|
+
"""Convert bytes to string."""
|
|
15
|
+
ret = bytes()
|
|
16
|
+
for byte in raw:
|
|
17
|
+
if byte == 0x00:
|
|
18
|
+
return ret.decode("utf-8")
|
|
19
|
+
ret += bytes([byte])
|
|
20
|
+
return ret.decode("utf-8")
|