blebox-uniapi 2.5.2__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 (40) hide show
  1. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/PKG-INFO +1 -1
  2. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/__init__.py +1 -1
  3. blebox_uniapi-2.5.4/blebox_uniapi/binary_sensor.py +189 -0
  4. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/box.py +28 -2
  5. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/box_types.py +11 -0
  6. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/cover.py +73 -3
  7. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/light.py +68 -26
  8. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/sensor.py +18 -3
  9. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/session.py +24 -2
  10. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/switch.py +11 -0
  11. blebox_uniapi-2.5.4/blebox_uniapi/update.py +23 -0
  12. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/PKG-INFO +1 -1
  13. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/SOURCES.txt +1 -0
  14. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/setup.cfg +1 -1
  15. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/setup.py +1 -1
  16. blebox_uniapi-2.5.2/blebox_uniapi/binary_sensor.py +0 -96
  17. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/CONTRIBUTING.rst +0 -0
  18. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/HISTORY.rst +0 -0
  19. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/LICENSE +0 -0
  20. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/MANIFEST.in +0 -0
  21. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/README.rst +0 -0
  22. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/button.py +0 -0
  23. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/climate.py +0 -0
  24. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/error.py +0 -0
  25. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/feature.py +0 -0
  26. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/jfollow.py +0 -0
  27. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
  28. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/not-zip-safe +0 -0
  29. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/requires.txt +0 -0
  30. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/top_level.txt +0 -0
  31. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/Makefile +0 -0
  32. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/authors.rst +0 -0
  33. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/conf.py +0 -0
  34. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/contributing.rst +0 -0
  35. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/history.rst +0 -0
  36. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/index.rst +0 -0
  37. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/installation.rst +0 -0
  38. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/make.bat +0 -0
  39. {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/readme.rst +0 -0
  40. {blebox_uniapi-2.5.2 → 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.2
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.2"
5
+ __version__ = "2.5.4"
@@ -0,0 +1,189 @@
1
+ from typing import Union, Optional, TYPE_CHECKING
2
+
3
+ from .feature import Feature
4
+
5
+ if TYPE_CHECKING:
6
+ from .box import Box
7
+
8
+
9
+ class BinarySensor(Feature):
10
+ """Class representing sensor with bool state."""
11
+
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
+ ):
20
+ self._sensor_id = sensor_id
21
+ self._name = name
22
+ super().__init__(product, alias, methods)
23
+
24
+ @property
25
+ def index(self) -> Optional[int]:
26
+ return self._sensor_id
27
+
28
+ @property
29
+ def name(self) -> Optional[str]:
30
+ return self._name
31
+
32
+ @classmethod
33
+ def many_from_config(
34
+ cls, product, box_type_config, extended_state
35
+ ) -> list["Feature"]:
36
+ type_class_map = {
37
+ "rain": Rain,
38
+ "flood": Flood,
39
+ "openStatus": Open,
40
+ "binary": Input,
41
+ }
42
+
43
+ output_list = list()
44
+ sensors_list = extended_state.get("multiSensor").get("sensors", {})
45
+ alias, methods = box_type_config[0]
46
+
47
+ for sensor in sensors_list:
48
+ sensor_type = sensor.get("type")
49
+ sensor_id = sensor.get("id")
50
+
51
+ if sensor_type in type_class_map:
52
+ klass = type_class_map[sensor_type]
53
+
54
+ if methods.get(sensor_type) is not None:
55
+ value_method = {sensor_type: methods[sensor_type](sensor_id)}
56
+ output_list.append(
57
+ klass(
58
+ product=product,
59
+ alias=sensor_type + "_" + str(sensor_id),
60
+ methods=value_method,
61
+ sensor_id=sensor_id,
62
+ name=sensor.get("name"),
63
+ )
64
+ )
65
+
66
+ return output_list
67
+
68
+
69
+ class Rain(BinarySensor):
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
+ ):
78
+ self._device_class = "moisture"
79
+ super().__init__(product, alias, methods, sensor_id=sensor_id, name=name)
80
+
81
+ @property
82
+ def state(self) -> bool:
83
+ return self._current > 0
84
+
85
+ @property
86
+ def device_class(self) -> str:
87
+ return self._device_class
88
+
89
+ def _read_rain(self, field: str) -> Union[float, int, None]:
90
+ product = self._product
91
+ if product.last_data is not None:
92
+ raw = self.raw_value(field)
93
+ if raw is not None: # no reading
94
+ return self.raw_value("rain")
95
+ return 0
96
+
97
+ def after_update(self) -> None:
98
+ self._current = self._read_rain("rain")
99
+
100
+
101
+ class Flood(BinarySensor):
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
+ ):
110
+ self._device_class = "moisture"
111
+ super().__init__(product, alias, methods, sensor_id=sensor_id, name=name)
112
+
113
+ @property
114
+ def state(self) -> bool:
115
+ return self._current > 0
116
+
117
+ @property
118
+ def device_class(self) -> str:
119
+ return self._device_class
120
+
121
+ def _read_flood(self, field: str) -> Union[float, int, None]:
122
+ product = self._product
123
+
124
+ if product.last_data is not None:
125
+ raw = self.raw_value(field)
126
+ if raw is not None: # no reading
127
+ return self.raw_value("flood")
128
+ return 0
129
+
130
+ def after_update(self) -> None:
131
+ self._current = self._read_flood("flood")
132
+
133
+
134
+ class Open(BinarySensor):
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
+ ):
143
+ self._device_class = "open"
144
+ super().__init__(product, alias, methods, sensor_id=sensor_id, name=name)
145
+
146
+ @property
147
+ def state(self) -> bool:
148
+ return self._current != 4 # 4 = closed, 0-3 = open
149
+
150
+ def _read_input(self, field: str) -> Union[float, int, None]:
151
+ product = self._product
152
+
153
+ if product.last_data is not None:
154
+ raw = self.raw_value(field)
155
+ if raw is not None: # no reading
156
+ return self.raw_value("openStatus")
157
+ return 0
158
+
159
+ def after_update(self) -> None:
160
+ self._current = self._read_input("openStatus")
161
+
162
+
163
+ class Input(BinarySensor):
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
+ ):
172
+ self._device_class = "input"
173
+ super().__init__(product, alias, methods, sensor_id=sensor_id, name=name)
174
+
175
+ @property
176
+ def state(self) -> bool:
177
+ return self._current > 0
178
+
179
+ def _read_input(self, field: str) -> Union[float, int, None]:
180
+ product = self._product
181
+
182
+ if product.last_data is not None:
183
+ raw = self.raw_value(field)
184
+ if raw is not None: # no reading
185
+ return self.raw_value("binary")
186
+ return 0
187
+
188
+ def after_update(self) -> None:
189
+ self._current = self._read_input("binary")
@@ -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
@@ -720,6 +728,7 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
720
728
  "temperature": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'temperature']|[0]|value",
