python-broadlink 1.0.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.
broadlink/climate.py ADDED
@@ -0,0 +1,474 @@
1
+ """Support for climate control."""
2
+ import enum
3
+ import struct
4
+ from typing import List, Sequence
5
+
6
+ from . import exceptions as e
7
+ from .device import Device
8
+ from .helpers import CRC16
9
+
10
+
11
+ class hysen(Device):
12
+ """Controls a Hysen heating thermostat.
13
+
14
+ This device is manufactured by Hysen and sold under different
15
+ brands, including Floureon, Beca Energy, Beok and Decdeal.
16
+
17
+ Supported models:
18
+ - HY02B05H
19
+ - HY03WE
20
+ """
21
+
22
+ TYPE = "HYS"
23
+
24
+ async def send_request(self, request: Sequence[int]) -> bytes:
25
+ """Send a request to the device."""
26
+ packet = bytearray()
27
+ packet.extend((len(request) + 2).to_bytes(2, "little"))
28
+ packet.extend(request)
29
+ packet.extend(CRC16.calculate(request).to_bytes(2, "little"))
30
+
31
+ response = await self.send_packet(0x6A, packet)
32
+ e.check_error(response[0x22:0x24])
33
+ payload = self.decrypt(response[0x38:])
34
+
35
+ p_len = int.from_bytes(payload[:0x02], "little")
36
+ nom_crc = int.from_bytes(payload[p_len:p_len+2], "little")
37
+ real_crc = CRC16.calculate(payload[0x02:p_len])
38
+
39
+ if nom_crc != real_crc:
40
+ raise e.DataValidationError(
41
+ -4008,
42
+ "Received data packet check error",
43
+ f"Expected a checksum of {nom_crc} and received {real_crc}",
44
+ )
45
+
46
+ return payload[0x02:p_len]
47
+
48
+ def _decode_temp(self, payload, base_index):
49
+ base_temp = payload[base_index] / 2.0
50
+ add_offset = (payload[4] >> 3) & 1 # should offset be added?
51
+ offset_raw_value = (payload[17] >> 4) & 3 # offset value
52
+ offset = (offset_raw_value + 1) / 10 if add_offset else 0.0
53
+ return base_temp + offset
54
+
55
+ async def get_temp(self) -> float:
56
+ """Return the room temperature in degrees celsius."""
57
+ payload = await self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])
58
+ return self._decode_temp(payload, 5)
59
+
60
+ async def get_external_temp(self) -> float:
61
+ """Return the external temperature in degrees celsius."""
62
+ payload = await self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x08])
63
+ return self._decode_temp(payload, 18)
64
+
65
+ async def get_full_status(self) -> dict:
66
+ """Return the state of the device.
67
+
68
+ Timer schedule included.
69
+ """
70
+ payload = await self.send_request([0x01, 0x03, 0x00, 0x00, 0x00, 0x16])
71
+ data = {}
72
+ data["remote_lock"] = payload[3] & 1
73
+ data["power"] = payload[4] & 1
74
+ data["active"] = (payload[4] >> 4) & 1
75
+ data["temp_manual"] = (payload[4] >> 6) & 1
76
+ data["heating_cooling"] = (payload[4] >> 7) & 1
77
+ data["room_temp"] = self._decode_temp(payload, 5)
78
+ data["thermostat_temp"] = payload[6] / 2.0
79
+ data["auto_mode"] = payload[7] & 0x0F
80
+ data["loop_mode"] = payload[7] >> 4
81
+ data["sensor"] = payload[8]
82
+ data["osv"] = payload[9]
83
+ data["dif"] = payload[10]
84
+ data["svh"] = payload[11]
85
+ data["svl"] = payload[12]
86
+ data["room_temp_adj"] = (
87
+ int.from_bytes(payload[13:15], "big", signed=True) / 10.0
88
+ )
89
+ data["fre"] = payload[15]
90
+ data["poweron"] = payload[16]
91
+ data["unknown"] = payload[17]
92
+ data["external_temp"] = self._decode_temp(payload, 18)
93
+ data["hour"] = payload[19]
94
+ data["min"] = payload[20]
95
+ data["sec"] = payload[21]
96
+ data["dayofweek"] = payload[22]
97
+
98
+ weekday = []
99
+ for i in range(0, 6):
100
+ weekday.append(
101
+ {
102
+ "start_hour": payload[2 * i + 23],
103
+ "start_minute": payload[2 * i + 24],
104
+ "temp": payload[i + 39] / 2.0,
105
+ }
106
+ )
107
+
108
+ data["weekday"] = weekday
109
+ weekend = []
110
+ for i in range(6, 8):
111
+ weekend.append(
112
+ {
113
+ "start_hour": payload[2 * i + 23],
114
+ "start_minute": payload[2 * i + 24],
115
+ "temp": payload[i + 39] / 2.0,
116
+ }
117
+ )
118
+
119
+ data["weekend"] = weekend
120
+ return data
121
+
122
+ # Change controller mode
123
+ # auto_mode = 1 for auto (scheduled/timed) mode, 0 for manual mode.
124
+ # Manual mode will activate last used temperature.
125
+ # In typical usage call set_temp to activate manual control and set temp.
126
+ # loop_mode refers to index in [ "12345,67", "123456,7", "1234567" ]
127
+ # E.g. loop_mode = 0 ("12345,67") means Saturday and Sunday (weekend schedule)
128
+ # loop_mode = 2 ("1234567") means every day, including Saturday and Sunday (weekday schedule)
129
+ # The sensor command is currently experimental
130
+ async def set_mode(
131
+ self, auto_mode: int, loop_mode: int, sensor: int = 0
132
+ ) -> None:
133
+ """Set the mode of the device."""
134
+ mode_byte = ((loop_mode + 1) << 4) + auto_mode
135
+ await self.send_request([0x01, 0x06, 0x00, 0x02, mode_byte, sensor])
136
+
137
+ # Advanced settings
138
+ # Sensor mode (SEN) sensor = 0 for internal sensor, 1 for external sensor,
139
+ # 2 for internal control temperature, external limit temperature. Factory default: 0.
140
+ # Set temperature range for external sensor (OSV) osv = 5..99. Factory default: 42C
141
+ # Deadzone for floor temprature (dIF) dif = 1..9. Factory default: 2C
142
+ # Upper temperature limit for internal sensor (SVH) svh = 5..99. Factory default: 35C
143
+ # Lower temperature limit for internal sensor (SVL) svl = 5..99. Factory default: 5C
144
+ # Actual temperature calibration (AdJ) adj = -0.5. Precision 0.1C
145
+ # Anti-freezing function (FrE) fre = 0 for anti-freezing function shut down,
146
+ # 1 for anti-freezing function open. Factory default: 0
147
+ # Power on memory (POn) poweron = 0 for off, 1 for on. Default: 0
148
+ async def set_advanced(
149
+ self,
150
+ loop_mode: int,
151
+ sensor: int,
152
+ osv: int,
153
+ dif: int,
154
+ svh: int,
155
+ svl: int,
156
+ adj: float,
157
+ fre: int,
158
+ poweron: int,
159
+ ) -> None:
160
+ """Set advanced options."""
161
+ await self.send_request(
162
+ [
163
+ 0x01,
164
+ 0x10,
165
+ 0x00,
166
+ 0x02,
167
+ 0x00,
168
+ 0x05,
169
+ 0x0A,
170
+ loop_mode,
171
+ sensor,
172
+ osv,
173
+ dif,
174
+ svh,
175
+ svl,
176
+ int(adj * 10) >> 8 & 0xFF,
177
+ int(adj * 10) & 0xFF,
178
+ fre,
179
+ poweron,
180
+ ]
181
+ )
182
+
183
+ # For backwards compatibility only. Prefer calling set_mode directly.
184
+ # Note this function invokes loop_mode=0 and sensor=0.
185
+ async def switch_to_auto(self) -> None:
186
+ """Switch mode to auto."""
187
+ await self.set_mode(auto_mode=1, loop_mode=0)
188
+
189
+ async def switch_to_manual(self) -> None:
190
+ """Switch mode to manual."""
191
+ await self.set_mode(auto_mode=0, loop_mode=0)
192
+
193
+ # Set temperature for manual mode (also activates manual mode if currently in automatic)
194
+ async def set_temp(self, temp: float) -> None:
195
+ """Set the target temperature."""
196
+ await self.send_request([0x01, 0x06, 0x00, 0x01, 0x00, int(temp * 2)])
197
+
198
+ # Set device on(1) or off(0), does not deactivate Wifi connectivity.
199
+ # Remote lock disables control by buttons on thermostat.
200
+ # heating_cooling: heating(0) cooling(1)
201
+ async def set_power(
202
+ self, power: int = 1, remote_lock: int = 0, heating_cooling: int = 0
203
+ ) -> None:
204
+ """Set the power state of the device."""
205
+ state = (heating_cooling << 7) + power
206
+ await self.send_request([0x01, 0x06, 0x00, 0x00, remote_lock, state])
207
+
208
+ # set time on device
209
+ # n.b. day=1 is Monday, ..., day=7 is Sunday
210
+ async def set_time(self, hour: int, minute: int, second: int, day: int) -> None:
211
+ """Set the time."""
212
+ await self.send_request(
213
+ [
214
+ 0x01,
215
+ 0x10,
216
+ 0x00,
217
+ 0x08,
218
+ 0x00,
219
+ 0x02,
220
+ 0x04,
221
+ hour,
222
+ minute,
223
+ second,
224
+ day
225
+ ]
226
+ )
227
+
228
+ # Set timer schedule
229
+ # Format is the same as you get from get_full_status.
230
+ # weekday is a list (ordered) of 6 dicts like:
231
+ # {'start_hour':17, 'start_minute':30, 'temp': 22 }
232
+ # Each one specifies the thermostat temp that will become effective at start_hour:start_minute
233
+ # weekend is similar but only has 2 (e.g. switch on in morning and off in afternoon)
234
+ async def set_schedule(self, weekday: List[dict], weekend: List[dict]) -> None:
235
+ """Set timer schedule."""
236
+ request = [0x01, 0x10, 0x00, 0x0A, 0x00, 0x0C, 0x18]
237
+
238
+ # weekday times
239
+ for i in range(0, 6):
240
+ request.append(weekday[i]["start_hour"])
241
+ request.append(weekday[i]["start_minute"])
242
+
243
+ # weekend times
244
+ for i in range(0, 2):
245
+ request.append(weekend[i]["start_hour"])
246
+ request.append(weekend[i]["start_minute"])
247
+
248
+ # weekday temperatures
249
+ for i in range(0, 6):
250
+ request.append(int(weekday[i]["temp"] * 2))
251
+
252
+ # weekend temperatures
253
+ for i in range(0, 2):
254
+ request.append(int(weekend[i]["temp"] * 2))
255
+
256
+ await self.send_request(request)
257
+
258
+
259
+ class hvac(Device):
260
+ """Controls a HVAC.
261
+
262
+ Supported models:
263
+ - Tornado SMART X SQ series
264
+ - Aux ASW-H12U3/JIR1DI-US
265
+ - Aux ASW-H36U2/LFR1DI-US
266
+ """
267
+
268
+ TYPE = "HVAC"
269
+
270
+ @enum.unique
271
+ class Mode(enum.IntEnum):
272
+ """Enumerates modes."""
273
+
274
+ AUTO = 0
275
+ COOL = 1
276
+ DRY = 2
277
+ HEAT = 3
278
+ FAN = 4
279
+
280
+ @enum.unique
281
+ class Speed(enum.IntEnum):
282
+ """Enumerates fan speed."""
283
+
284
+ HIGH = 1
285
+ MID = 2
286
+ LOW = 3
287
+ AUTO = 5
288
+
289
+ @enum.unique
290
+ class Preset(enum.IntEnum):
291
+ """Enumerates presets."""
292
+
293
+ NORMAL = 0
294
+ TURBO = 1
295
+ MUTE = 2
296
+
297
+ @enum.unique
298
+ class SwHoriz(enum.IntEnum):
299
+ """Enumerates horizontal swing."""
300
+
301
+ ON = 0
302
+ OFF = 7
303
+
304
+ @enum.unique
305
+ class SwVert(enum.IntEnum):
306
+ """Enumerates vertical swing."""
307
+
308
+ ON = 0
309
+ POS1 = 1
310
+ POS2 = 2
311
+ POS3 = 3
312
+ POS4 = 4
313
+ POS5 = 5
314
+ OFF = 7
315
+
316
+ def _encode(self, data: bytes) -> bytes:
317
+ """Encode data for transport."""
318
+ packet = bytearray(10)
319
+ p_len = 10 + len(data)
320
+ struct.pack_into(
321
+ "<HHHHH", packet, 0, p_len, 0x00BB, 0x8006, 0, len(data)
322
+ )
323
+ packet += data
324
+ crc = CRC16.calculate(packet[0x02:], polynomial=0x9BE4)
325
+ packet += crc.to_bytes(2, "little")
326
+ return packet
327
+
328
+ def _decode(self, response: bytes) -> bytes:
329
+ """Decode data from transport."""
330
+ # payload[0x2:0x8] == bytes([0xbb, 0x00, 0x07, 0x00, 0x00, 0x00])
331
+ payload = self.decrypt(response[0x38:])
332
+ p_len = int.from_bytes(payload[:0x02], "little")
333
+ nom_crc = int.from_bytes(payload[p_len:p_len+2], "little")
334
+ real_crc = CRC16.calculate(payload[0x02:p_len], polynomial=0x9BE4)
335
+
336
+ if nom_crc != real_crc:
337
+ raise e.DataValidationError(
338
+ -4008,
339
+ "Received data packet check error",
340
+ f"Expected a checksum of {nom_crc} and received {real_crc}",
341
+ )
342
+
343
+ d_len = int.from_bytes(payload[0x08:0x0A], "little")
344
+ return payload[0x0A:0x0A+d_len]
345
+
346
+ async def _send(self, command: int, data: bytes = b"") -> bytes:
347
+ """Send a command to the unit."""
348
+ prefix = bytes([((command << 4) | 1), 1])
349
+ packet = self._encode(prefix + data)
350
+ response = await self.send_packet(0x6A, packet)
351
+ e.check_error(response[0x22:0x24])
352
+ return self._decode(response)[0x02:]
353
+
354
+ def _parse_state(self, data: bytes) -> dict:
355
+ """Parse state."""
356
+ state = {}
357
+ state["power"] = bool(data[0x08] & 1 << 5)
358
+ state["target_temp"] = 8 + (data[0x00] >> 3) + (data[0x04] >> 7) * 0.5
359
+ state["swing_v"] = self.SwVert(data[0x00] & 0b111)
360
+ state["swing_h"] = self.SwHoriz(data[0x01] >> 5)
361
+ state["mode"] = self.Mode(data[0x05] >> 5)
362
+ state["speed"] = self.Speed(data[0x03] >> 5)
363
+ state["preset"] = self.Preset(data[0x04] >> 6)
364
+ state["sleep"] = bool(data[0x05] & 1 << 2)
365
+ state["ifeel"] = bool(data[0x05] & 1 << 3)
366
+ state["health"] = bool(data[0x08] & 1 << 1)
367
+ state["clean"] = bool(data[0x08] & 1 << 2)
368
+ state["display"] = bool(data[0x0A] & 1 << 4)
369
+ state["mildew"] = bool(data[0x0A] & 1 << 3)
370
+ return state
371
+
372
+ async def set_state(
373
+ self,
374
+ power: bool,
375
+ target_temp: float, # 16<=target_temp<=32
376
+ mode: Mode,
377
+ speed: Speed,
378
+ preset: Preset,
379
+ swing_h: SwHoriz,
380
+ swing_v: SwVert,
381
+ sleep: bool,
382
+ ifeel: bool,
383
+ display: bool,
384
+ health: bool,
385
+ clean: bool,
386
+ mildew: bool,
387
+ ) -> dict:
388
+ """Set the state of the device."""
389
+ # TODO: decode unknown bits
390
+ UNK0 = 0b100
391
+ UNK1 = 0b1101
392
+ UNK2 = 0b101
393
+
394
+ target_temp = round(target_temp * 2) / 2
395
+
396
+ if preset == self.Preset.MUTE:
397
+ if mode != self.Mode.FAN:
398
+ raise ValueError("mute is only available in fan mode")
399
+ speed = self.Speed.LOW
400
+
401
+ elif preset == self.Preset.TURBO:
402
+ if mode not in {self.Mode.COOL, self.Mode.HEAT}:
403
+ raise ValueError("turbo is only available in cooling/heating")
404
+ speed = self.Speed.HIGH
405
+
406
+ data = bytearray(0x0D)
407
+ data[0x00] = (int(target_temp) - 8 << 3) | swing_v
408
+ data[0x01] = (swing_h << 5) | UNK0
409
+ data[0x02] = ((target_temp % 1 == 0.5) << 7) | UNK1
410
+ data[0x03] = speed << 5
411
+ data[0x04] = preset << 6
412
+ data[0x05] = mode << 5 | sleep << 2 | ifeel << 3
413
+ data[0x08] = power << 5 | clean << 2 | (health and 0b11)
414
+ data[0x0A] = display << 4 | mildew << 3
415
+ data[0x0C] = UNK2
416
+
417
+ resp = await self._send(0, data)
418
+ return self._parse_state(resp)
419
+
420
+ async def get_state(self) -> dict:
421
+ """Returns a dictionary with the unit's parameters.
422
+
423
+ Returns:
424
+ dict:
425
+ power (bool):
426
+ target_temp (float): temperature set point 16<n<32
427
+ mode (hvac.Mode):
428
+ speed (hvac.Speed):
429
+ preset (hvac.Preset):
430
+ swing_h (hvac.SwHoriz):
431
+ swing_v (hvac.SwVert):
432
+ sleep (bool):
433
+ ifeel (bool):
434
+ display (bool):
435
+ health (bool):
436
+ clean (bool):
437
+ mildew (bool):
438
+ """
439
+ resp = await self._send(1)
440
+
441
+ if len(resp) < 13:
442
+ raise e.DataValidationError(
443
+ -4007,
444
+ "Received data packet length error",
445
+ f"Expected at least 15 bytes and received {len(resp) + 2}",
446
+ )
447
+
448
+ return self._parse_state(resp)
449
+
450
+ async def get_ac_info(self) -> dict:
451
+ """Returns dictionary with AC info.
452
+
453
+ Returns:
454
+ dict:
455
+ power (bool): power
456
+ ambient_temp (float): ambient temperature
457
+ """
458
+ resp = await self._send(2)
459
+
460
+ if len(resp) < 22:
461
+ raise e.DataValidationError(
462
+ -4007,
463
+ "Received data packet length error",
464
+ f"Expected at least 24 bytes and received {len(resp) + 2}",
465
+ )
466
+
467
+ ac_info = {}
468
+ ac_info["power"] = resp[0x1] & 1
469
+
470
+ ambient_temp = resp[0x05] & 0b11111, resp[0x15] & 0b11111
471
+ if any(ambient_temp):
472
+ ac_info["ambient_temp"] = ambient_temp[0] + ambient_temp[1] / 10.0
473
+
474
+ return ac_info
broadlink/const.py ADDED
@@ -0,0 +1,5 @@
1
+ """Constants."""
2
+ DEFAULT_BCAST_ADDR = "255.255.255.255"
3
+ DEFAULT_PORT = 80
4
+ DEFAULT_RETRY_INTVL = 1
5
+ DEFAULT_TIMEOUT = 10
broadlink/cover.py ADDED
@@ -0,0 +1,182 @@
1
+ """Support for covers."""
2
+ import asyncio
3
+ from typing import Sequence
4
+
5
+ from . import exceptions as e
6
+ from .device import Device
7
+
8
+
9
+ class dooya(Device):
10
+ """Controls a Dooya curtain motor."""
11
+
12
+ TYPE = "DT360E"
13
+
14
+ async def _send(self, command: int, attribute: int = 0) -> int:
15
+ """Send a packet to the device."""
16
+ packet = bytearray(16)
17
+ packet[0x00] = 0x09
18
+ packet[0x02] = 0xBB
19
+ packet[0x03] = command
20
+ packet[0x04] = attribute
21
+ packet[0x09] = 0xFA
22
+ packet[0x0A] = 0x44
23
+
24
+ resp = await self.send_packet(0x6A, packet)
25
+ e.check_error(resp[0x22:0x24])
26
+ payload = self.decrypt(resp[0x38:])
27
+ return payload[4]
28
+
29
+ async def open(self) -> int:
30
+ """Open the curtain."""
31
+ return await self._send(0x01)
32
+
33
+ async def close(self) -> int:
34
+ """Close the curtain."""
35
+ return await self._send(0x02)
36
+
37
+ async def stop(self) -> int:
38
+ """Stop the curtain."""
39
+ return await self._send(0x03)
40
+
41
+ async def get_percentage(self) -> int:
42
+ """Return the position of the curtain."""
43
+ return await self._send(0x06, 0x5D)
44
+
45
+ async def set_percentage_and_wait(self, new_percentage: int) -> None:
46
+ """Set the position of the curtain."""
47
+ current = await self.get_percentage()
48
+ if current > new_percentage:
49
+ await self.close()
50
+ while current is not None and current > new_percentage:
51
+ await asyncio.sleep(0.2)
52
+ current = await self.get_percentage()
53
+
54
+ elif current < new_percentage:
55
+ await self.open()
56
+ while current is not None and current < new_percentage:
57
+ await asyncio.sleep(0.2)
58
+ current = await self.get_percentage()
59
+ await self.stop()
60
+
61
+
62
+ class dooya2(Device):
63
+ """Controls a Dooya curtain motor (version 2)."""
64
+
65
+ TYPE = "DT360E-2"
66
+
67
+ async def _send(self, operation: int, data: Sequence = b""):
68
+ """Send a command to the device."""
69
+ packet = bytearray(12)
70
+ packet[0x02] = 0xA5
71
+ packet[0x03] = 0xA5
72
+ packet[0x04] = 0x5A
73
+ packet[0x05] = 0x5A
74
+ packet[0x08] = operation
75
+ packet[0x09] = 0x0B
76
+
77
+ if data:
78
+ data_len = len(data)
79
+ packet[0x0A] = data_len & 0xFF
80
+ packet[0x0B] = data_len >> 8
81
+ packet += bytes(2)
82
+ packet.extend(data)
83
+
84
+ checksum = sum(packet, 0xBEAF) & 0xFFFF
85
+ packet[0x06] = checksum & 0xFF
86
+ packet[0x07] = checksum >> 8
87
+
88
+ packet_len = len(packet) - 2
89
+ packet[0x00] = packet_len & 0xFF
90
+ packet[0x01] = packet_len >> 8
91
+
92
+ resp = await self.send_packet(0x6A, packet)
93
+ e.check_error(resp[0x22:0x24])
94
+ payload = self.decrypt(resp[0x38:])
95
+ return payload
96
+
97
+ async def open(self) -> None:
98
+ """Open the curtain."""
99
+ await self._send(2, [0x00, 0x01, 0x00])
100
+
101
+ async def close(self) -> None:
102
+ """Close the curtain."""
103
+ await self._send(2, [0x00, 0x02, 0x00])
104
+
105
+ async def stop(self) -> None:
106
+ """Stop the curtain."""
107
+ await self._send(2, [0x00, 0x03, 0x00])
108
+
109
+ async def get_percentage(self) -> int:
110
+ """Return the position of the curtain."""
111
+ resp = await self._send(1, [0x00, 0x06, 0x00])
112
+ return resp[0x11]
113
+
114
+ async def set_percentage(self, new_percentage: int) -> None:
115
+ """Set the position of the curtain."""
116
+ await self._send(2, [0x00, 0x09, new_percentage])
117
+
118
+
119
+ class wser(Device):
120
+ """Controls a Wistar curtain motor"""
121
+
122
+ TYPE = "WSER"
123
+
124
+ async def _send(self, operation: int, data: Sequence = b""):
125
+ """Send a command to the device."""
126
+ packet = bytearray(12)
127
+ packet[0x02] = 0xA5
128
+ packet[0x03] = 0xA5
129
+ packet[0x04] = 0x5A
130
+ packet[0x05] = 0x5A
131
+ packet[0x08] = operation
132
+ packet[0x09] = 0x0B
133
+
134
+ if data:
135
+ data_len = len(data)
136
+ packet[0x0A] = data_len & 0xFF
137
+ packet[0x0B] = data_len >> 8
138
+ packet += bytes(2)
139
+ packet.extend(data)
140
+
141
+ checksum = sum(packet, 0xBEAF) & 0xFFFF
142
+ packet[0x06] = checksum & 0xFF
143
+ packet[0x07] = checksum >> 8
144
+
145
+ packet_len = len(packet) - 2
146
+ packet[0x00] = packet_len & 0xFF
147
+ packet[0x01] = packet_len >> 8
148
+
149
+ resp = await self.send_packet(0x6A, packet)
150
+ e.check_error(resp[0x22:0x24])
151
+ payload = self.decrypt(resp[0x38:])
152
+ return payload
153
+
154
+ async def get_position(self) -> int:
155
+ """Return the position of the curtain."""
156
+ resp = await self._send(1, [])
157
+ position = resp[0x0E]
158
+ return position
159
+
160
+ async def open(self) -> int:
161
+ """Open the curtain."""
162
+ resp = await self._send(2, [0x4A, 0x31, 0xA0])
163
+ position = resp[0x0E]
164
+ return position
165
+
166
+ async def close(self) -> int:
167
+ """Close the curtain."""
168
+ resp = await self._send(2, [0x61, 0x32, 0xA0])
169
+ position = resp[0x0E]
170
+ return position
171
+
172
+ async def stop(self) -> int:
173
+ """Stop the curtain."""
174
+ resp = await self._send(2, [0x4C, 0x73, 0xA0])
175
+ position = resp[0x0E]
176
+ return position
177
+
178
+ async def set_position(self, position: int) -> int:
179
+ """Set the position of the curtain."""
180
+ resp = await self._send(2, [position, 0x70, 0xA0])
181
+ position = resp[0x0E]
182
+ return position