blebox-uniapi 2.5.3__tar.gz → 2.5.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.
Files changed (39) hide show
  1. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/PKG-INFO +1 -1
  2. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/__init__.py +1 -1
  3. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/binary_sensor.py +44 -9
  4. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/box.py +28 -2
  5. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/box_types.py +8 -0
  6. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/cover.py +73 -3
  7. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/light.py +58 -24
  8. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/session.py +24 -2
  9. blebox_uniapi-2.5.4/blebox_uniapi/update.py +23 -0
  10. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/PKG-INFO +1 -1
  11. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/SOURCES.txt +1 -0
  12. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/setup.cfg +1 -1
  13. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/setup.py +1 -1
  14. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/CONTRIBUTING.rst +0 -0
  15. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/HISTORY.rst +0 -0
  16. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/LICENSE +0 -0
  17. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/MANIFEST.in +0 -0
  18. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/README.rst +0 -0
  19. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/button.py +0 -0
  20. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/climate.py +0 -0
  21. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/error.py +0 -0
  22. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/feature.py +0 -0
  23. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/jfollow.py +0 -0
  24. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/sensor.py +0 -0
  25. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/switch.py +0 -0
  26. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
  27. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/not-zip-safe +0 -0
  28. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/requires.txt +0 -0
  29. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/top_level.txt +0 -0
  30. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/Makefile +0 -0
  31. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/authors.rst +0 -0
  32. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/conf.py +0 -0
  33. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/contributing.rst +0 -0
  34. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/history.rst +0 -0
  35. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/index.rst +0 -0
  36. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/installation.rst +0 -0
  37. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/make.bat +0 -0
  38. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/readme.rst +0 -0
  39. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/usage.rst +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: blebox_uniapi
3
- Version: 2.5.3
3
+ Version: 2.5.4
4
4
  Summary: Python API for accessing BleBox smart home devices
5
5
  Home-page: https://github.com/blebox/blebox_uniapi
6
6
  Author: BleBox
@@ -2,4 +2,4 @@
2
2
 
3
3
  __author__ = """BleBox"""
4
4
  __email__ = "opensource@blebox.eu"
5
- __version__ = "2.5.3"
5
+ __version__ = "2.5.4"
@@ -9,7 +9,14 @@ if TYPE_CHECKING:
9
9
  class BinarySensor(Feature):
10
10
  """Class representing sensor with bool state."""
11
11
 
12
- def __init__(self, product: "Box", alias: str, methods: dict, sensor_id: Optional[int] = None, name: Optional[str] = None):
12
+ def __init__(
13
+ self,
14
+ product: "Box",
15
+ alias: str,
16
+ methods: dict,
17
+ sensor_id: Optional[int] = None,
18
+ name: Optional[str] = None,
19
+ ):
13
20
  self._sensor_id = sensor_id
14
21
  self._name = name
15
22
  super().__init__(product, alias, methods)
@@ -60,7 +67,14 @@ class BinarySensor(Feature):
60
67
 
61
68
 
62
69
  class Rain(BinarySensor):
63
- def __init__(self, product: "Box", alias: str, methods: dict, sensor_id: Optional[int] = None, name: Optional[str] = None):
70
+ def __init__(
71
+ self,
72
+ product: "Box",
73
+ alias: str,
74
+ methods: dict,
75
+ sensor_id: Optional[int] = None,
76
+ name: Optional[str] = None,
77
+ ):
64
78
  self._device_class = "moisture"
65
79
  super().__init__(product, alias, methods, sensor_id=sensor_id, name=name)
66
80
 
@@ -85,7 +99,14 @@ class Rain(BinarySensor):
85
99
 
86
100
 
87
101
  class Flood(BinarySensor):
88
- def __init__(self, product: "Box", alias: str, methods: dict, sensor_id: Optional[int] = None, name: Optional[str] = None):
102
+ def __init__(
103
+ self,
104
+ product: "Box",
105
+ alias: str,
106
+ methods: dict,
107
+ sensor_id: Optional[int] = None,
108
+ name: Optional[str] = None,
109
+ ):
89
110
  self._device_class = "moisture"
90
111
  super().__init__(product, alias, methods, sensor_id=sensor_id, name=name)
91
112
 
@@ -111,9 +132,16 @@ class Flood(BinarySensor):
111
132
 
112
133
 
113
134
  class Open(BinarySensor):
