HueBLE 2.2.3__tar.gz → 2.2.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HueBLE
3
- Version: 2.2.3
3
+ Version: 2.2.4
4
4
  Summary: Python module for controlling and monitoring Bluetooth Philips Hue bulbs
5
5
  Author-email: Harvey Lelliott <harveylelliott@duck.com>
6
6
  License: MIT License
@@ -64,7 +64,8 @@ Python module for controlling Bluetooth Philips Hue lights.
64
64
 
65
65
  This Python module enables you to control Philips Hue Bluetooth lights directly
66
66
  from your computer, without the need for a Hue bridge or ZigBee dongle.
67
- It leverages the Bleak library to interact with Bluetooth Philips Hue lights.
67
+ It leverages the Bleak library to interact with Bluetooth Philips Hue lights including
68
+ the smart plugs.
68
69
 
69
70
 
70
71
  ## Features
@@ -73,10 +74,13 @@ It leverages the Bleak library to interact with Bluetooth Philips Hue lights.
73
74
  - 🌗 Brightness control
74
75
  - 🌡️ Colour temp control
75
76
  - 🌈 XY colour control
77
+ - ✨ Built-in effects with custom colour and speed
78
+ - ⏱️ Custom transitions, including instant changes
76
79
  - ❔ Light state (power/brightness/temp/colour)
77
80
  - ⚙️ Light configuration (name)
78
81
  - 📊 Light metadata (manufacturer/model/zigbee address)
79
82
  - 🤜 Supports push & polling models
83
+ - 🔌 Hue smart plug support
80
84
  - 🔂 Simple structure
81
85
  - 📜 Mediocre documentation
82
86
  - ✔️ More emojis than strictly necessary
