HueBLE 2.2.2__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.2
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.
@@ -17,7 +17,6 @@ from struct import pack, unpack
17
17
  from typing import Callable
18
18
  from enum import Enum
19
19
 
20
-
21
20
  #: String containing manufacturer. Handle 15.
22
21
  UUID_MANUFACTURER = "00002a29-0000-1000-8000-00805f9b34fb"
23
22
 
@@ -130,6 +129,10 @@ UNPACK_EFFECT_API_TEMPERATURE_WITH_EFFECT = "<xxBxxBxxHxxBxxB"
130
129
  #: data stream unpack format string for decoding plain temperature without effect (effect API)
131
130
  UNPACK_EFFECT_API_TEMPERATURE_WITHOUT_EFFECT = "<xxBxxBxxH"
132
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
+
133
136
 
134
137
  _LOGGER = logging.getLogger(__name__)
135
138
 
@@ -140,14 +143,23 @@ class EffectType(Enum):
140
143
  CANDLE = 0x01
141
144
  FIREPLACE = 0x02
142
145
  PRISM = 0x03
146
+ SUNRISE = 0x09
143
147
  SPARKLE = 0x0A
144
148
  OPAL = 0x0B
145
149
  GLISTEN = 0x0C
150
+ SUNSET = 0x0D
146
151
  UNDERWATER = 0x0E
147
152
  COSMOS = 0x0F
148
153
  SUNBEAM = 0x10
149
154
  ENCHANT = 0x11
150
155
 
156
+ @classmethod
157
+ def _missing_(cls, value):
158
+ _LOGGER.warning(
159
+ f"Unknown effect type: '{value}' reported by light, using default of none!"
160
+ )
161
+ return cls.NONE
162
+
151
163
 
152
164
  #: EFFECT_COMMANDS (hex string) for the Effect Characteristic endpoint containing one byte id and one byte data length
153
165
  class EffectCommands(Enum):
@@ -155,10 +167,26 @@ class EffectCommands(Enum):
155
167
  BRIGHTNESS = "0201"
156
168
  TEMPERATURE = "0302"
157
169
  COLOURXY = "0404"
170
+ TRANSITION_TIME = "0502"
158
171
  EFFECT = "0601"
159
172
  EFFECT_SPEED = "0801"
160
173
 
161
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
+
162
190
  class HueBleError(Exception):
163
191
  """
164
192
  Base exception of HueBLE.
@@ -469,10 +497,19 @@ class HueBleLight(object):
469
497
  f"""Light "{self.name}" has informed us of a new"""
470
498
  f""" colour temp state of ({self.colour_temp})"""
471
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
+ )
472
509
  else:
473
510
  # so far unkown
474
511
  _LOGGER.warning(
475
- "unrecognized effect response with length {len(data)}: {data}"
512
+ f"unrecognized effect response with length {len(data)}: {data}"
476
513
  )
477
514
 
478
515
  if effect_state_changed:
@@ -1205,6 +1242,11 @@ class HueBleLight(object):
1205
1242
  if write_state:
1206
1243
  self._colour_temp = colour_temp
1207
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)
1208
1250
  else:
1209
1251
  # so far unkown
1210
1252
  _LOGGER.warning(
@@ -1217,28 +1259,77 @@ class HueBleLight(object):
1217
1259
  """Sets the name of the light. Not tested, use at own risk."""
1218
1260
  await self._write_gatt(UUID_NAME, str.encode(name))
1219
1261
 
1220
- async def set_power(self, on: bool):
1262
+ async def set_power(self, on: bool, transition_ms: int | None = None):
1221
1263
  """Sets the power state of the light."""
1222
- 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
1267
+
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)
1223
1274
 
1224
- async def set_brightness(self, brightness: int):
1275
+ async def set_brightness(
1276
+ self, brightness: int, transition_ms: int | None = None
1277
+ ):
1225
1278
  """Sets the brightness from an integer between 0 and 255"""
1226
- 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
1227
1283
 
1228
- async def set_colour_temp(self, colour_temp: int):
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)
1290
+
1291
+ async def set_colour_temp(
1292
+ self, colour_temp: int, transition_ms: int | None = None
1293
+ ):
1229
1294
  """Sets the temperature from an int between 153 and 500.
