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.
Files changed (84) hide show
  1. pyvlx/__init__.py +21 -0
  2. pyvlx/api/__init__.py +23 -0
  3. pyvlx/api/activate_scene.py +63 -0
  4. pyvlx/api/api_event.py +73 -0
  5. pyvlx/api/command_send.py +85 -0
  6. pyvlx/api/factory_default.py +34 -0
  7. pyvlx/api/frame_creation.py +202 -0
  8. pyvlx/api/frames/__init__.py +76 -0
  9. pyvlx/api/frames/alias_array.py +45 -0
  10. pyvlx/api/frames/frame.py +56 -0
  11. pyvlx/api/frames/frame_activate_scene.py +92 -0
  12. pyvlx/api/frames/frame_activation_log_updated.py +14 -0
  13. pyvlx/api/frames/frame_command_send.py +280 -0
  14. pyvlx/api/frames/frame_discover_nodes.py +64 -0
  15. pyvlx/api/frames/frame_error_notification.py +42 -0
  16. pyvlx/api/frames/frame_facory_default.py +32 -0
  17. pyvlx/api/frames/frame_get_all_nodes_information.py +218 -0
  18. pyvlx/api/frames/frame_get_limitation.py +127 -0
  19. pyvlx/api/frames/frame_get_local_time.py +38 -0
  20. pyvlx/api/frames/frame_get_network_setup.py +64 -0
  21. pyvlx/api/frames/frame_get_node_information.py +223 -0
  22. pyvlx/api/frames/frame_get_protocol_version.py +53 -0
  23. pyvlx/api/frames/frame_get_scene_list.py +82 -0
  24. pyvlx/api/frames/frame_get_state.py +47 -0
  25. pyvlx/api/frames/frame_get_version.py +72 -0
  26. pyvlx/api/frames/frame_helper.py +40 -0
  27. pyvlx/api/frames/frame_house_status_monitor_disable_cfm.py +14 -0
  28. pyvlx/api/frames/frame_house_status_monitor_disable_req.py +14 -0
  29. pyvlx/api/frames/frame_house_status_monitor_enable_cfm.py +14 -0
  30. pyvlx/api/frames/frame_house_status_monitor_enable_req.py +14 -0
  31. pyvlx/api/frames/frame_leave_learn_state.py +41 -0
  32. pyvlx/api/frames/frame_node_information_changed.py +57 -0
  33. pyvlx/api/frames/frame_node_state_position_changed_notification.py +84 -0
  34. pyvlx/api/frames/frame_password_change.py +114 -0
  35. pyvlx/api/frames/frame_password_enter.py +70 -0
  36. pyvlx/api/frames/frame_reboot.py +32 -0
  37. pyvlx/api/frames/frame_set_node_name.py +73 -0
  38. pyvlx/api/frames/frame_set_utc.py +45 -0
  39. pyvlx/api/frames/frame_status_request.py +212 -0
  40. pyvlx/api/get_all_nodes_information.py +46 -0
  41. pyvlx/api/get_limitation.py +64 -0
  42. pyvlx/api/get_local_time.py +34 -0
  43. pyvlx/api/get_network_setup.py +34 -0
  44. pyvlx/api/get_node_information.py +42 -0
  45. pyvlx/api/get_protocol_version.py +40 -0
  46. pyvlx/api/get_scene_list.py +49 -0
  47. pyvlx/api/get_state.py +43 -0
  48. pyvlx/api/get_version.py +34 -0
  49. pyvlx/api/house_status_monitor.py +52 -0
  50. pyvlx/api/leave_learn_state.py +33 -0
  51. pyvlx/api/password_enter.py +39 -0
  52. pyvlx/api/reboot.py +33 -0
  53. pyvlx/api/session_id.py +20 -0
  54. pyvlx/api/set_node_name.py +32 -0
  55. pyvlx/api/set_utc.py +31 -0
  56. pyvlx/api/status_request.py +48 -0
  57. pyvlx/config.py +54 -0
  58. pyvlx/connection.py +182 -0
  59. pyvlx/const.py +685 -0
  60. pyvlx/dataobjects.py +161 -0
  61. pyvlx/discovery.py +100 -0
  62. pyvlx/exception.py +26 -0
  63. pyvlx/heartbeat.py +79 -0
  64. pyvlx/klf200gateway.py +167 -0
  65. pyvlx/lightening_device.py +102 -0
  66. pyvlx/log.py +4 -0
  67. pyvlx/node.py +74 -0
  68. pyvlx/node_helper.py +165 -0
  69. pyvlx/node_updater.py +162 -0
  70. pyvlx/nodes.py +99 -0
  71. pyvlx/on_off_switch.py +44 -0
  72. pyvlx/opening_device.py +644 -0
  73. pyvlx/parameter.py +357 -0
  74. pyvlx/py.typed +0 -0
  75. pyvlx/pyvlx.py +124 -0
  76. pyvlx/scene.py +53 -0
  77. pyvlx/scenes.py +60 -0
  78. pyvlx/slip.py +48 -0
  79. pyvlx/string_helper.py +20 -0
  80. pyvlx-0.2.27.dist-info/METADATA +122 -0
  81. pyvlx-0.2.27.dist-info/RECORD +84 -0
  82. pyvlx-0.2.27.dist-info/WHEEL +5 -0
  83. pyvlx-0.2.27.dist-info/licenses/LICENSE +165 -0
  84. 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")