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.
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/PKG-INFO +1 -1
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/__init__.py +1 -1
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/binary_sensor.py +44 -9
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/box.py +28 -2
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/box_types.py +42 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/cover.py +103 -5
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/light.py +58 -24
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/sensor.py +4 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/session.py +24 -2
- blebox_uniapi-2.5.5/blebox_uniapi/update.py +23 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/PKG-INFO +1 -1
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/SOURCES.txt +1 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/setup.cfg +1 -1
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/setup.py +1 -1
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/CONTRIBUTING.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/HISTORY.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/LICENSE +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/MANIFEST.in +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/README.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/button.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/climate.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/error.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/feature.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/jfollow.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi/switch.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/not-zip-safe +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/requires.txt +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/blebox_uniapi.egg-info/top_level.txt +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/Makefile +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/authors.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/conf.py +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/contributing.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/history.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/index.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/installation.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/make.bat +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/docs/readme.rst +0 -0
- {blebox_uniapi-2.5.3 → blebox_uniapi-2.5.5}/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")
|
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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:
|
|
@@ -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,
|
|
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
|