114
- def __init__(self, product: "Box", alias: str, methods: dict):
135
+ def __init__(
136
+ self,
137
+ product: "Box",
138
+ alias: str,
139
+ methods: dict,
140
+ sensor_id: Optional[int] = None,
141
+ name: Optional[str] = None,
142
+ ):
115
143
  self._device_class = "open"
116
- super().__init__(product, alias, methods)
144
+ super().__init__(product, alias, methods, sensor_id=sensor_id, name=name)
117
145
 
118
146
  @property
119
147
  def state(self) -> bool:
@@ -130,12 +158,19 @@ class Open(BinarySensor):
130
158
 
131
159
  def after_update(self) -> None:
132
160
  self._current = self._read_input("openStatus")
133
-
134
-
161
+
162
+
135
163
  class Input(BinarySensor):
136
- def __init__(self, product: "Box", alias: str, methods: dict):
164
+ def __init__(
165
+ self,
166
+ product: "Box",
167
+ alias: str,
168
+ methods: dict,
169
+ sensor_id: Optional[int] = None,
170
+ name: Optional[str] = None,
171
+ ):
137
172
  self._device_class = "input"
138
- super().__init__(product, alias, methods)
173
+ super().__init__(product, alias, methods, sensor_id=sensor_id, name=name)
139
174
 
140
175
  @property
141
176
  def state(self) -> bool:
@@ -14,6 +14,7 @@ from .sensor import SensorFactory
14
14
  from .binary_sensor import BinarySensor
15
15
  from .session import ApiHost
16
16
  from .switch import Switch
17
+ from .update import Update
17
18
 
18
19
  from .error import (
19
20
  UnsupportedBoxResponse,
@@ -27,7 +28,6 @@ from .error import (
27
28
  HttpError,
28
29
  )
29
30
 
30
-
31
31
  DEFAULT_PORT = 80
32
32
 
33
33
 
@@ -105,6 +105,8 @@ class Box:
105
105
  info, f"{location} has no hardware version"
106
106
  ) from ex
107
107
 
108
+ available_firmware_version = info.get("availableFv")
109
+
108
110
  level = int(info.get("apiLevel", _DEFAULT_API_LEVEL))
109
111
 
110
112
  self._data_path = config["api_path"]
@@ -114,6 +116,7 @@ class Box:
114
116
  self._address = address
115
117
  self._firmware_version = firmware_version
116
118
  self._hardware_version = hardware_version
119
+ self._available_firmware_version = available_firmware_version
117
120
  self._api_version = level
118
121
  self._model = config.get("model", type)
119
122
  self._api = config.get("api", {})
@@ -143,6 +146,7 @@ class Box:
143
146
  )
144
147
  except KeyError:
145
148
  raise UnsupportedBoxResponse("Failed to initialize:", info)
149
+ features["updates"] = [Update(self, "update", {})]
146
150
  return features
147
151
 
148
152
  @classmethod
@@ -153,7 +157,9 @@ class Box:
153
157
  except HttpError:
154
158
  path = "/info"
155
159
  data = await api_host.async_api_get(path)
156
- info = data.get("device", data) # type: ignore
160
+ if data is None:
161
+ raise UnsupportedBoxResponse("Device returned non-JSON response")
162
+ info = data.get("device", data)
157
163
  extended_state = None
158
164
 
159
165
  config = cls._match_device_config(info)
@@ -218,6 +224,10 @@ class Box:
218
224
  def hardware_version(self) -> Any:
219
225
  return self._hardware_version
220
226
 
227
+ @property
228
+ def available_firmware_version(self) -> Any:
229
+ return self._available_firmware_version
230
+
221
231
  @property
222
232
  def api_version(self) -> int:
223
233
  return self._api_version
@@ -366,3 +376,19 @@ class Box:
366
376
  response = await self._session.async_api_post(path, post_data)
367
377
  self._update_last_data(response)
368
378
  self._last_real_update = time.time()
379
+
380
+ async def async_ota_check(self) -> None:
381
+ await self._session.async_api_get_ota("/api/ota/check")
382
+ for _ in range(3):
383
+ await asyncio.sleep(1)
384
+ response = await self._session.async_api_get("/info")
385
+ if response is None:
386
+ raise UnsupportedBoxResponse("Device returned non-JSON response")
387
+ response = response.get("device", response)
388
+ if response.get("availableFv") is not None:
389
+ self._firmware_version = response.get("fv", self._firmware_version)
390
+ self._available_firmware_version = response["availableFv"]
391
+ return
392
+
393
+ async def async_ota_update(self) -> None:
394
+ await self._session.async_api_get("/api/ota/update")
@@ -194,6 +194,14 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
194
194
  },
