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.
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/PKG-INFO +1 -1
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/__init__.py +1 -1
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/binary_sensor.py +44 -9
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/box.py +28 -2
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/box_types.py +8 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/cover.py +73 -3
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/light.py +58 -24
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/session.py +24 -2
- blebox_uniapi-2.5.4/blebox_uniapi/update.py +23 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/PKG-INFO +1 -1
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/SOURCES.txt +1 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/setup.cfg +1 -1
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/setup.py +1 -1
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/CONTRIBUTING.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/HISTORY.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/LICENSE +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/MANIFEST.in +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/README.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/button.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/climate.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/error.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/feature.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/jfollow.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/sensor.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi/switch.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/not-zip-safe +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/requires.txt +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/top_level.txt +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/Makefile +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/authors.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/conf.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/contributing.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/history.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/index.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/installation.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/make.bat +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/readme.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.4}/docs/usage.rst +0 -0
|
@@ -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__(
|
|
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__(
|
|
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__(
|
|
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__(
|
|
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__(
|
|
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
|
-
|
|
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")
|
|
@@ -33,12 +33,21 @@ class ShutterBoxControlType(IntEnum):
|
|
|
33
33
|
|
|
34
34
|
SEGMENTED_SHUTTER = 1
|
|
35
35
|
NO_CALIBRATION = 2
|
|
36
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|