1230
1295
  Uses mireds."""
1231
1296
  temp = max(min(int(colour_temp), 500), 153)
1232
- y = temp.to_bytes(2, "little")
1233
- 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)
1234
1308
 
1235
- 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
+ ):
1236
1312
  """Sets the XY colour coordinates from floats between 0.0 and 1.0."""
1237
- buf = pack("<HH", int(x * 0xFFFF), int(y * 0xFFFF))
1238
- 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)
1239
1324
 
1240
1325
  async def set_colour_effect(
1241
- 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,
1242
1333
  ):
1243
1334
  """
1244
1335
  Sets XY colour, brightness, effect and effect speed.
@@ -1262,7 +1353,7 @@ class HueBleLight(object):
1262
1353
  bytes.fromhex(EffectCommands.EFFECT.value),
1263
1354
  max(min(effect.value, 254), 1),
1264
1355
  bytes.fromhex(EffectCommands.EFFECT_SPEED.value),
1265
- max(min(effect_speed, 254), 1),
1356
+ max(min(effect_speed, 255), 0),
1266
1357
  )
1267
1358
  else:
1268
1359
  # if no effect is selected we can just operate in colourxy mode and ommit the effect data in the transfer
@@ -1276,10 +1367,17 @@ class HueBleLight(object):
1276
1367
  int(x * 0xFFFF),
1277
1368
  int(y * 0xFFFF),
1278
1369
  )
1370
+ if transition_ms is not None:
1371
+ buf += _transition_tlv(transition_ms)
1279
1372
  await self._write_gatt(UUID_EFFECTS, buf)
1280
1373
 
1281
1374
  async def set_temperature_effect(
1282
- 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,
1283
1381
  ):
1284
1382
  """
1285
1383
  Sets colour temperature, brightness, effect and effect speed.
@@ -1303,7 +1401,7 @@ class HueBleLight(object):
1303
1401
  bytes.fromhex(EffectCommands.EFFECT.value),
1304
1402
  max(min(effect.value, 254), 1),
1305
1403
  bytes.fromhex(EffectCommands.EFFECT_SPEED.value),
1306
- max(min(effect_speed, 254), 1),
1404
+ max(min(effect_speed, 255), 0),
1307
1405
  )
1308
1406
  else:
1309
1407
  # if no effect is selected we can just operate in colourxy mode and ommit the effect data in the transfer
@@ -1316,6 +1414,8 @@ class HueBleLight(object):
1316
1414
  bytes.fromhex(EffectCommands.TEMPERATURE.value),
1317
1415
  temperature,
1318
1416
  )
1417
+ if transition_ms is not None:
1418
+ buf += _transition_tlv(transition_ms)
1319
1419
  await self._write_gatt(UUID_EFFECTS, buf)
1320
1420
 
1321
1421
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HueBLE
3
- Version: 2.2.2
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.2"
3
+ version = "2.2.4"
4
4
  dependencies = [
5
5
  "bleak>=0.19.0",
6
6
  "bleak-retry-connector",
@@ -370,6 +370,49 @@ async def test_commands(
370
370
  },
371
371
  id="temperature_effect",
372
372
  ),
373
+ pytest.param(
374
+ {
375
+ HueBLE.UUID_MANUFACTURER: "Philippe Hue".encode(),
376
+ HueBLE.UUID_MODEL: "Fancy new light".encode(),
377
+ HueBLE.UUID_FW_VERSION: "1.7.10".encode(),
378
+ HueBLE.UUID_ZIGBEE_ADDRESS: bytes.fromhex("00010203040506070809"),
379
+ HueBLE.UUID_NAME: "L A M P".encode(),
380
+ HueBLE.UUID_EFFECTS: bytes.fromhex("01010102014503022c01060115080138"),
381
+ },
382
+ {
383
+ "manufacturer": "Philippe Hue",
384
+ "model": "Fancy new light",
385
+ "firmware": "1.7.10",
386
+ "zigbee_address": "00:01:02:03:04:05:06:07:08:09",
387
+ "name_in_app": "L A M P",
388
+ "power_state": True,
389
+ "brightness": 69,
390
+ "colour_temp": 300,
391
+ "colour_xy": None,
392
+ "effect": (HueBLE.EffectType.NONE, 56),
393
+ },
394
+ id="unknown_effect",
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
+ ),
373
416
  ],
374
417
  )
375
418
  async def test_poll_state(
File without changes
File without changes