721
729
  "wind": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'wind']|[0]|value",
722
730
  "humidity": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'humidity']|[0]|value",
731
+ "openStatus": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'openStatus']|[0]|value",
723
732
  },
724
733
  ]
725
734
  ],
@@ -729,6 +738,8 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
729
738
  {
730
739
  "rain": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'rain']|[0]|value",
731
740
  "flood": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'flood']|[0]|value",
741
+ "openStatus": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'openStatus']|[0]|value",
742
+ "binary": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'binary']|[0]|value",
732
743
  },
733
744
  ]
734
745
  ],
@@ -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
@@ -112,8 +112,10 @@ class Light(Feature):
112
112
  color_mode,
113
113
  effect_list,
114
114
  current_effect,
115
+ index: Optional[int] = None,
115
116
  ) -> None:
116
117
  super().__init__(product, alias, methods)
118
+ self._index = index
117
119
  config = self.CONFIG[product.type]
118
120
  self.mask = mask
119
121
  self.desired_color = desired_color
@@ -187,6 +189,7 @@ class Light(Feature):
187
189
  product,
188
190
  alias=alias + "_" + indicator,
189
191
  mask=mask,
192
+ index=i,
190
193
  **const_kwargs,
191
194
  )
192
195
  )
@@ -208,12 +211,13 @@ class Light(Feature):
208
211
  ct = ctx2
209
212
  if len(desired_color) > 8:
210
213
  ct = ctx2_v3
211
- for indicator, mask in ct.items():
214
+ for i, (indicator, mask) in enumerate(ct.items()):
212
215
  object_list.append(
213
216
  cls(
214
217
  product,
215
218
  alias=alias + "_" + indicator,
216
219
  mask=mask,
220
+ index=i,
217
221
  **const_kwargs,
218
222
  )
219
223
  )
