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.
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/PKG-INFO +1 -1
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/__init__.py +1 -1
- blebox_uniapi-2.5.4/blebox_uniapi/binary_sensor.py +189 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/box.py +28 -2
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/box_types.py +11 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/cover.py +73 -3
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/light.py +68 -26
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/sensor.py +18 -3
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/session.py +24 -2
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/switch.py +11 -0
- blebox_uniapi-2.5.4/blebox_uniapi/update.py +23 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/PKG-INFO +1 -1
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/SOURCES.txt +1 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/setup.cfg +1 -1
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/setup.py +1 -1
- blebox_uniapi-2.5.2/blebox_uniapi/binary_sensor.py +0 -96
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/CONTRIBUTING.rst +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/HISTORY.rst +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/LICENSE +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/MANIFEST.in +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/README.rst +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/button.py +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/climate.py +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/error.py +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/feature.py +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi/jfollow.py +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/not-zip-safe +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/requires.txt +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/blebox_uniapi.egg-info/top_level.txt +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/Makefile +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/authors.rst +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/conf.py +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/contributing.rst +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/history.rst +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/index.rst +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/installation.rst +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/make.bat +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/readme.rst +0 -0
- {blebox_uniapi-2.5.2 → blebox_uniapi-2.5.4}/docs/usage.rst +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
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.
|
|
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,
|
|
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]:
|
|
@@ -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,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
|
|
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
|