blebox-uniapi 2.5.3__tar.gz → 2.5.5__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.5}/PKG-INFO +1 -1
  2. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/__init__.py +1 -1
  3. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/binary_sensor.py +44 -9
  4. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/box.py +28 -2
  5. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/box_types.py +42 -0
  6. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/cover.py +103 -5
  7. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/light.py +58 -24
  8. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/sensor.py +4 -0
  9. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/session.py +24 -2
  10. blebox_uniapi-2.5.5/blebox_uniapi/update.py +23 -0
  11. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/PKG-INFO +1 -1
  12. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/SOURCES.txt +1 -0
  13. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/setup.cfg +1 -1
  14. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/setup.py +1 -1
  15. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/CONTRIBUTING.rst +0 -0
  16. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/HISTORY.rst +0 -0
  17. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/LICENSE +0 -0
  18. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/MANIFEST.in +0 -0
  19. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/README.rst +0 -0
  20. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/button.py +0 -0
  21. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/climate.py +0 -0
  22. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/error.py +0 -0
  23. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/feature.py +0 -0
  24. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/jfollow.py +0 -0
  25. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/switch.py +0 -0
  26. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
  27. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/not-zip-safe +0 -0
  28. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/requires.txt +0 -0
  29. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/top_level.txt +0 -0
  30. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/Makefile +0 -0
  31. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/authors.rst +0 -0
  32. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/conf.py +0 -0
  33. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/contributing.rst +0 -0
  34. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/history.rst +0 -0
  35. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/index.rst +0 -0
  36. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/installation.rst +0 -0
  37. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/make.bat +0 -0
  38. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/readme.rst +0 -0
  39. {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/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.5
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.5"
@@ -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
@@ -372,6 +380,36 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
372
380
  ]
373
381
  ],
374
382
  },
383
+ 20220505: {
384
+ "api_path": "/state/extended",
385
+ "extended_state_path": "/state/extended",
386
+ "api": {
387
+ # note: old control api (i.e. /s/0, /s/1, /s/2) still supported but
388
+ # now deprecated. switchBox has now API consistent with switchBoxD
389
+ "on": lambda x=None: ("GET", f"/s/{x}/1", None),
390
+ "off": lambda x=None: ("GET", f"/s/{x}/0", None),
391
+ },
392
+ "switches": [
393
+ [
394
+ "relay",
395
+ {"state": lambda x: f"relays[?relay==`{x}`]|[0]|state"},
396
+ "relay",
397
+ ]
398
+ ],
399
+ "sensors": [
400
+ [
401
+ "switchBox.energy",
402
+ {
403
+ # note: switchbox/switchboxD sensors are currently not indexed (singletons)
404
+ "powerConsumption": lambda x: "powerMeasuring.powerConsumption[0]|value ",
405
+ "activePower": lambda x: "sensors[?type == 'activePower']|[0]|value",
406
+ "voltage": lambda x: "sensors[?type == 'voltage']|[0]|value",
407
+ "periodS": "powerMeasuring.powerConsumption[0]|periodS",
408
+ "measurement_enabled": "powerMeasuring.enabled",
409
+ },
410
+ ]
411
+ ],
412
+ },
375
413
  },
376
414
  # switchBoxD
377
415
  "switchBoxD": {
@@ -716,11 +754,15 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
716
754
  "reactivePower": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reactivePower']|[0]|value",
717
755
  "reverseActiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reverseActiveEnergy']|[0]|value",
718
756
  "forwardActiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'forwardActiveEnergy']|[0]|value",
757
+ "forwardReactiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'forwardReactiveEnergy']|[0]|value",
758
+ "reverseReactiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reverseReactiveEnergy']|[0]|value",
719
759
  "illuminance": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'illuminance']|[0]|value",
720
760
  "temperature": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'temperature']|[0]|value",
721
761
  "wind": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'wind']|[0]|value",
722
762
  "humidity": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'humidity']|[0]|value",
723
763
  "openStatus": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'openStatus']|[0]|value",
764
+ "co2": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2']|[0]|value",
765
+ "co2Definition": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2Definition']|[0]|value",
724
766
  },
725
767
  ]
726
768
  ],
@@ -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):
@@ -92,7 +101,7 @@ class UnifiedCoverType(IntEnum):
92
101
  class Gate:
