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/__init__.py +342 -0
- broadlink/alarm.py +43 -0
- broadlink/climate.py +474 -0
- broadlink/const.py +5 -0
- broadlink/cover.py +182 -0
- broadlink/device.py +470 -0
- broadlink/exceptions.py +161 -0
- broadlink/helpers.py +43 -0
- broadlink/hub.py +98 -0
- broadlink/light.py +200 -0
- broadlink/protocol.py +50 -0
- broadlink/remote.py +521 -0
- broadlink/sensor.py +90 -0
- broadlink/switch.py +472 -0
- python_broadlink-1.0.0.dist-info/METADATA +360 -0
- python_broadlink-1.0.0.dist-info/RECORD +19 -0
- python_broadlink-1.0.0.dist-info/WHEEL +5 -0
- python_broadlink-1.0.0.dist-info/licenses/LICENSE +22 -0
- python_broadlink-1.0.0.dist-info/top_level.txt +1 -0
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
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
|