@@ -177,6 +181,39 @@ if __name__ == "__main__":
177
181
  ```
178
182
 
179
183
 
184
+ ### Transitions
185
+
186
+ Power, brightness, colour temperature and colour changes can use a custom
187
+ transition time.
188
+
189
+ ```python
190
+ await light.set_power(True, transition_ms=0)
191
+ await light.set_brightness(254, transition_ms=100)
192
+ await light.set_colour_temp(250, transition_ms=400)
193
+ await light.set_colour_xy(0.3, 0.4, transition_ms=200)
194
+ ```
195
+
196
+ `transition_ms=0` makes the change instant. Transition times use 100 ms steps.
197
+
198
+
199
+ ### Effects
200
+
201
+ HueBLE supports Hue effects such as Candle, Fireplace, Sunrise and Sunset.
202
+ Effects can use a custom colour, brightness and speed.
203
+
204
+ ```python
205
+ await light.set_colour_effect(
206
+ 0.30,
207
+ 0.14,
208
+ 254,
209
+ HueBLE.EffectType.CANDLE,
210
+ 128,
211
+ )
212
+ ```
213
+
214
+ Effect speed ranges from `0` (slowest) to `255` (fastest).
215
+
216
+
180
217
  ### Demo program
181
218
 
182
219
  A more fully featured demo program can be found in ``` examples/demo.py ``` which demonstrates all of the implemented features.
@@ -129,6 +129,10 @@ UNPACK_EFFECT_API_TEMPERATURE_WITH_EFFECT = "<xxBxxBxxHxxBxxB"
129
129
  #: data stream unpack format string for decoding plain temperature without effect (effect API)
130
130
  UNPACK_EFFECT_API_TEMPERATURE_WITHOUT_EFFECT = "<xxBxxBxxH"
131
131
 
132
+ #: data stream unpack format string for decoding the onoff-only report (effect API),
133
+ #: sent by devices that only support on/off, e.g. the Hue smart plug
134
+ UNPACK_EFFECT_API_ONOFF_ONLY = "<xxB"
135
+
132
136
 
133
137
  _LOGGER = logging.getLogger(__name__)
134
138
 
@@ -139,9 +143,11 @@ class EffectType(Enum):
139
143
  CANDLE = 0x01
140
144
  FIREPLACE = 0x02
141
145
  PRISM = 0x03
146
+ SUNRISE = 0x09
142
147
  SPARKLE = 0x0A
143
148
  OPAL = 0x0B
144
149
  GLISTEN = 0x0C
150
+ SUNSET = 0x0D
145
151
  UNDERWATER = 0x0E
146
152
  COSMOS = 0x0F
147
153
  SUNBEAM = 0x10
@@ -161,10 +167,26 @@ class EffectCommands(Enum):
161
167
  BRIGHTNESS = "0201"
162
168
  TEMPERATURE = "0302"
163
169
  COLOURXY = "0404"
170
+ TRANSITION_TIME = "0502"
164
171
  EFFECT = "0601"
165
172
  EFFECT_SPEED = "0801"
166
173
 
167
174
 
175
+ def _transition_tlv(transition_ms: int) -> bytes:
176
+ if not isinstance(transition_ms, int):
177
+ raise TypeError("transition_ms must be an integer")
178
+ if transition_ms < 0 or transition_ms % 100 != 0:
179
+ raise ValueError("transition_ms must be a non-negative multiple of 100")
180
+
181
+ units = transition_ms // 100
182
+ if units > 0xFFFF:
183
+ raise ValueError("transition_ms is too large")
184
+
185
+ return bytes.fromhex(EffectCommands.TRANSITION_TIME.value) + units.to_bytes(
186
+ 2, "little"
187
+ )
188
+
189
+
168
190
  class HueBleError(Exception):
169
191
  """
170
192
  Base exception of HueBLE.
@@ -475,10 +497,19 @@ class HueBleLight(object):
475
497
  f"""Light "{self.name}" has informed us of a new"""
476
498
  f""" colour temp state of ({self.colour_temp})"""
477
499
  )
500
+ elif len(data) == 3:
501
+ # device only reports its onoff value (e.g. smart plug)
502
+ onoff = unpack(UNPACK_EFFECT_API_ONOFF_ONLY, data)[0]
503
+ self._power_on = bool(onoff)
504
+
505
+ _LOGGER.debug(
506
+ f"""Light "{self.name}" has informed us of a new"""
507
+ f""" power state ({self._power_on})"""
508
+ )
478
509
  else:
479
510
  # so far unkown
480
511
  _LOGGER.warning(
481
- "unrecognized effect response with length {len(data)}: {data}"
512
+ f"unrecognized effect response with length {len(data)}: {data}"
482
513
  )
483
514
 
484
515
  if effect_state_changed:
@@ -1211,6 +1242,11 @@ class HueBleLight(object):
1211
1242
  if write_state:
1212
1243
  self._colour_temp = colour_temp
1213
1244
  self._power_on = bool(onoff)
1245
+ elif len(buf) == 3:
1246
+ # device only reports its onoff value (e.g. smart plug)
1247
+ onoff = unpack(UNPACK_EFFECT_API_ONOFF_ONLY, buf)[0]
1248
+ if write_state:
1249
+ self._power_on = bool(onoff)
1214
1250
  else:
1215
1251
  # so far unkown
1216
1252
  _LOGGER.warning(
@@ -1223,28 +1259,77 @@ class HueBleLight(object):
1223
1259
  """Sets the name of the light. Not tested, use at own risk."""
1224
1260
  await self._write_gatt(UUID_NAME, str.encode(name))
1225
1261
 
1226
- async def set_power(self, on: bool):
1262
+ async def set_power(self, on: bool, transition_ms: int | None = None):
1227
1263
  """Sets the power state of the light."""
1228
- await self._write_gatt(UUID_POWER, bytes([1 if on else 0]))
1264
+ if transition_ms is None:
1265
+ await self._write_gatt(UUID_POWER, bytes([1 if on else 0]))
1266
+ return
1229
1267
 
1230
- async def set_brightness(self, brightness: int):
1268
+ buf = (
1269
+ bytes.fromhex(EffectCommands.ONOFF.value)
1270
+ + bytes([1 if on else 0])
1271
+ + _transition_tlv(transition_ms)
1272
+ )
1273
+ await self._write_gatt(UUID_EFFECTS, buf)
1274
+
1275
+ async def set_brightness(
1276
+ self, brightness: int, transition_ms: int | None = None
1277
+ ):
1231
1278
  """Sets the brightness from an integer between 0 and 255"""
1232
- await self._write_gatt(UUID_BRIGHTNESS, bytes([max(min(brightness, 254), 1)]))
1279
+ brightness = max(min(brightness, 254), 1)
1280
+ if transition_ms is None:
1281
+ await self._write_gatt(UUID_BRIGHTNESS, bytes([brightness]))
1282
+ return
1283
+
1284
+ buf = (
1285
+ bytes.fromhex(EffectCommands.BRIGHTNESS.value)
1286
+ + bytes([brightness])
1287
+ + _transition_tlv(transition_ms)
1288
+ )
1289
+ await self._write_gatt(UUID_EFFECTS, buf)
1233
1290
 
1234
- async def set_colour_temp(self, colour_temp: int):
1291
+ async def set_colour_temp(
1292
+ self, colour_temp: int, transition_ms: int | None = None
1293
+ ):
1235
1294
  """Sets the temperature from an int between 153 and 500.
1236
1295
  Uses mireds."""
1237
1296
  temp = max(min(int(colour_temp), 500), 153)
1238
- y = temp.to_bytes(2, "little")
1239
- await self._write_gatt(UUID_TEMPERATURE, y)
1297
+ data = temp.to_bytes(2, "little")
1298
+ if transition_ms is None:
1299
+ await self._write_gatt(UUID_TEMPERATURE, data)
1300
+ return
1301
+
1302
+ buf = (
1303
+ bytes.fromhex(EffectCommands.TEMPERATURE.value)
1304
+ + data
1305
+ + _transition_tlv(transition_ms)
1306
+ )
1307
+ await self._write_gatt(UUID_EFFECTS, buf)
1240
1308
 
1241
- async def set_colour_xy(self, x: float, y: float):
1309
+ async def set_colour_xy(
1310
+ self, x: float, y: float, transition_ms: int | None = None
1311
+ ):
1242
1312
  """Sets the XY colour coordinates from floats between 0.0 and 1.0."""
1243
- buf = pack("<HH", int(x * 0xFFFF), int(y * 0xFFFF))
1244
- await self._write_gatt(UUID_XY_COLOUR, buf)
1313
+ data = pack("<HH", int(x * 0xFFFF), int(y * 0xFFFF))
1314
+ if transition_ms is None:
1315
+ await self._write_gatt(UUID_XY_COLOUR, data)
1316
+ return
1317
+
1318
+ buf = (
1319
+ bytes.fromhex(EffectCommands.COLOURXY.value)
1320
+ + data
1321
+ + _transition_tlv(transition_ms)
1322
+ )
1323
+ await self._write_gatt(UUID_EFFECTS, buf)
1245
1324
 
1246
1325
  async def set_colour_effect(
1247
- self, x: float, y: float, brightness: int, effect: EffectType, effect_speed: int
1326
+ self,
1327
+ x: float,
1328
+ y: float,
1329
+ brightness: int,
1330
+ effect: EffectType,
1331
+ effect_speed: int,
1332
+ transition_ms: int | None = None,
1248
1333
  ):
1249
1334
  """
1250
1335
  Sets XY colour, brightness, effect and effect speed.
@@ -1268,7 +1353,7 @@ class HueBleLight(object):
1268
1353
  bytes.fromhex(EffectCommands.EFFECT.value),
1269
1354
  max(min(effect.value, 254), 1),
1270
1355
  bytes.fromhex(EffectCommands.EFFECT_SPEED.value),
1271
- max(min(effect_speed, 254), 1),
1356
+ max(min(effect_speed, 255), 0),
1272
1357
  )
1273
1358
  else:
1274
1359
  # if no effect is selected we can just operate in colourxy mode and ommit the effect data in the transfer
@@ -1282,10 +1367,17 @@ class HueBleLight(object):
1282
1367
  int(x * 0xFFFF),
1283
1368
  int(y * 0xFFFF),
1284
1369
  )
1370
+ if transition_ms is not None:
1371
+ buf += _transition_tlv(transition_ms)
1285
1372
  await self._write_gatt(UUID_EFFECTS, buf)
1286
1373
 
1287
1374
  async def set_temperature_effect(
1288
- self, colour_temp: int, brightness: int, effect: EffectType, effect_speed: int
1375
+ self,
1376
+ colour_temp: int,
1377
+ brightness: int,
1378
+ effect: EffectType,
1379
+ effect_speed: int,
1380
+ transition_ms: int | None = None,
1289
1381
  ):
1290
1382
  """
1291
1383
  Sets colour temperature, brightness, effect and effect speed.
@@ -1309,7 +1401,7 @@ class HueBleLight(object):
1309
1401
  bytes.fromhex(EffectCommands.EFFECT.value),
1310
1402
  max(min(effect.value, 254), 1),
1311
1403
  bytes.fromhex(EffectCommands.EFFECT_SPEED.value),
1312
- max(min(effect_speed, 254), 1),
1404
+ max(min(effect_speed, 255), 0),
1313
1405
  )
1314
1406
  else:
1315
1407
  # if no effect is selected we can just operate in colourxy mode and ommit the effect data in the transfer
@@ -1322,6 +1414,8 @@ class HueBleLight(object):
1322
1414
  bytes.fromhex(EffectCommands.TEMPERATURE.value),
1323
1415
  temperature,
1324
1416
  )
1417
+ if transition_ms is not None:
1418
+ buf += _transition_tlv(transition_ms)
1325
1419
  await self._write_gatt(UUID_EFFECTS, buf)
1326
1420
 
1327
1421
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HueBLE
3
- Version: 2.2.3
3
+ Version: 2.2.4
4
4
  Summary: Python module for controlling and monitoring Bluetooth Philips Hue bulbs
5
5
  Author-email: Harvey Lelliott <harveylelliott@duck.com>
6
6
  License: MIT License
@@ -64,7 +64,8 @@ Python module for controlling Bluetooth Philips Hue lights.
64
64
 
65
65
  This Python module enables you to control Philips Hue Bluetooth lights directly
66
66
  from your computer, without the need for a Hue bridge or ZigBee dongle.
67
- It leverages the Bleak library to interact with Bluetooth Philips Hue lights.
67
+ It leverages the Bleak library to interact with Bluetooth Philips Hue lights including
68
+ the smart plugs.
68
69
 
69
70
 
70
71
  ## Features
@@ -73,10 +74,13 @@ It leverages the Bleak library to interact with Bluetooth Philips Hue lights.
73
74
  - 🌗 Brightness control
74
75
  - 🌡️ Colour temp control
75
76
  - 🌈 XY colour control
77
+ - ✨ Built-in effects with custom colour and speed
78
+ - ⏱️ Custom transitions, including instant changes
76
79
  - ❔ Light state (power/brightness/temp/colour)
77
80
  - ⚙️ Light configuration (name)
78
81
  - 📊 Light metadata (manufacturer/model/zigbee address)
79
82
  - 🤜 Supports push & polling models
83
+ - 🔌 Hue smart plug support
80
84
  - 🔂 Simple structure
81
85
  - 📜 Mediocre documentation
82
86
  - ✔️ More emojis than strictly necessary
@@ -177,6 +181,39 @@ if __name__ == "__main__":
177
181
  ```