93
102
  _control_type: Optional[int]
94
103
 
95
- def __init__(self, control_type: int):
104
+ def __init__(self, control_type: int, is_calibrated: bool = False):
96
105
  self._control_type = control_type
97
106
 
98
107
  def read_state(self, alias: str, raw_value: Any, product: "Box") -> int:
@@ -139,6 +148,10 @@ class Gate:
139
148
  def close_command(self) -> str:
140
149
  return "close"
141
150
 
151
+ @property
152
+ def is_calibrated(self) -> None:
153
+ return None
154
+
142
155
  def stop_command(self, has_stop: bool) -> str:
143
156
  return "stop"
144
157
 
@@ -148,6 +161,11 @@ class Gate:
148
161
 
149
162
  class Shutter(Gate):
150
163
  _control_type: Optional[ShutterBoxControlType]
164
+ _is_calibrated: bool
165
+
166
+ def __init__(self, control_type: int, is_calibrated: bool = False):
167
+ super().__init__(control_type)
168
+ self._is_calibrated = is_calibrated
151
169
 
152
170
  @property
153
171
  def min_position(self) -> int:
@@ -155,7 +173,42 @@ class Shutter(Gate):
155
173
 
156
174
  @property
157
175
  def has_tilt(self) -> bool:
158
- return self._control_type == ShutterBoxControlType.TILT_SHUTTER
176
+ return self._control_type in (
177
+ ShutterBoxControlType.TILT_SHUTTER_90,
178
+ ShutterBoxControlType.TILT_SHUTTER_180,
179
+ ShutterBoxControlType.TILT_ONLY,
180
+ ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY,
181
+ ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING,
182
+ )
183
+
184
+ @property
185
+ def tilt_only(self) -> bool:
186
+ return self._control_type in (
187
+ ShutterBoxControlType.TILT_ONLY,
188
+ ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY,
189
+ )
190
+
191
+ @property
192
+ def is_tilt_180(self) -> bool:
193
+ return self._control_type == ShutterBoxControlType.TILT_SHUTTER_180
194
+
195
+ @property
196
+ def is_slider(self) -> bool:
197
+ return (
198
+ self._control_type
199
+ not in (
200
+ ShutterBoxControlType.NO_CALIBRATION,
201
+ ShutterBoxControlType.CURTAIN,
202
+ ShutterBoxControlType.TILT_ONLY,
203
+ ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY,
204
+ ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING,
205
+ )
206
+ and self.is_calibrated is True
207
+ )
208
+
209
+ @property
210
+ def is_calibrated(self) -> bool:
211
+ return self._is_calibrated
159
212
 
160
213
  def read_cover_type(
161
214
  self, alias: str, raw_value: Any, product: "Box"
@@ -164,7 +217,7 @@ class Shutter(Gate):
164
217
  return UnifiedCoverType.SHUTTER
165
218
  if self._control_type == ShutterBoxControlType.NO_CALIBRATION:
166
219
  return UnifiedCoverType.SHUTTER
167
- if self._control_type == ShutterBoxControlType.TILT_SHUTTER:
220
+ if self._control_type == ShutterBoxControlType.TILT_SHUTTER_90:
168
221
  return UnifiedCoverType.SHUTTER
169
222
  if self._control_type == ShutterBoxControlType.WINDOW_OPENER:
170
223
  return UnifiedCoverType.WINDOW
@@ -176,6 +229,24 @@ class Shutter(Gate):
176
229
  return UnifiedCoverType.SHADE
177
230
  if self._control_type == ShutterBoxControlType.CURTAIN:
178
231
  return UnifiedCoverType.CURTAIN
232
+ if self._control_type == ShutterBoxControlType.ROOF_SEGMENTED_SHUTTER:
233
+ return UnifiedCoverType.SHUTTER
234
+ if self._control_type == ShutterBoxControlType.SERVOMOTOR:
235
+ return UnifiedCoverType.SHUTTER
236
+ if self._control_type == ShutterBoxControlType.CURTAIN_ONE_SIDED:
237
+ return UnifiedCoverType.CURTAIN
238
+ if self._control_type == ShutterBoxControlType.CURTAIN_TWO_SIDED:
239
+ return UnifiedCoverType.CURTAIN
240
+ if self._control_type == ShutterBoxControlType.TILT_ONLY:
241
+ return UnifiedCoverType.SHUTTER
242
+ if self._control_type == ShutterBoxControlType.PERGOLA_ROOF:
243
+ return UnifiedCoverType.AWNING
244
+ if self._control_type == ShutterBoxControlType.PERGOLA_ROOF_TILT_ONLY:
245
+ return UnifiedCoverType.AWNING
246
+ if self._control_type == ShutterBoxControlType.TILT_SHUTTER_180:
247
+ return UnifiedCoverType.SHUTTER
248
+ if self._control_type == ShutterBoxControlType.TILT_SHUTTER_WITHOUT_POSITIONING:
249
+ return UnifiedCoverType.SHUTTER
179
250
 
180
251
 
181
252
  class GateBox(Gate):
@@ -301,13 +372,20 @@ class Cover(Feature):
301
372
  extended_state: dict,
302
373
  ) -> None:
303
374
  control_type = None
375
+ is_calibrated: bool = False
304
376
  if extended_state and issubclass(subclass, Shutter):
305
377
  control_type = extended_state.get("shutter", {}).get("controlType", None)
378
+ is_calibrated = bool(
379
+ extended_state.get("shutter", {})
380
+ .get("calibrationParameters", {})
381
+ .get("isCalibrated", False)
382
+ )
383
+
306
384
  elif extended_state and issubclass(subclass, GateBoxB):
307
385
  control_type = extended_state.get("gate", {}).get("openCloseMode", None)
308
386
 
309
387
  self._device_class = dev_class
310
- self._attributes: GateT = subclass(control_type)
388
+ self._attributes: GateT = subclass(control_type, is_calibrated)
311
389
  self._tilt_current = None
312
390
  super().__init__(product, alias, methods)
313
391
 
@@ -329,6 +407,10 @@ class Cover(Feature):
329
407
  def tilt_current(self):
330
408
  return self._tilt_current
331
409
 
410
+ @property
411
+ def is_calibrated(self) -> Optional[bool]:
412
+ return self._attributes.is_calibrated
413
+
332
414
  @property
333
415
  def is_slider(self) -> Any:
334
416
  return self._attributes.is_slider
@@ -345,6 +427,22 @@ class Cover(Feature):
345
427
  def has_stop(self) -> bool:
346
428
  return self._has_stop
347
429
 
430
+ @property
431
+ def tilt_only(self) -> bool:
432
+ return (
433
+ self._attributes.tilt_only
434
+ if hasattr(self._attributes, "tilt_only")
435
+ else False
436
+ )
437
+
438
+ @property
439
+ def is_tilt_180(self) -> bool:
440
+ return (
441
+ self._attributes.is_tilt_180
442
+ if hasattr(self._attributes, "is_tilt_180")
443
+ else False
444
+ )
445
+
348
446
  @property
349
447
  def cover_type(self) -> Optional[UnifiedCoverType]:
350
448
  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:
@@ -160,12 +160,16 @@ class BaseSensor(Feature):
160
160
  @SensorFactory.register("apparentPower", unit="va")
161
161
  @SensorFactory.register("reactivePower", unit="var")
162
162
  @SensorFactory.register("activePower", unit="W")
163
+ @SensorFactory.register("reverseReactiveEnergy", unit="kvarh", scale=1_000)
164
+ @SensorFactory.register("forwardReactiveEnergy", unit="kvarh", scale=1_000)
163
165
  @SensorFactory.register("reverseActiveEnergy", unit="kWh", scale=1_000)
164
166
  @SensorFactory.register("forwardActiveEnergy", unit="kWh", scale=1_000)
165
167
  @SensorFactory.register("illuminance", unit="lx", scale=100)
166
168
  @SensorFactory.register("humidity", unit="percentage", scale=100)
167
169
  @SensorFactory.register("wind", unit="m/s", scale=10)
168
170
  @SensorFactory.register("openStatus", unit="")
171
+ @SensorFactory.register("co2", unit="ppm")
172
+ @SensorFactory.register("co2Definition", unit="")
169
173
  class GenericSensor(BaseSensor):
170
174
  def __init__(
171
175
  # base sensor params
@@ -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.5
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.5
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.5",
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