195
195
  ]
196
196
  ],
197
+ "sensors": [
198
+ [
199
+ "temperature",
200
+ {
201
+ "temperature": lambda x: f"sensors[?id == `{x}`]|[0]|value",
202
+ },
203
+ ]
204
+ ],
197
205
  }
198
206
  },
199
207
  # saunaBox
@@ -33,12 +33,21 @@ class ShutterBoxControlType(IntEnum):
33
33
 
34
34
  SEGMENTED_SHUTTER = 1
35
35
  NO_CALIBRATION = 2
36
- TILT_SHUTTER = 3
36
+ TILT_SHUTTER_90 = 3
37
37
  WINDOW_OPENER = 4
38
38
  MATERIAL_SHUTTER = 5
39
39
  AWNING = 6
40
40
  SCREEN = 7
41
41
  CURTAIN = 8
42
+ ROOF_SEGMENTED_SHUTTER = 9
43
+ SERVOMOTOR = 10
44
+ CURTAIN_ONE_SIDED = 11
45
+ CURTAIN_TWO_SIDED = 12
46
+ TILT_ONLY = 13
47
+ PERGOLA_ROOF = 14
48
+ PERGOLA_ROOF_TILT_ONLY = 15
49
+ TILT_SHUTTER_180 = 16
50
+ TILT_SHUTTER_WITHOUT_POSITIONING = 17
42
51
 
43
52
 
44
53
  class GateBoxControlType(IntEnum):
@@ -155,7 +164,34 @@ class Shutter(Gate):
155
164
 
156
165
  @property
157
166
  def has_tilt(self) -> bool:
158
- return self._control_type == ShutterBoxControlType.TILT_SHUTTER
167
+ return self._control_type in (
168
+ ShutterBoxControlType.TILT_SHUTTER_90,
169
+ ShutterBoxControlType.TILT_SHUTTER_180,
170
+ ShutterBoxControlType.TILT_ONLY,
171
+ ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY,
172
+ ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING,
173
+ )
174
+
175
+ @property
176
+ def tilt_only(self) -> bool:
177
+ return self._control_type in (
178
+ ShutterBoxControlType.TILT_ONLY,
179
+ ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY,
180
+ )
181
+
182
+ @property
183
+ def is_tilt_180(self) -> bool:
184
+ return self._control_type == ShutterBoxControlType.TILT_SHUTTER_180
185
+
186
+ @property
187
+ def is_slider(self) -> bool:
188
+ return self._control_type not in (
189
+ ShutterBoxControlType.NO_CALIBRATION,
190
+ ShutterBoxControlType.CURTAIN,
191
+ ShutterBoxControlType.TILT_ONLY,
192
+ ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY,
193
+ ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING,
194
+ )
159
195
 