178
182
 
179
183
 
184
+ ### Transitions
185
+
186
+ Power, brightness, colour temperature and colour changes can use a custom
187
+ transition time.
188
+
189
+ ```python
190
+ await light.set_power(True, transition_ms=0)
191
+ await light.set_brightness(254, transition_ms=100)
192
+ await light.set_colour_temp(250, transition_ms=400)
193
+ await light.set_colour_xy(0.3, 0.4, transition_ms=200)
194
+ ```
195
+
196
+ `transition_ms=0` makes the change instant. Transition times use 100 ms steps.
197
+
198
+
199
+ ### Effects
200
+
201
+ HueBLE supports Hue effects such as Candle, Fireplace, Sunrise and Sunset.
202
+ Effects can use a custom colour, brightness and speed.
203
+
204
+ ```python
205
+ await light.set_colour_effect(
206
+ 0.30,
207
+ 0.14,
208
+ 254,
209
+ HueBLE.EffectType.CANDLE,
210
+ 128,
211
+ )
212
+ ```
213
+
214
+ Effect speed ranges from `0` (slowest) to `255` (fastest).
215
+
216
+
180
217
  ### Demo program
181
218
 
182
219
  A more fully featured demo program can be found in ``` examples/demo.py ``` which demonstrates all of the implemented features.