@@ -247,6 +251,10 @@ class Light(Feature):
247
251
  else:
248
252
  return None
249
253
 
254
+ @property
255
+ def index(self) -> Optional[int]:
256
+ return self._index
257
+
250
258
  @property
251
259
  def effect_list(self):
252
260
  if isinstance(self._effect_list, dict):
@@ -272,6 +280,23 @@ class Light(Feature):
272
280
  )
273
281
  return int(max(iterable))
274
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
+
275
300
  def apply_brightness(self, value: int, brightness: int) -> Any:
276
301
  """Return list of values with applied brightness."""
277
302
  if not isinstance(brightness, int):
@@ -289,7 +314,17 @@ class Light(Feature):
289
314
  if brightness == 0:
290
315
  return [value]
291
316
 
292
- 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))
293
328
  return res
294
329
 
295
330
  def evaluate_off_value(self, config: dict, raw_hex: str) -> str:
@@ -302,10 +337,10 @@ class Light(Feature):
302
337
  :return: str
303
338
  """
304
339
  if self.mask:
305
- if len(raw_hex) < len(self.mask("x").replace("x", "")):
306
- return "0" * len(raw_hex)
307
- else:
308
- 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
309
344
  elif raw_hex is not None:
310
345
  if len(raw_hex) < len(config["off"]):
311
346
  return config["off"][: len(raw_hex)]
@@ -348,6 +383,18 @@ class Light(Feature):
348
383
  white_hex = value[6:8]
349
384
  return f"{rgb_hex}{white_hex}"
350
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
+
351
398
  def return_color_temp_with_brightness(
352
399
  self, value, brightness: Any
353
400
  ) -> Optional[str]:
@@ -366,17 +413,8 @@ class Light(Feature):
366
413
  return self.rgb_hex_to_rgb_list(warm + cold)
367
414
 
368
415
  def value_for_selected_channels_from_given_val(self, value: str):
369
- if self.color_mode in [BleboxColorMode.CT, BleboxColorMode.CTx2]:
370
- lambda_result = self.mask("xxxx")
371
- elif self.color_mode == BleboxColorMode.MONO:
372
- lambda_result = self.mask("xx")
373
- elif self.color_mode == BleboxColorMode.RGB:
374
- lambda_result = self.mask("xxxxxx")
375
- elif (
376
- self.color_mode == BleboxColorMode.RGBW
377
- or self.color_mode == BleboxColorMode.RGBorW
378
- ):
379
- lambda_result = self.mask("xxxxxxxx")
416
+ placeholder = self._get_placeholder_for_color_mode()
417
+ lambda_result = self.mask(placeholder)
380
418
  first_index = lambda_result.index("x")
381
419
  last_index = lambda_result.rindex("x")
382
420
  return value[first_index : last_index + 1]
@@ -443,10 +481,9 @@ class Light(Feature):
443
481
 
444
482
  def _set_last_on_value(self, alias, product, raw):
445
483
  if raw == self._off_value:
446
- if (
447
- product.type == "wLightBox"
448
- ): # jezeli urzadzenie typu wLightBox ma wyciagnac last_color
449
- 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)
450
487
  if self.mask is not None:
451
488
  raw = self.value_for_selected_channels_from_given_val(raw)
452
489
  if raw == self._off_value:
@@ -506,11 +543,15 @@ class Light(Feature):
506
543
  if self.color_mode in (BleboxColorMode.RGBW, BleboxColorMode.RGBorW):
507
544
  return 255, 255, 255, 255
508
545
  if self.color_mode == BleboxColorMode.MONO:
509
- return 255
546
+ return [255]
510
547
  if self.color_mode in (BleboxColorMode.CT, BleboxColorMode.CTx2):
511
548
  return 255, 255
512
549
  else:
513
- if self.color_mode == BleboxColorMode.MONO:
550
+ if self.color_mode in (
551
+ BleboxColorMode.MONO,
552
+ BleboxColorMode.CT,
553
+ BleboxColorMode.CTx2,
554
+ ):
514
555
  return self.rgb_hex_to_rgb_list(self._last_on_state)
515
556
  return self.normalise_elements_of_rgb(
516
557
  self.rgb_hex_to_rgb_list(self._last_on_state)
@@ -521,11 +562,12 @@ class Light(Feature):
521
562
  return [255] * len(self.rgb_hex_to_rgb_list(self._last_on_state))
522
563
  else:
523
564
  if self.color_mode == BleboxColorMode.RGB:
524
- return self.normalise_elements_of_rgb(
525
- self.rgb_hex_to_rgb_list(self._last_on_state[:6])
526
- )
565
+ return self.rgb_hex_to_rgb_list(self._last_on_state[:6])
527
566
  elif self.color_mode == BleboxColorMode.MONO:
528
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)
529
571
  else:
530
572
  return self.rgb_hex_to_rgb_list(self._last_on_state)
531
573
  else:
@@ -77,6 +77,7 @@ class SensorFactory:
77
77
  alias=alias,
78
78
  methods=materialized_methods,
79
79
  sensor_id=sensor_id,
80
+ name=sensor.get("name"),
80
81
  )
81
82
  object_list.append(feature)
82
83
 
@@ -110,9 +111,11 @@ class BaseSensor(Feature):
110
111
  methods: dict,
111
112
  sensor_type: str = None,
112
113
  sensor_id: Optional[int] = None,
114
+ name: Optional[str] = None,
113
115
  ):
114
116
  self._sensor_type = sensor_type
115
117
  self._sensor_id = sensor_id
118
+ self._name = name
116
119
  super().__init__(product, alias, methods)
117
120
 
118
121
  @property
@@ -135,6 +138,14 @@ class BaseSensor(Feature):
135
138
  def probe_id(self):
136
139
  return self.sensor_id
137
140
 
141
+ @property
142
+ def index(self) -> Optional[int]:
143
+ return self._sensor_id
144
+
145
+ @property
146
+ def name(self) -> Optional[str]:
147
+ return self._name
148
+
138
149
  @classmethod
139
150
  def many_from_config(cls, product, box_type_config, extended_state):
140
151
  raise NotImplementedError("Please use SensorFactory")
@@ -154,6 +165,7 @@ class BaseSensor(Feature):
154
165
  @SensorFactory.register("illuminance", unit="lx", scale=100)
155
166
  @SensorFactory.register("humidity", unit="percentage", scale=100)
156
167
  @SensorFactory.register("wind", unit="m/s", scale=10)
168
+ @SensorFactory.register("openStatus", unit="")
157
169
  class GenericSensor(BaseSensor):
158
170
  def __init__(
159
171
  # base sensor params
@@ -162,6 +174,7 @@ class GenericSensor(BaseSensor):
162
174
  alias: str,
163
175
  methods: dict,
164
176
  sensor_id: Optional[int],
177
+ name: Optional[str] = None,
165
178
  *,
166
179
  # generalization params
167
180
  sensor_type: str,
@@ -169,7 +182,7 @@ class GenericSensor(BaseSensor):
169
182
  scale: float = 1,
170
183
  precision: Optional[int] = None,
171
184
  ):
172
- super().__init__(product, alias, methods, sensor_id=sensor_id)
185
+ super().__init__(product, alias, methods, sensor_id=sensor_id, name=name)
173
186
  self._unit = unit
174
187
  self._scale = scale
175
188
  self._precision = precision
@@ -225,8 +238,9 @@ class Temperature(BaseSensor):
225
238
  alias: str,
226
239
  methods: dict,
227
240
  sensor_id: Optional[int] = None,
241
+ name: Optional[str] = None,
228
242
  ):
229
- super().__init__(product, alias, methods, sensor_id=sensor_id)
243
+ super().__init__(product, alias, methods, sensor_id=sensor_id, name=name)
230
244
  self._unit = "celsius"
231
245
  self._device_class = "temperature"
232
246
 
@@ -258,8 +272,9 @@ class AirQuality(BaseSensor):
258
272
  alias: str,
259
273
  methods: dict,
260
274
  sensor_id: Optional[str] = None,
275
+ name: Optional[str] = None,
261
276
  ):
262
- super().__init__(product, alias, methods, sensor_id)
277
+ super().__init__(product, alias, methods, sensor_id, name=name)
263
278
  self._unit = "concentration_of_mp"
264
279
  self._device_class = alias
265
280
 
@@ -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]:
@@ -15,11 +15,13 @@ class Switch(Feature):
15
15
  methods: dict,
16
16
  dev_class: str,
17
17
  unit_id: Union[str, int, None] = 0,
18
+ name: Optional[str] = None,
18
19
  ):
19
20
  methods = self.resolve_access_method_paths(methods, str(unit_id))
20
21
  super().__init__(product, alias, methods)
21
22
  self._device_class = dev_class
22
23
  self._unit_id = unit_id
24
+ self._name = name
23
25
 
24
26
  @classmethod
25
27
  def many_from_config(
@@ -46,6 +48,7 @@ class Switch(Feature):
46
48
  methods,
47
49
  relay_type,
48
50
  relay_id,
51
+ name=relay.get("name"),
49
52
  )
50
53
  )
51
54
 
@@ -74,6 +77,14 @@ class Switch(Feature):
74
77
  unit = self._unit_id
75
78
  return [] if unit is None else [unit]
76
79
 
80
+ @property
81
+ def index(self) -> Optional[int]:
82
+ return self._unit_id
83
+
84
+ @property
85
+ def name(self) -> Optional[str]:
86
+ return self._name
87
+
77
88
  async def async_turn_on(self, **kwargs: Any) -> None:
78
89
  await self.async_api_command("on", *self._unit_args)
79
90
 
@@ -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.2
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.2
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.2",
26
+ version="2.5.4",
27
27
  author="BleBox",
28
28
  author_email="opensource@blebox.eu",
29
29
  python_requires=">=3.9",
@@ -1,96 +0,0 @@
1
- from typing import Union, TYPE_CHECKING
2
-
3
- from .feature import Feature
4
-
5
- if TYPE_CHECKING:
6
- from .box import Box
7
-
8
-
9
- class BinarySensor(Feature):
10
- """Class representing sensor with bool state."""
11
-
12
- def __init__(self, product: "Box", alias: str, methods: dict):
13
- super().__init__(product, alias, methods)
14
-
15
- @classmethod
16
- def many_from_config(
17
- cls, product, box_type_config, extended_state
18
- ) -> list["Feature"]:
19
- type_class_map = {
20
- "rain": Rain,
21
- "flood": Flood,
22
- }
23
-
24
- output_list = list()
25
- sensors_list = extended_state.get("multiSensor").get("sensors", {})
26
- alias, methods = box_type_config[0]
27
-
28
- for sensor in sensors_list:
29
- sensor_type = sensor.get("type")
30
- sensor_id = sensor.get("id")
31
-
32
- if sensor_type in type_class_map:
33
- klass = type_class_map[sensor_type]
34
-
35
- if methods.get(sensor_type) is not None:
36
- value_method = {sensor_type: methods[sensor_type](sensor_id)}
37
- output_list.append(
38
- klass(
39
- product=product,
40
- alias=sensor_type + "_" + str(sensor_id),
41
- methods=value_method,
42
- )
43
- )
44
-
45
- return output_list
46
-
47
-
48
- class Rain(BinarySensor):
49
- def __init__(self, product: "Box", alias: str, methods: dict):
50
- self._device_class = "moisture"
51
- super().__init__(product, alias, methods)
52
-
53
- @property
54
- def state(self) -> bool:
55
- return self._current > 0
56
-
57
- @property
58
- def device_class(self) -> str:
59
- return self._device_class
60
-
61
- def _read_rain(self, field: str) -> Union[float, int, None]:
62
- product = self._product
63
- if product.last_data is not None:
64
- raw = self.raw_value(field)
65
- if raw is not None: # no reading
66
- return self.raw_value("rain")
67
- return 0
68
-
69
- def after_update(self) -> None:
70
- self._current = self._read_rain("rain")
71
-
72
-
73
- class Flood(BinarySensor):
74
- def __init__(self, product: "Box", alias: str, methods: dict):
75
- self._device_class = "moisture"
76
- super().__init__(product, alias, methods)
77
-
78
- @property
79
- def state(self) -> bool:
80
- return self._current > 0
81
-
82
- @property
83
- def device_class(self) -> str:
84
- return self._device_class
85
-
86
- def _read_flood(self, field: str) -> Union[float, int, None]:
87
- product = self._product
88
-
89
- if product.last_data is not None:
90
- raw = self.raw_value(field)
91
- if raw is not None: # no reading
92
- return self.raw_value("flood")
93
- return 0
94
-
95
- def after_update(self) -> None:
96
- self._current = self._read_flood("flood")
File without changes
File without changes
File without changes
File without changes