160
196
  def read_cover_type(
161
197
  self, alias: str, raw_value: Any, product: "Box"
@@ -164,7 +200,7 @@ class Shutter(Gate):
164
200
  return UnifiedCoverType.SHUTTER
165
201
  if self._control_type == ShutterBoxControlType.NO_CALIBRATION:
166
202
  return UnifiedCoverType.SHUTTER
167
- if self._control_type == ShutterBoxControlType.TILT_SHUTTER:
203
+ if self._control_type == ShutterBoxControlType.TILT_SHUTTER_90:
168
204
  return UnifiedCoverType.SHUTTER
169
205
  if self._control_type == ShutterBoxControlType.WINDOW_OPENER:
170
206
  return UnifiedCoverType.WINDOW
@@ -176,6 +212,24 @@ class Shutter(Gate):
176
212
  return UnifiedCoverType.SHADE
177
213
  if self._control_type == ShutterBoxControlType.CURTAIN:
178
214
  return UnifiedCoverType.CURTAIN
215
+ if self._control_type == ShutterBoxControlType.ROOF_SEGMENTED_SHUTTER:
216
+ return UnifiedCoverType.SHUTTER
217
+ if self._control_type == ShutterBoxControlType.SERVOMOTOR:
218
+ return UnifiedCoverType.SHUTTER
219
+ if self._control_type == ShutterBoxControlType.CURTAIN_ONE_SIDED:
220
+ return UnifiedCoverType.CURTAIN
221
+ if self._control_type == ShutterBoxControlType.CURTAIN_TWO_SIDED:
222
+ return UnifiedCoverType.CURTAIN
223
+ if self._control_type == ShutterBoxControlType.TILT_ONLY:
224
+ return UnifiedCoverType.SHUTTER
225
+ if self._control_type == ShutterBoxControlType.PERGOLA_ROOF:
226
+ return UnifiedCoverType.AWNING
227
+ if self._control_type == ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY:
228
+ return UnifiedCoverType.AWNING
229
+ if self._control_type == ShutterBoxControlType.TILT_SHUTTER_180:
230
+ return UnifiedCoverType.SHUTTER
231
+ if self._control_type == ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING:
232
+ return UnifiedCoverType.SHUTTER
179
233
 
180
234
 
181
235
  class GateBox(Gate):
@@ -345,6 +399,22 @@ class Cover(Feature):
345
399
  def has_stop(self) -> bool:
346
400
  return self._has_stop
347
401
 
402
+ @property
403
+ def tilt_only(self) -> bool:
404
+ return (
405
+ self._attributes.tilt_only
406
+ if hasattr(self._attributes, "tilt_only")
407
+ else False
408
+ )
409
+
410
+ @property
411
+ def is_tilt_180(self) -> bool:
412
+ return (
413
+ self._attributes.is_tilt_180
414
+ if hasattr(self._attributes, "is_tilt_180")
415
+ else False
416
+ )
417
+
348
418
  @property
349
419
  def cover_type(self) -> Optional[UnifiedCoverType]:
350
420
  return self._cover_type
@@ -280,6 +280,23 @@ class Light(Feature):
280
280
  )
281
281
  return int(max(iterable))
282
282
 
283
+ @staticmethod
284
+ def swap_ww_cw(value: list) -> list:
285
+ """Swap WW and CW channels in RGBWW mode for API compatibility."""
286
+ if len(value) == 5:
287
+ value = list(value)
288
+ value[3], value[4] = value[4], value[3]
289
+ return value
290
+
291
+ @staticmethod
292
+ def scale_rgbww_brightness(value: list, brightness: int) -> list:
293
+ """Scale RGBWW value preserving WW:CW ratio."""
294
+ current_max = max(value)
295
+ if current_max > 0:
296
+ return [round(x * brightness / current_max) for x in value]
297
+ else:
298
+ return [0, 0, 0, 0, brightness]
299
+
283
300
  def apply_brightness(self, value: int, brightness: int) -> Any:
284
301
  """Return list of values with applied brightness."""
285
302
  if not isinstance(brightness, int):
@@ -297,7 +314,17 @@ class Light(Feature):
297
314
  if brightness == 0:
298
315
  return [value]
299
316
 
300
- res = list(map(lambda x: round(x * (brightness / 255)), value))
317
+ if self.color_mode in (
318
+ BleboxColorMode.RGB,
319
+ BleboxColorMode.RGBW,
320
+ BleboxColorMode.RGBorW,
321
+ ):
322
+ value = self.normalise_elements_of_rgb(list(value))
323
+ res = list(map(lambda x: round(x * (brightness / 255)), value))
324
+ elif self.color_mode == BleboxColorMode.RGBWW:
325
+ res = self.scale_rgbww_brightness(value, brightness)
326
+ else:
327
+ res = list(map(lambda x: round(x * (brightness / 255)), value))
301
328
  return res
302
329
 
303
330
  def evaluate_off_value(self, config: dict, raw_hex: str) -> str:
@@ -310,10 +337,10 @@ class Light(Feature):
310
337
  :return: str