@@ -15,7 +15,8 @@ Python module for controlling Bluetooth Philips Hue lights.
15
15
 
16
16
  This Python module enables you to control Philips Hue Bluetooth lights directly
17
17
  from your computer, without the need for a Hue bridge or ZigBee dongle.
18
- It leverages the Bleak library to interact with Bluetooth Philips Hue lights.
18
+ It leverages the Bleak library to interact with Bluetooth Philips Hue lights including
19
+ the smart plugs.
19
20
 
20
21
 
21
22
  ## Features
@@ -24,10 +25,13 @@ It leverages the Bleak library to interact with Bluetooth Philips Hue lights.
24
25
  - 🌗 Brightness control
25
26
  - 🌡️ Colour temp control
26
27
  - 🌈 XY colour control
28
+ - ✨ Built-in effects with custom colour and speed
29
+ - ⏱️ Custom transitions, including instant changes
27
30
  - ❔ Light state (power/brightness/temp/colour)
28
31
  - ⚙️ Light configuration (name)
29
32
  - 📊 Light metadata (manufacturer/model/zigbee address)
30
33
  - 🤜 Supports push & polling models
34
+ - 🔌 Hue smart plug support
31
35
  - 🔂 Simple structure
32
36
  - 📜 Mediocre documentation
33
37
  - ✔️ More emojis than strictly necessary
