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/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)