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/hub.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Support for hubs."""
|
|
2
|
+
import json
|
|
3
|
+
import struct
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from . import exceptions as e
|
|
7
|
+
from .device import Device
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class s3(Device):
|
|
11
|
+
"""Controls a Broadlink S3."""
|
|
12
|
+
|
|
13
|
+
TYPE = "S3"
|
|
14
|
+
MAX_SUBDEVICES = 8
|
|
15
|
+
|
|
16
|
+
async def get_subdevices(self, step: int = 5) -> list:
|
|
17
|
+
"""Return a list of sub devices."""
|
|
18
|
+
total = self.MAX_SUBDEVICES
|
|
19
|
+
sub_devices = []
|
|
20
|
+
seen = set()
|
|
21
|
+
index = 0
|
|
22
|
+
|
|
23
|
+
while index < total:
|
|
24
|
+
state = {"count": step, "index": index}
|
|
25
|
+
packet = self._encode(14, state)
|
|
26
|
+
resp = await self.send_packet(0x6A, packet)
|
|
27
|
+
e.check_error(resp[0x22:0x24])
|
|
28
|
+
resp = self._decode(resp)
|
|
29
|
+
|
|
30
|
+
for device in resp["list"]:
|
|
31
|
+
did = device["did"]
|
|
32
|
+
if did in seen:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
seen.add(did)
|
|
36
|
+
sub_devices.append(device)
|
|
37
|
+
|
|
38
|
+
total = resp["total"]
|
|
39
|
+
if len(seen) >= total:
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
index += step
|
|
43
|
+
|
|
44
|
+
return sub_devices
|
|
45
|
+
|
|
46
|
+
async def get_state(self, did: Optional[str] = None) -> dict:
|
|
47
|
+
"""Return the power state of the device."""
|
|
48
|
+
state = {}
|
|
49
|
+
if did is not None:
|
|
50
|
+
state["did"] = did
|
|
51
|
+
|
|
52
|
+
packet = self._encode(1, state)
|
|
53
|
+
response = await self.send_packet(0x6A, packet)
|
|
54
|
+
e.check_error(response[0x22:0x24])
|
|
55
|
+
return self._decode(response)
|
|
56
|
+
|
|
57
|
+
async def set_state(
|
|
58
|
+
self,
|
|
59
|
+
did: Optional[str] = None,
|
|
60
|
+
pwr1: Optional[bool] = None,
|
|
61
|
+
pwr2: Optional[bool] = None,
|
|
62
|
+
pwr3: Optional[bool] = None,
|
|
63
|
+
) -> dict:
|
|
64
|
+
"""Set the power state of the device."""
|
|
65
|
+
state = {}
|
|
66
|
+
if did is not None:
|
|
67
|
+
state["did"] = did
|
|
68
|
+
if pwr1 is not None:
|
|
69
|
+
state["pwr1"] = int(bool(pwr1))
|
|
70
|
+
if pwr2 is not None:
|
|
71
|
+
state["pwr2"] = int(bool(pwr2))
|
|
72
|
+
if pwr3 is not None:
|
|
73
|
+
state["pwr3"] = int(bool(pwr3))
|
|
74
|
+
|
|
75
|
+
packet = self._encode(2, state)
|
|
76
|
+
response = await self.send_packet(0x6A, packet)
|
|
77
|
+
e.check_error(response[0x22:0x24])
|
|
78
|
+
return self._decode(response)
|
|
79
|
+
|
|
80
|
+
def _encode(self, flag: int, state: dict) -> bytes:
|
|
81
|
+
"""Encode a JSON packet."""
|
|
82
|
+
# flag: 1 for reading, 2 for writing.
|
|
83
|
+
packet = bytearray(12)
|
|
84
|
+
data = json.dumps(state, separators=(",", ":")).encode()
|
|
85
|
+
struct.pack_into(
|
|
86
|
+
"<HHHBBI", packet, 0, 0xA5A5, 0x5A5A, 0, flag, 0x0B, len(data)
|
|
87
|
+
)
|
|
88
|
+
packet.extend(data)
|
|
89
|
+
checksum = sum(packet, 0xBEAF) & 0xFFFF
|
|
90
|
+
packet[0x04:0x06] = checksum.to_bytes(2, "little")
|
|
91
|
+
return packet
|
|
92
|
+
|
|
93
|
+
def _decode(self, response: bytes) -> dict:
|
|
94
|
+
"""Decode a JSON packet."""
|
|
95
|
+
payload = self.decrypt(response[0x38:])
|
|
96
|
+
js_len = struct.unpack_from("<I", payload, 0x08)[0]
|
|
97
|
+
state = json.loads(payload[0x0C:0x0C+js_len])
|
|
98
|
+
return state
|
broadlink/light.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Support for lights."""
|
|
2
|
+
import enum
|
|
3
|
+
import json
|
|
4
|
+
import struct
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from . import exceptions as e
|
|
8
|
+
from .device import Device
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class lb1(Device):
|
|
12
|
+
"""Controls a Broadlink LB1."""
|
|
13
|
+
|
|
14
|
+
TYPE = "LB1"
|
|
15
|
+
|
|
16
|
+
@enum.unique
|
|
17
|
+
class ColorMode(enum.IntEnum):
|
|
18
|
+
"""Enumerates color modes."""
|
|
19
|
+
|
|
20
|
+
RGB = 0
|
|
21
|
+
WHITE = 1
|
|
22
|
+
SCENE = 2
|
|
23
|
+
|
|
24
|
+
async def get_state(self) -> dict:
|
|
25
|
+
"""Return the power state of the device.
|
|
26
|
+
|
|
27
|
+
Example: `{'red': 128, 'blue': 255, 'green': 128, 'pwr': 1, 'brightness': 75, 'colortemp': 2700, 'hue': 240, 'saturation': 50, 'transitionduration': 1500, 'maxworktime': 0, 'bulb_colormode': 1, 'bulb_scenes': '["@01686464,0,0,0", "#ffffff,10,0,#000000,190,0,0", "2700+100,0,0,0", "#ff0000,500,2500,#00FF00,500,2500,#0000FF,500,2500,0", "@01686464,100,2400,@01686401,100,2400,0", "@01686464,100,2400,@01686401,100,2400,@005a6464,100,2400,@005a6401,100,2400,0", "@01686464,10,0,@00000000,190,0,0", "@01686464,200,0,@005a6464,200,0,0"]', 'bulb_scene': '', 'bulb_sceneidx': 255}`
|
|
28
|
+
"""
|
|
29
|
+
packet = self._encode(1, {})
|
|
30
|
+
response = await self.send_packet(0x6A, packet)
|
|
31
|
+
e.check_error(response[0x22:0x24])
|
|
32
|
+
return self._decode(response)
|
|
33
|
+
|
|
34
|
+
async def set_state(
|
|
35
|
+
self,
|
|
36
|
+
pwr: Optional[bool] = None,
|
|
37
|
+
red: Optional[int] = None,
|
|
38
|
+
blue: Optional[int] = None,
|
|
39
|
+
green: Optional[int] = None,
|
|
40
|
+
brightness: Optional[int] = None,
|
|
41
|
+
colortemp: Optional[int] = None,
|
|
42
|
+
hue: Optional[int] = None,
|
|
43
|
+
saturation: Optional[int] = None,
|
|
44
|
+
transitionduration: Optional[int] = None,
|
|
45
|
+
maxworktime: Optional[int] = None,
|
|
46
|
+
bulb_colormode: Optional[int] = None,
|
|
47
|
+
bulb_scenes: Optional[str] = None,
|
|
48
|
+
bulb_scene: Optional[str] = None,
|
|
49
|
+
bulb_sceneidx: Optional[int] = None,
|
|
50
|
+
) -> dict:
|
|
51
|
+
"""Set the power state of the device."""
|
|
52
|
+
state = {}
|
|
53
|
+
if pwr is not None:
|
|
54
|
+
state["pwr"] = int(bool(pwr))
|
|
55
|
+
if red is not None:
|
|
56
|
+
state["red"] = int(red)
|
|
57
|
+
if blue is not None:
|
|
58
|
+
state["blue"] = int(blue)
|
|
59
|
+
if green is not None:
|
|
60
|
+
state["green"] = int(green)
|
|
61
|
+
if brightness is not None:
|
|
62
|
+
state["brightness"] = int(brightness)
|
|
63
|
+
if colortemp is not None:
|
|
64
|
+
state["colortemp"] = int(colortemp)
|
|
65
|
+
if hue is not None:
|
|
66
|
+
state["hue"] = int(hue)
|
|
67
|
+
if saturation is not None:
|
|
68
|
+
state["saturation"] = int(saturation)
|
|
69
|
+
if transitionduration is not None:
|
|
70
|
+
state["transitionduration"] = int(transitionduration)
|
|
71
|
+
if maxworktime is not None:
|
|
72
|
+
state["maxworktime"] = int(maxworktime)
|
|
73
|
+
if bulb_colormode is not None:
|
|
74
|
+
state["bulb_colormode"] = int(bulb_colormode)
|
|
75
|
+
if bulb_scenes is not None:
|
|
76
|
+
state["bulb_scenes"] = str(bulb_scenes)
|
|
77
|
+
if bulb_scene is not None:
|
|
78
|
+
state["bulb_scene"] = str(bulb_scene)
|
|
79
|
+
if bulb_sceneidx is not None:
|
|
80
|
+
state["bulb_sceneidx"] = int(bulb_sceneidx)
|
|
81
|
+
|
|
82
|
+
packet = self._encode(2, state)
|
|
83
|
+
response = await self.send_packet(0x6A, packet)
|
|
84
|
+
e.check_error(response[0x22:0x24])
|
|
85
|
+
return self._decode(response)
|
|
86
|
+
|
|
87
|
+
def _encode(self, flag: int, state: dict) -> bytes:
|
|
88
|
+
"""Encode a JSON packet."""
|
|
89
|
+
# flag: 1 for reading, 2 for writing.
|
|
90
|
+
packet = bytearray(14)
|
|
91
|
+
data = json.dumps(state, separators=(",", ":")).encode()
|
|
92
|
+
p_len = 12 + len(data)
|
|
93
|
+
struct.pack_into(
|
|
94
|
+
"<HHHHBBI", packet, 0, p_len, 0xA5A5, 0x5A5A, 0, flag, 0x0B, len(data)
|
|
95
|
+
)
|
|
96
|
+
packet.extend(data)
|
|
97
|
+
checksum = sum(packet[0x02:], 0xBEAF) & 0xFFFF
|
|
98
|
+
packet[0x06:0x08] = checksum.to_bytes(2, "little")
|
|
99
|
+
return packet
|
|
100
|
+
|
|
101
|
+
def _decode(self, response: bytes) -> dict:
|
|
102
|
+
"""Decode a JSON packet."""
|
|
103
|
+
payload = self.decrypt(response[0x38:])
|
|
104
|
+
js_len = struct.unpack_from("<I", payload, 0xA)[0]
|
|
105
|
+
state = json.loads(payload[0xE:0xE+js_len])
|
|
106
|
+
return state
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class lb2(Device):
|
|
110
|
+
"""Controls a Broadlink LB26/LB27."""
|
|
111
|
+
|
|
112
|
+
TYPE = "LB2"
|
|
113
|
+
|
|
114
|
+
@enum.unique
|
|
115
|
+
class ColorMode(enum.IntEnum):
|
|
116
|
+
"""Enumerates color modes."""
|
|
117
|
+
|
|
118
|
+
RGB = 0
|
|
119
|
+
WHITE = 1
|
|
120
|
+
SCENE = 2
|
|
121
|
+
|
|
122
|
+
async def get_state(self) -> dict:
|
|
123
|
+
"""Return the power state of the device.
|
|
124
|
+
|
|
125
|
+
Example: `{'red': 128, 'blue': 255, 'green': 128, 'pwr': 1, 'brightness': 75, 'colortemp': 2700, 'hue': 240, 'saturation': 50, 'transitionduration': 1500, 'maxworktime': 0, 'bulb_colormode': 1, 'bulb_scenes': '["@01686464,0,0,0", "#ffffff,10,0,#000000,190,0,0", "2700+100,0,0,0", "#ff0000,500,2500,#00FF00,500,2500,#0000FF,500,2500,0", "@01686464,100,2400,@01686401,100,2400,0", "@01686464,100,2400,@01686401,100,2400,@005a6464,100,2400,@005a6401,100,2400,0", "@01686464,10,0,@00000000,190,0,0", "@01686464,200,0,@005a6464,200,0,0"]', 'bulb_scene': ''}`
|
|
126
|
+
"""
|
|
127
|
+
packet = self._encode(1, {})
|
|
128
|
+
response = await self.send_packet(0x6A, packet)
|
|
129
|
+
e.check_error(response[0x22:0x24])
|
|
130
|
+
return self._decode(response)
|
|
131
|
+
|
|
132
|
+
async def set_state(
|
|
133
|
+
self,
|
|
134
|
+
pwr: Optional[bool] = None,
|
|
135
|
+
red: Optional[int] = None,
|
|
136
|
+
blue: Optional[int] = None,
|
|
137
|
+
green: Optional[int] = None,
|
|
138
|
+
brightness: Optional[int] = None,
|
|
139
|
+
colortemp: Optional[int] = None,
|
|
140
|
+
hue: Optional[int] = None,
|
|
141
|
+
saturation: Optional[int] = None,
|
|
142
|
+
transitionduration: Optional[int] = None,
|
|
143
|
+
maxworktime: Optional[int] = None,
|
|
144
|
+
bulb_colormode: Optional[int] = None,
|
|
145
|
+
bulb_scenes: Optional[str] = None,
|
|
146
|
+
bulb_scene: Optional[str] = None,
|
|
147
|
+
) -> dict:
|
|
148
|
+
"""Set the power state of the device."""
|
|
149
|
+
state = {}
|
|
150
|
+
if pwr is not None:
|
|
151
|
+
state["pwr"] = int(bool(pwr))
|
|
152
|
+
if red is not None:
|
|
153
|
+
state["red"] = int(red)
|
|
154
|
+
if blue is not None:
|
|
155
|
+
state["blue"] = int(blue)
|
|
156
|
+
if green is not None:
|
|
157
|
+
state["green"] = int(green)
|
|
158
|
+
if brightness is not None:
|
|
159
|
+
state["brightness"] = int(brightness)
|
|
160
|
+
if colortemp is not None:
|
|
161
|
+
state["colortemp"] = int(colortemp)
|
|
162
|
+
if hue is not None:
|
|
163
|
+
state["hue"] = int(hue)
|
|
164
|
+
if saturation is not None:
|
|
165
|
+
state["saturation"] = int(saturation)
|
|
166
|
+
if transitionduration is not None:
|
|
167
|
+
state["transitionduration"] = int(transitionduration)
|
|
168
|
+
if maxworktime is not None:
|
|
169
|
+
state["maxworktime"] = int(maxworktime)
|
|
170
|
+
if bulb_colormode is not None:
|
|
171
|
+
state["bulb_colormode"] = int(bulb_colormode)
|
|
172
|
+
if bulb_scenes is not None:
|
|
173
|
+
state["bulb_scenes"] = str(bulb_scenes)
|
|
174
|
+
if bulb_scene is not None:
|
|
175
|
+
state["bulb_scene"] = str(bulb_scene)
|
|
176
|
+
|
|
177
|
+
packet = self._encode(2, state)
|
|
178
|
+
response = await self.send_packet(0x6A, packet)
|
|
179
|
+
e.check_error(response[0x22:0x24])
|
|
180
|
+
return self._decode(response)
|
|
181
|
+
|
|
182
|
+
def _encode(self, flag: int, state: dict) -> bytes:
|
|
183
|
+
"""Encode a JSON packet."""
|
|
184
|
+
# flag: 1 for reading, 2 for writing.
|
|
185
|
+
packet = bytearray(12)
|
|
186
|
+
data = json.dumps(state, separators=(",", ":")).encode()
|
|
187
|
+
struct.pack_into(
|
|
188
|
+
"<HHHBBI", packet, 0, 0xA5A5, 0x5A5A, 0, flag, 0x0B, len(data)
|
|
189
|
+
)
|
|
190
|
+
packet.extend(data)
|
|
191
|
+
checksum = sum(packet, 0xBEAF) & 0xFFFF
|
|
192
|
+
packet[0x04:0x06] = checksum.to_bytes(2, "little")
|
|
193
|
+
return packet
|
|
194
|
+
|
|
195
|
+
def _decode(self, response: bytes) -> dict:
|
|
196
|
+
"""Decode a JSON packet."""
|
|
197
|
+
payload = self.decrypt(response[0x38:])
|
|
198
|
+
js_len = struct.unpack_from("<I", payload, 0x08)[0]
|
|
199
|
+
state = json.loads(payload[0x0C:0x0C+js_len])
|
|
200
|
+
return state
|
broadlink/protocol.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""The networking part of the python-broadlink library."""
|
|
2
|
+
import datetime as dt
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Datetime:
|
|
7
|
+
"""Helps to pack and unpack datetime objects for the Broadlink protocol."""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def pack(datetime: dt.datetime) -> bytes:
|
|
11
|
+
"""Pack the timestamp to be sent over the Broadlink protocol."""
|
|
12
|
+
data = bytearray(12)
|
|
13
|
+
utcoffset = int(datetime.utcoffset().total_seconds() / 3600)
|
|
14
|
+
data[:0x04] = utcoffset.to_bytes(4, "little", signed=True)
|
|
15
|
+
data[0x04:0x06] = datetime.year.to_bytes(2, "little")
|
|
16
|
+
data[0x06] = datetime.minute
|
|
17
|
+
data[0x07] = datetime.hour
|
|
18
|
+
data[0x08] = int(datetime.strftime("%y"))
|
|
19
|
+
data[0x09] = datetime.isoweekday()
|
|
20
|
+
data[0x0A] = datetime.day
|
|
21
|
+
data[0x0B] = datetime.month
|
|
22
|
+
return data
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def unpack(data: bytes) -> dt.datetime:
|
|
26
|
+
"""Unpack a timestamp received over the Broadlink protocol."""
|
|
27
|
+
utcoffset = int.from_bytes(data[0x00:0x04], "little", signed=True)
|
|
28
|
+
year = int.from_bytes(data[0x04:0x06], "little")
|
|
29
|
+
minute = data[0x06]
|
|
30
|
+
hour = data[0x07]
|
|
31
|
+
subyear = data[0x08]
|
|
32
|
+
isoweekday = data[0x09]
|
|
33
|
+
day = data[0x0A]
|
|
34
|
+
month = data[0x0B]
|
|
35
|
+
|
|
36
|
+
tz_info = dt.timezone(dt.timedelta(hours=utcoffset))
|
|
37
|
+
datetime = dt.datetime(year, month, day, hour, minute, 0, 0, tz_info)
|
|
38
|
+
|
|
39
|
+
if datetime.isoweekday() != isoweekday:
|
|
40
|
+
raise ValueError("isoweekday does not match")
|
|
41
|
+
if int(datetime.strftime("%y")) != subyear:
|
|
42
|
+
raise ValueError("subyear does not match")
|
|
43
|
+
|
|
44
|
+
return datetime
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def now() -> dt.datetime:
|
|
48
|
+
"""Return the current date and time with timezone info."""
|
|
49
|
+
tz_info = dt.timezone(dt.timedelta(seconds=-time.timezone))
|
|
50
|
+
return dt.datetime.now(tz_info)
|