@@ -128,6 +132,39 @@ if __name__ == "__main__":
128
132
  ```
129
133
 
130
134
 
135
+ ### Transitions
136
+
137
+ Power, brightness, colour temperature and colour changes can use a custom
138
+ transition time.
139
+
140
+ ```python
141
+ await light.set_power(True, transition_ms=0)
142
+ await light.set_brightness(254, transition_ms=100)
143
+ await light.set_colour_temp(250, transition_ms=400)
144
+ await light.set_colour_xy(0.3, 0.4, transition_ms=200)
145
+ ```
146
+
147
+ `transition_ms=0` makes the change instant. Transition times use 100 ms steps.
148
+
149
+
150
+ ### Effects
151
+
152
+ HueBLE supports Hue effects such as Candle, Fireplace, Sunrise and Sunset.
153
+ Effects can use a custom colour, brightness and speed.
154
+
155
+ ```python
156
+ await light.set_colour_effect(
157
+ 0.30,
158
+ 0.14,
159
+ 254,
160
+ HueBLE.EffectType.CANDLE,
161
+ 128,
162
+ )
163
+ ```
164
+
165
+ Effect speed ranges from `0` (slowest) to `255` (fastest).
166
+
167
+
131
168
  ### Demo program
132
169
 
133
170
  A more fully featured demo program can be found in ``` examples/demo.py ``` which demonstrates all of the implemented features.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "HueBLE"
3
- version = "2.2.3"
3
+ version = "2.2.4"
4
4
  dependencies = [
5
5
  "bleak>=0.19.0",
6
6
  "bleak-retry-connector",
@@ -393,6 +393,26 @@ async def test_commands(
393
393
  },
394
394
  id="unknown_effect",
395
395
  ),
396
+ pytest.param(
397
+ {
398
+ HueBLE.UUID_MANUFACTURER: "Philippe Hue".encode(),
399
+ HueBLE.UUID_MODEL: "Fancy new light".encode(),
400
+ HueBLE.UUID_FW_VERSION: "1.7.10".encode(),
401
+ HueBLE.UUID_ZIGBEE_ADDRESS: bytes.fromhex("00010203040506070809"),
402
+ HueBLE.UUID_NAME: "L A M P".encode(),
403
+ # on/off-only device (e.g. smart plug): only the onoff TLV is reported
404
+ HueBLE.UUID_EFFECTS: bytes.fromhex("010100"),
405
+ },
406
+ {
407
+ "manufacturer": "Philippe Hue",
408
+ "model": "Fancy new light",
409
+ "firmware": "1.7.10",
410
+ "zigbee_address": "00:01:02:03:04:05:06:07:08:09",
411
+ "name_in_app": "L A M P",
412
+ "power_state": False,
413
+ },
414
+ id="effect_onoff_only",
415
+ ),
396
416
  ],
397
417
  )
398
418
  async def test_poll_state(
File without changes
File without changes