311
338
  """
312
339
  if self.mask:
313
- if len(raw_hex) < len(self.mask("x").replace("x", "")):
314
- return "0" * len(raw_hex)
315
- else:
316
- return "0" * (len(raw_hex) - len(self.mask("x").replace("x", "")))
340
+ placeholder = self._get_placeholder_for_color_mode()
341
+ mask_result = self.mask(placeholder)
342
+ x_count = mask_result.count("x")
343
+ return "0" * x_count
317
344
  elif raw_hex is not None:
318
345
  if len(raw_hex) < len(config["off"]):
319
346
  return config["off"][: len(raw_hex)]
@@ -356,6 +383,18 @@ class Light(Feature):
356
383
  white_hex = value[6:8]
357
384
  return f"{rgb_hex}{white_hex}"
358
385
 
386
+ def _get_placeholder_for_color_mode(self) -> str:
387
+ if self._color_mode == BleboxColorMode.MONO:
388
+ return "xx"
389
+ elif self._color_mode in (BleboxColorMode.CT, BleboxColorMode.CTx2):
390
+ return "xxxx"
391
+ elif self._color_mode == BleboxColorMode.RGB:
392
+ return "xxxxxx"
393
+ elif self._color_mode in (BleboxColorMode.RGBW, BleboxColorMode.RGBorW):
394
+ return "xxxxxxxx"
395
+ else:
396
+ return "x"
397
+
359
398
  def return_color_temp_with_brightness(
360
399
  self, value, brightness: Any
361
400
  ) -> Optional[str]:
@@ -374,17 +413,8 @@ class Light(Feature):
374
413
  return self.rgb_hex_to_rgb_list(warm + cold)
375
414
 
376
415
  def value_for_selected_channels_from_given_val(self, value: str):
377
- if self.color_mode in [BleboxColorMode.CT, BleboxColorMode.CTx2]:
378
- lambda_result = self.mask("xxxx")
379
- elif self.color_mode == BleboxColorMode.MONO:
380
- lambda_result = self.mask("xx")
381
- elif self.color_mode == BleboxColorMode.RGB:
382
- lambda_result = self.mask("xxxxxx")
383
- elif (
384
- self.color_mode == BleboxColorMode.RGBW
385
- or self.color_mode == BleboxColorMode.RGBorW
386
- ):
387
- lambda_result = self.mask("xxxxxxxx")
416
+ placeholder = self._get_placeholder_for_color_mode()
417
+ lambda_result = self.mask(placeholder)
388
418
  first_index = lambda_result.index("x")
389
419
  last_index = lambda_result.rindex("x")
390
420
  return value[first_index : last_index + 1]
@@ -451,10 +481,9 @@ class Light(Feature):
451
481
 
452
482
  def _set_last_on_value(self, alias, product, raw):
453
483
  if raw == self._off_value:
454
- if (
455
- product.type == "wLightBox"
456
- ): # jezeli urzadzenie typu wLightBox ma wyciagnac last_color
457
- raw = product.expect_rgbw(alias, self.raw_value("last_color"))
484
+ last_color = self.raw_value("last_color")
485
+ if last_color is not None:
486
+ raw = product.expect_rgbw(alias, last_color)
458
487
  if self.mask is not None:
459
488
  raw = self.value_for_selected_channels_from_given_val(raw)
460
489
  if raw == self._off_value:
@@ -518,7 +547,11 @@ class Light(Feature):
518
547
  if self.color_mode in (BleboxColorMode.CT, BleboxColorMode.CTx2):
519
548
  return 255, 255
520
549
  else:
521
- if self.color_mode == BleboxColorMode.MONO:
550
+ if self.color_mode in (
551
+ BleboxColorMode.MONO,
552
+ BleboxColorMode.CT,
553
+ BleboxColorMode.CTx2,
554
+ ):
522
555
  return self.rgb_hex_to_rgb_list(self._last_on_state)
523
556
  return self.normalise_elements_of_rgb(
524
557
  self.rgb_hex_to_rgb_list(self._last_on_state)
@@ -529,11 +562,12 @@ class Light(Feature):
529
562
  return [255] * len(self.rgb_hex_to_rgb_list(self._last_on_state))
530
563
  else:
531
564
  if self.color_mode == BleboxColorMode.RGB:
532
- return self.normalise_elements_of_rgb(
533
- self.rgb_hex_to_rgb_list(self._last_on_state[:6])
534
- )
565
+ return self.rgb_hex_to_rgb_list(self._last_on_state[:6])
535
566
  elif self.color_mode == BleboxColorMode.MONO:
536
567
  return self._last_on_state
568
+ elif self.color_mode == BleboxColorMode.RGBWW:
569
+ result = self.rgb_hex_to_rgb_list(self._last_on_state)
570
+ return self.swap_ww_cw(result)
537
571
  else:
538
572
  return self.rgb_hex_to_rgb_list(self._last_on_state)
539
573
  else:
@@ -46,7 +46,11 @@ class ApiHost:
46
46
  self._loop = loop
47
47
 
48
48
  async def async_request(
49
- self, path: str, async_method: Any, data: Union[dict, str, None] = None
49
+ self,
50
+ path: str,
51
+ async_method: Any,
52
+ data: Union[dict, str, None] = None,
53
+ allow_ota_check_response: bool = False,
50
54
  ) -> Optional[dict]:
51
55
  # TODO: check timeout
52
56
  client_timeout = self._timeout
@@ -57,7 +61,8 @@ class ApiHost:
57
61
  else:
58
62
  response = await async_method(url, timeout=client_timeout)
59
63
 
60
- if response.status != 200:
64
+ accepted_statuses = (200, 202, 204) if allow_ota_check_response else (200,)
65
+ if response.status not in accepted_statuses:
61
66
  if response.status == 401:
62
67
  raise error.UnauthorizedRequest(
63
68
  f"Request to {url} failed with HTTP {response.status}, UNAUTHORISED"
@@ -66,6 +71,9 @@ class ApiHost:
66
71
  f"Request to {url} failed with HTTP {response.status}"
67
72
  )
68
73
 
74
+ if response.content_type != "application/json":
75
+ return None
76
+
69
77
  return await response.json()
70
78
 
71
79
  except asyncio.TimeoutError as ex:
@@ -81,6 +89,11 @@ class ApiHost:
81
89
  except aiohttp.ClientError as ex:
82
90
  raise error.ClientError(f"API request {url} failed: {ex}") from ex
83
91
 
92
+ except UnicodeDecodeError as ex:
93
+ raise error.ConnectionError(
94
+ f"Invalid response encoding from {url}: {ex}"
95
+ ) from ex
96
+
84
97
  async def async_api_get(self, path: str) -> Optional[dict]:
85
98
  try:
86
99
  return await self.async_request(path, self._session.get)
@@ -88,6 +101,15 @@ class ApiHost:
88
101
  logger.error(f"EXCEPTION DURING API CALL: {ex}")
89
102
  raise ex
90
103
 
104
+ async def async_api_get_ota(self, path: str) -> Optional[dict]:
105
+ try:
106
+ return await self.async_request(
107
+ path, self._session.get, allow_ota_check_response=True
108
+ )
109
+ except Exception as ex:
110
+ logger.error(f"EXCEPTION DURING API CALL: {ex}")
111
+ raise ex
112
+
91
113
  async def async_api_post(
92
114
  self, path: str, data: Union[dict, str, None]
93
115
  ) -> Optional[dict]:
@@ -0,0 +1,23 @@
1
+ from typing import Optional
2
+
3
+ from .feature import Feature
4
+
5
+
6
+ class Update(Feature):
7
+ @property
8
+ def installed_version(self) -> Optional[str]:
9
+ return self._product.firmware_version
10
+
11
+ @property
12
+ def latest_version(self) -> Optional[str]:
13
+ return self._product.available_firmware_version
14
+
15
+ async def async_update(self) -> None:
16
+ # OTA state is stored as Box attributes, not in last_data. async_update_data() won't trigger the OTA check.
17
+ await self._product.async_ota_check()
18
+
19
+ async def async_install(self) -> None:
20
+ await self._product.async_ota_update()
21
+
22
+ def after_update(self) -> None:
23
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: blebox-uniapi
3
- Version: 2.5.3
3
+ Version: 2.5.4
4
4
  Summary: Python API for accessing BleBox smart home devices
5
5
  Home-page: https://github.com/blebox/blebox_uniapi
6
6
  Author: BleBox
@@ -19,6 +19,7 @@ blebox_uniapi/light.py
19
19
  blebox_uniapi/sensor.py
20
20
  blebox_uniapi/session.py
21
21
  blebox_uniapi/switch.py
22
+ blebox_uniapi/update.py
22
23
  blebox_uniapi.egg-info/PKG-INFO
23
24
  blebox_uniapi.egg-info/SOURCES.txt
24
25
  blebox_uniapi.egg-info/dependency_links.txt
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.5.3
2
+ current_version = 2.5.4
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -23,7 +23,7 @@ test_requirements = [
23
23
  ]
24
24
 
25
25
  setup(
26
- version="2.5.3",
26
+ version="2.5.4",
27
27
  author="BleBox",
28
28
  author_email="opensource@blebox.eu",
29
29
  python_requires=">=3.9",
File without changes
File without changes
File without changes
File without changes