blebox-uniapi 2.5.6__tar.gz → 2.5.8__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.6 → blebox_uniapi-2.5.8}/PKG-INFO +1 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/__init__.py +1 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/box.py +4 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/box_types.py +37 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/climate.py +38 -7
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/sensor.py +100 -20
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi.egg-info/PKG-INFO +1 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/setup.cfg +1 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/setup.py +1 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_box.py +18 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_climate.py +39 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_sensor.py +207 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/CONTRIBUTING.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/HISTORY.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/LICENSE +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/MANIFEST.in +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/README.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/binary_sensor.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/button.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/cover.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/error.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/feature.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/jfollow.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/light.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/session.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/switch.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi/update.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi.egg-info/SOURCES.txt +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi.egg-info/not-zip-safe +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi.egg-info/requires.txt +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/blebox_uniapi.egg-info/top_level.txt +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/docs/Makefile +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/docs/authors.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/docs/conf.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/docs/contributing.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/docs/history.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/docs/index.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/docs/installation.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/docs/make.bat +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/docs/readme.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/docs/usage.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_box_types.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_button.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_cover.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_light.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_light_unit.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_session.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_switch.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_unknown.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.8}/tests/test_update.py +0 -0
|
@@ -387,7 +387,10 @@ class Box:
|
|
|
387
387
|
await self._session.async_api_get_ota("/api/ota/check")
|
|
388
388
|
for _ in range(3):
|
|
389
389
|
await asyncio.sleep(1)
|
|
390
|
-
|
|
390
|
+
try:
|
|
391
|
+
response = await self._session.async_api_get("/api/device/state")
|
|
392
|
+
except HttpError:
|
|
393
|
+
response = await self._session.async_api_get("/info")
|
|
391
394
|
if response is None:
|
|
392
395
|
raise UnsupportedBoxResponse("Device returned non-JSON response")
|
|
393
396
|
response = response.get("device", response)
|
|
@@ -191,6 +191,7 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
|
|
|
191
191
|
"minimum": "thermo.minimumTemp",
|
|
192
192
|
"maximum": "thermo.maximumTemp",
|
|
193
193
|
"temperature": lambda x: f"sensors[?id == `{x}`]|[0]|value",
|
|
194
|
+
"temperature.state": lambda x: f"sensors[?id == `{x}`]|[0]|state",
|
|
194
195
|
"state": "thermo.state",
|
|
195
196
|
"mode": "thermo.mode",
|
|
196
197
|
"safetySensorId": "thermo.safetyTempSensor.sensorId",
|
|
@@ -203,6 +204,7 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
|
|
|
203
204
|
"temperature",
|
|
204
205
|
{
|
|
205
206
|
"temperature": lambda x: f"sensors[?id == `{x}`]|[0]|value",
|
|
207
|
+
"temperature.state": lambda x: f"sensors[?id == `{x}`]|[0]|state",
|
|
206
208
|
},
|
|
207
209
|
]
|
|
208
210
|
],
|
|
@@ -530,7 +532,7 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
|
|
|
530
532
|
{
|
|
531
533
|
"temperature": "tempSensor.sensors[?id == `0`]|[0]|value",
|
|
532
534
|
"trend": "tempSensor.sensors[?id == `0`]|[0]|trend",
|
|
533
|
-
"state": "tempSensor.sensors[?id == `0`]|[0]|state",
|
|
535
|
+
"temperature.state": "tempSensor.sensors[?id == `0`]|[0]|state",
|
|
534
536
|
"elapsed": "tempSensor.sensors[?id == `0`]|[0]|elapsedTimeS",
|
|
535
537
|
},
|
|
536
538
|
]
|
|
@@ -682,7 +684,9 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
|
|
|
682
684
|
"multiSensor",
|
|
683
685
|
{
|
|
684
686
|
"temperature": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
|
|
687
|
+
"temperature.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
|
|
685
688
|
"wind": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
|
|
689
|
+
"wind.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
|
|
686
690
|
},
|
|
687
691
|
]
|
|
688
692
|
],
|
|
@@ -704,8 +708,11 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
|
|
|
704
708
|
"multiSensor",
|
|
705
709
|
{
|
|
706
710
|
"temperature": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
|
|
711
|
+
"temperature.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
|
|
707
712
|
"wind": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
|
|
713
|
+
"wind.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
|
|
708
714
|
"humidity": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
|
|
715
|
+
"humidity.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
|
|
709
716
|
},
|
|
710
717
|
]
|
|
711
718
|
],
|
|
@@ -727,9 +734,13 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
|
|
|
727
734
|
"multiSensor",
|
|
728
735
|
{
|
|
729
736
|
"illuminance": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
|
|
737
|
+
"illuminance.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
|
|
730
738
|
"temperature": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
|
|
739
|
+
"temperature.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
|
|
731
740
|
"wind": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'wind']|[0]|value",
|
|
741
|
+
"wind.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'wind']|[0]|state",
|
|
732
742
|
"humidity": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
|
|
743
|
+
"humidity.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
|
|
733
744
|
},
|
|
734
745
|
]
|
|
735
746
|
],
|
|
@@ -751,22 +762,47 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
|
|
|
751
762
|
"multiSensor",
|
|
752
763
|
{
|
|
753
764
|
"frequency": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'frequency']|[0]|value",
|
|
765
|
+
"frequency.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'frequency']|[0]|state",
|
|
754
766
|
"current": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'current']|[0]|value",
|
|
767
|
+
"current.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'current']|[0]|state",
|
|
755
768
|
"voltage": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'voltage']|[0]|value",
|
|
769
|
+
"voltage.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'voltage']|[0]|state",
|
|
756
770
|
"apparentPower": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'apparentPower']|[0]|value",
|
|
771
|
+
"apparentPower.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'apparentPower']|[0]|state",
|
|
757
772
|
"activePower": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'activePower']|[0]|value",
|
|
773
|
+
"activePower.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'activePower']|[0]|state",
|
|
758
774
|
"reactivePower": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reactivePower']|[0]|value",
|
|
775
|
+
"reactivePower.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reactivePower']|[0]|state",
|
|
759
776
|
"reverseActiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reverseActiveEnergy']|[0]|value",
|
|
777
|
+
"reverseActiveEnergy.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reverseActiveEnergy']|[0]|state",
|
|
760
778
|
"forwardActiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'forwardActiveEnergy']|[0]|value",
|
|
779
|
+
"forwardActiveEnergy.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'forwardActiveEnergy']|[0]|state",
|
|
761
780
|
"forwardReactiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'forwardReactiveEnergy']|[0]|value",
|
|
781
|
+
"forwardReactiveEnergy.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'forwardReactiveEnergy']|[0]|state",
|
|
762
782
|
"reverseReactiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reverseReactiveEnergy']|[0]|value",
|
|
783
|
+
"reverseReactiveEnergy.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reverseReactiveEnergy']|[0]|state",
|
|
763
784
|
"illuminance": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'illuminance']|[0]|value",
|
|
785
|
+
"illuminance.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'illuminance']|[0]|state",
|
|
764
786
|
"temperature": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'temperature']|[0]|value",
|
|
787
|
+
"temperature.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'temperature']|[0]|state",
|
|
765
788
|
"wind": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'wind']|[0]|value",
|
|
789
|
+
"wind.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'wind']|[0]|state",
|
|
766
790
|
"humidity": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'humidity']|[0]|value",
|
|
791
|
+
"humidity.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'humidity']|[0]|state",
|
|
767
792
|
"openStatus": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'openStatus']|[0]|value",
|
|
793
|
+
"openStatus.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'openStatus']|[0]|state",
|
|
768
794
|
"co2": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2']|[0]|value",
|
|
795
|
+
"co2.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2']|[0]|state",
|
|
769
796
|
"co2Definition": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2Definition']|[0]|value",
|
|
797
|
+
"co2Definition.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2Definition']|[0]|state",
|
|
798
|
+
"gaugePressure": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'gaugePressure']|[0]|value",
|
|
799
|
+
"gaugePressure.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'gaugePressure']|[0]|state",
|
|
800
|
+
"liquidHeight": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'liquidHeight']|[0]|value",
|
|
801
|
+
"liquidHeight.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'liquidHeight']|[0]|state",
|
|
802
|
+
"fillLevel": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'fillLevel']|[0]|value",
|
|
803
|
+
"fillLevel.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'fillLevel']|[0]|state",
|
|
804
|
+
"volume": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'volume']|[0]|value",
|
|
805
|
+
"volume.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'volume']|[0]|state",
|
|
770
806
|
},
|
|
771
807
|
]
|
|
772
808
|
],
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
1
2
|
from .sensor import Temperature
|
|
2
3
|
from typing import Optional, Any, Union
|
|
3
4
|
from .feature import Feature
|
|
4
5
|
from blebox_uniapi.jfollow import follow
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
class BleboxThermoState(IntEnum):
|
|
9
|
+
"""Possible states of a thermo regulator, as reported by the device."""
|
|
10
|
+
|
|
11
|
+
OFF = 0
|
|
12
|
+
ON = 1
|
|
13
|
+
BOOST = 3
|
|
14
|
+
SAFETY_OFF = 4
|
|
15
|
+
|
|
16
|
+
|
|
7
17
|
class Climate(Temperature):
|
|
8
18
|
_is_on: Optional[bool]
|
|
9
19
|
_desired: Union[float, int, None]
|
|
@@ -90,12 +100,18 @@ class Climate(Temperature):
|
|
|
90
100
|
raw = self.raw_value("state")
|
|
91
101
|
if raw is not None: # no reading
|
|
92
102
|
alias = self._alias
|
|
93
|
-
return product.expect_int(alias, raw,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
103
|
+
return product.expect_int(alias, raw, max(BleboxThermoState), 0) in (
|
|
104
|
+
BleboxThermoState.ON,
|
|
105
|
+
BleboxThermoState.BOOST,
|
|
106
|
+
)
|
|
97
107
|
return None
|
|
98
108
|
|
|
109
|
+
def _read_safety_off(self) -> bool:
|
|
110
|
+
"""Return True when the safety system forced the output off."""
|
|
111
|
+
if self._product.last_data is None:
|
|
112
|
+
return False
|
|
113
|
+
return self.raw_value("state") == BleboxThermoState.SAFETY_OFF
|
|
114
|
+
|
|
99
115
|
def _read_operating_state(self) -> Optional[int]:
|
|
100
116
|
"""Return current operating state"""
|
|
101
117
|
if self._product.last_data is not None:
|
|
@@ -107,13 +123,13 @@ class Climate(Temperature):
|
|
|
107
123
|
return None
|
|
108
124
|
|
|
109
125
|
def _read_is_heating(self) -> Optional[bool]:
|
|
110
|
-
if not self._product.last_data:
|
|
126
|
+
if not self._product.last_data or self.current is None or self.desired is None:
|
|
111
127
|
return None
|
|
112
128
|
|
|
113
129
|
return self.is_on and (self.current < self.desired)
|
|
114
130
|
|
|
115
131
|
def _read_is_cooling(self) -> Optional[bool]:
|
|
116
|
-
if not self._product.last_data:
|
|
132
|
+
if not self._product.last_data or self.current is None or self.desired is None:
|
|
117
133
|
return None
|
|
118
134
|
|
|
119
135
|
return self.is_on and (self.current > self.desired)
|
|
@@ -131,7 +147,22 @@ class Climate(Temperature):
|
|
|
131
147
|
def after_update(self) -> None:
|
|
132
148
|
self._is_on = self._read_is_on()
|
|
133
149
|
self._desired = self._read_temperature("desired")
|
|
134
|
-
|
|
150
|
+
|
|
151
|
+
state = self._read_state("temperature")
|
|
152
|
+
if self._state_is_error(state):
|
|
153
|
+
self._error = True
|
|
154
|
+
self._current = None
|
|
155
|
+
else:
|
|
156
|
+
self._error = False
|
|
157
|
+
if self._state_is_initializing(state):
|
|
158
|
+
self._current = None
|
|
159
|
+
else:
|
|
160
|
+
self._current = self._read_temperature("temperature")
|
|
161
|
+
|
|
162
|
+
if self._read_safety_off():
|
|
163
|
+
self._error = True
|
|
164
|
+
|
|
165
|
+
self._native_value = self._current
|
|
135
166
|
self._is_heating = self._read_is_heating()
|
|
136
167
|
self._is_cooling = self._read_is_cooling()
|
|
137
168
|
self._havc_action = self._read_operating_state()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
-
import
|
|
2
|
+
from enum import IntEnum
|
|
3
3
|
from functools import partial
|
|
4
4
|
|
|
5
5
|
from .feature import Feature
|
|
@@ -67,10 +67,9 @@ class SensorFactory:
|
|
|
67
67
|
# note: methods for sensor readings are provided as template
|
|
68
68
|
# functions (lambdas) in the box config. We need to "materialize"
|
|
69
69
|
# them to make sure they are properly indexed by sensor ID
|
|
70
|
-
materialized_methods =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
70
|
+
materialized_methods = Feature.resolve_access_method_paths(
|
|
71
|
+
methods, sensor_id
|
|
72
|
+
)
|
|
74
73
|
|
|
75
74
|
feature = constructor(
|
|
76
75
|
product=product,
|
|
@@ -97,12 +96,26 @@ class SensorFactory:
|
|
|
97
96
|
return []
|
|
98
97
|
|
|
99
98
|
|
|
99
|
+
class BleboxSensorState(IntEnum):
|
|
100
|
+
"""Possible states of a sensor reading, not all valid for every device type."""
|
|
101
|
+
|
|
102
|
+
IDLE = 0
|
|
103
|
+
INITIALIZING = 1
|
|
104
|
+
ACTIVE = 2
|
|
105
|
+
ERROR = 3
|
|
106
|
+
ABOVE_RANGE = 4
|
|
107
|
+
BELOW_RANGE = 5
|
|
108
|
+
NOT_CONFIGURED = 6
|
|
109
|
+
|
|
110
|
+
|
|
100
111
|
class BaseSensor(Feature):
|
|
101
112
|
_unit: str
|
|
102
113
|
_device_class: str
|
|
103
|
-
_native_value: Union[float, int, str]
|
|
114
|
+
_native_value: Optional[Union[float, int, str]] = None
|
|
104
115
|
_sensor_type: Optional[str]
|
|
105
116
|
_sensor_id: Optional[int]
|
|
117
|
+
_error: bool = False
|
|
118
|
+
_needs_configuration: bool = False
|
|
106
119
|
|
|
107
120
|
def __init__(
|
|
108
121
|
self,
|
|
@@ -130,6 +143,31 @@ class BaseSensor(Feature):
|
|
|
130
143
|
def native_value(self):
|
|
131
144
|
return self._native_value
|
|
132
145
|
|
|
146
|
+
@property
|
|
147
|
+
def is_error(self) -> bool:
|
|
148
|
+
return self._error
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def needs_configuration(self) -> bool:
|
|
152
|
+
return self._needs_configuration
|
|
153
|
+
|
|
154
|
+
def _read_state(self, name: str) -> Optional[int]:
|
|
155
|
+
if self._product.last_data is None:
|
|
156
|
+
return None
|
|
157
|
+
raw = self.raw_value(f"{name}.state")
|
|
158
|
+
state = int(raw) if isinstance(raw, (int, float)) else None
|
|
159
|
+
self._needs_configuration = state == BleboxSensorState.NOT_CONFIGURED
|
|
160
|
+
return state
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _state_is_error(state: Optional[int]) -> bool:
|
|
164
|
+
# note: unconfigured probe reports garbage values
|
|
165
|
+
return state in (BleboxSensorState.ERROR, BleboxSensorState.NOT_CONFIGURED)
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _state_is_initializing(state: Optional[int]) -> bool:
|
|
169
|
+
return state == BleboxSensorState.INITIALIZING
|
|
170
|
+
|
|
133
171
|
@property
|
|
134
172
|
def sensor_id(self):
|
|
135
173
|
return self._sensor_id
|
|
@@ -170,6 +208,10 @@ class BaseSensor(Feature):
|
|
|
170
208
|
@SensorFactory.register("openStatus", unit="")
|
|
171
209
|
@SensorFactory.register("co2", unit="ppm")
|
|
172
210
|
@SensorFactory.register("co2Definition", unit="")
|
|
211
|
+
@SensorFactory.register("gaugePressure", unit="mbar")
|
|
212
|
+
@SensorFactory.register("liquidHeight", unit="cm")
|
|
213
|
+
@SensorFactory.register("fillLevel", unit="percentage", scale=10)
|
|
214
|
+
@SensorFactory.register("volume", unit="L")
|
|
173
215
|
class GenericSensor(BaseSensor):
|
|
174
216
|
def __init__(
|
|
175
217
|
# base sensor params
|
|
@@ -201,9 +243,22 @@ class GenericSensor(BaseSensor):
|
|
|
201
243
|
if product.last_data is None:
|
|
202
244
|
return
|
|
203
245
|
|
|
246
|
+
state = self._read_state(self._device_class)
|
|
247
|
+
if self._state_is_error(state):
|
|
248
|
+
self._error = True
|
|
249
|
+
self._native_value = None
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
self._error = False
|
|
253
|
+
|
|
254
|
+
if self._state_is_initializing(state):
|
|
255
|
+
self._native_value = None
|
|
256
|
+
return
|
|
257
|
+
|
|
204
258
|
raw = self.raw_value(self._device_class)
|
|
205
|
-
if not isinstance(raw,
|
|
206
|
-
|
|
259
|
+
if not isinstance(raw, (int, float)):
|
|
260
|
+
self._native_value = None
|
|
261
|
+
return
|
|
207
262
|
|
|
208
263
|
native = raw / self._scale
|
|
209
264
|
if self._precision:
|
|
@@ -234,7 +289,7 @@ class PowerConsumption(GenericSensor):
|
|
|
234
289
|
|
|
235
290
|
@SensorFactory.register("temperature")
|
|
236
291
|
class Temperature(BaseSensor):
|
|
237
|
-
_current: Union[float, int
|
|
292
|
+
_current: Optional[Union[float, int]]
|
|
238
293
|
|
|
239
294
|
def __init__(
|
|
240
295
|
self,
|
|
@@ -249,21 +304,35 @@ class Temperature(BaseSensor):
|
|
|
249
304
|
self._device_class = "temperature"
|
|
250
305
|
|
|
251
306
|
@property
|
|
252
|
-
def current(self) -> Union[float, int
|
|
307
|
+
def current(self) -> Optional[Union[float, int]]:
|
|
253
308
|
return self._current
|
|
254
309
|
|
|
255
|
-
def _read_temperature(self, field: str) -> Union[float, int
|
|
310
|
+
def _read_temperature(self, field: str) -> Optional[Union[float, int]]:
|
|
256
311
|
product = self._product
|
|
257
312
|
if product.last_data is not None:
|
|
258
313
|
raw = self.raw_value(field)
|
|
259
|
-
if raw
|
|
260
|
-
|
|
261
|
-
return round(product.expect_int(alias, raw, 12500, -5500) / 100.0, 1)
|
|
314
|
+
if isinstance(raw, (int, float)):
|
|
315
|
+
return round(raw / 100.0, 1)
|
|
262
316
|
return None
|
|
263
317
|
|
|
264
318
|
def after_update(self) -> None:
|
|
265
|
-
|
|
266
|
-
|
|
319
|
+
state = self._read_state("temperature")
|
|
320
|
+
if self._state_is_error(state):
|
|
321
|
+
self._error = True
|
|
322
|
+
self._current = None
|
|
323
|
+
self._native_value = None
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
self._error = False
|
|
327
|
+
|
|
328
|
+
if self._state_is_initializing(state):
|
|
329
|
+
self._current = None
|
|
330
|
+
self._native_value = None
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
current = self._read_temperature("temperature")
|
|
334
|
+
self._current = current
|
|
335
|
+
self._native_value = current
|
|
267
336
|
|
|
268
337
|
|
|
269
338
|
@SensorFactory.register("airSensor")
|
|
@@ -282,14 +351,25 @@ class AirQuality(BaseSensor):
|
|
|
282
351
|
self._unit = "concentration_of_mp"
|
|
283
352
|
self._device_class = alias
|
|
284
353
|
|
|
285
|
-
def _pm_value(self, name: str) -> Optional[int]:
|
|
354
|
+
def _pm_value(self, name: str) -> Optional[Union[int, float]]:
|
|
286
355
|
product = self._product
|
|
287
356
|
if product.last_data is not None:
|
|
288
357
|
raw = self.raw_value(name)
|
|
289
|
-
if raw
|
|
290
|
-
|
|
291
|
-
return product.expect_int(alias, raw, 3000, 0)
|
|
358
|
+
if isinstance(raw, (int, float)):
|
|
359
|
+
return raw
|
|
292
360
|
return None
|
|
293
361
|
|
|
294
362
|
def after_update(self) -> None:
|
|
363
|
+
state = self._read_state(self.device_class)
|
|
364
|
+
if self._state_is_error(state):
|
|
365
|
+
self._error = True
|
|
366
|
+
self._native_value = None
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
self._error = False
|
|
370
|
+
|
|
371
|
+
if self._state_is_initializing(state):
|
|
372
|
+
self._native_value = None
|
|
373
|
+
return
|
|
374
|
+
|
|
295
375
|
self._native_value = self._pm_value(f"{self.device_class}.value")
|
|
@@ -234,3 +234,21 @@ async def test_async_ota_check_returns_silently_when_no_available_fv(
|
|
|
234
234
|
with patch("asyncio.sleep", new=AsyncMock()):
|
|
235
235
|
await box.async_ota_check()
|
|
236
236
|
assert box.available_firmware_version is None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def test_async_ota_check_falls_back_to_info(mock_session, sample_data, config):
|
|
240
|
+
box = Box(mock_session, sample_data, config, None)
|
|
241
|
+
mock_session.async_api_get_ota = AsyncMock(return_value=None)
|
|
242
|
+
mock_session.async_api_get = AsyncMock(
|
|
243
|
+
side_effect=[
|
|
244
|
+
error.HttpError("Request to /api/device/state failed with HTTP 404"),
|
|
245
|
+
{"device": {"availableFv": "2.0", "fv": "1.5"}},
|
|
246
|
+
]
|
|
247
|
+
)
|
|
248
|
+
with patch("asyncio.sleep", new=AsyncMock()):
|
|
249
|
+
await box.async_ota_check()
|
|
250
|
+
mock_session.async_api_get.assert_has_calls(
|
|
251
|
+
[mock.call("/api/device/state"), mock.call("/info")]
|
|
252
|
+
)
|
|
253
|
+
assert box.available_firmware_version == "2.0"
|
|
254
|
+
assert box.firmware_version == "1.5"
|
|
@@ -39,7 +39,6 @@ class BleBoxClimateEntity(CommonEntity, ClimateDevice):
|
|
|
39
39
|
def __init__(self, feature):
|
|
40
40
|
super().__init__(feature)
|
|
41
41
|
ClimateDevice.__init__(self)
|
|
42
|
-
pass
|
|
43
42
|
|
|
44
43
|
"""Representation of a BleBox climate feature."""
|
|
45
44
|
|
|
@@ -294,6 +293,45 @@ class TestSauna(DefaultBoxTest):
|
|
|
294
293
|
entity = (await self.async_entities(aioclient_mock))[0]
|
|
295
294
|
assert entity.device_info["name"] == "My ThermoBox"
|
|
296
295
|
|
|
296
|
+
async def test_thermo_sensor_error_state(self, aioclient_mock):
|
|
297
|
+
"""A broken probe (state=3, out-of-range value) must not prevent setup."""
|
|
298
|
+
self.DEVICE_INFO = self.DEVICE_INFO_THERMO
|
|
299
|
+
self.DEVICE_EXTENDED_INFO = jmerge(
|
|
300
|
+
self.DEVICE_EXTENDED_INFO_THERMO,
|
|
301
|
+
'{ "sensors": [ { "value": 18349, "state": 3 } ] }',
|
|
302
|
+
)
|
|
303
|
+
await self.allow_get_info(aioclient_mock)
|
|
304
|
+
entity = (await self.async_entities(aioclient_mock))[0]
|
|
305
|
+
|
|
306
|
+
assert entity.current_temperature is None
|
|
307
|
+
assert entity._feature.is_error is True
|
|
308
|
+
|
|
309
|
+
async def test_thermo_sensor_initializing_state(self, aioclient_mock):
|
|
310
|
+
"""state=1 (measurement in progress) must not surface a value or an error."""
|
|
311
|
+
self.DEVICE_INFO = self.DEVICE_INFO_THERMO
|
|
312
|
+
self.DEVICE_EXTENDED_INFO = jmerge(
|
|
313
|
+
self.DEVICE_EXTENDED_INFO_THERMO,
|
|
314
|
+
'{ "sensors": [ { "value": 18349, "state": 1 } ] }',
|
|
315
|
+
)
|
|
316
|
+
await self.allow_get_info(aioclient_mock)
|
|
317
|
+
entity = (await self.async_entities(aioclient_mock))[0]
|
|
318
|
+
|
|
319
|
+
assert entity.current_temperature is None
|
|
320
|
+
assert entity._feature.is_error is False
|
|
321
|
+
|
|
322
|
+
async def test_thermo_safety_off_state(self, aioclient_mock):
|
|
323
|
+
"""state=4 (output blocked by safety system) must not prevent setup."""
|
|
324
|
+
self.DEVICE_INFO = self.DEVICE_INFO_THERMO
|
|
325
|
+
self.DEVICE_EXTENDED_INFO = jmerge(
|
|
326
|
+
self.DEVICE_EXTENDED_INFO_THERMO,
|
|
327
|
+
'{ "thermo": { "state": 4 } }',
|
|
328
|
+
)
|
|
329
|
+
await self.allow_get_info(aioclient_mock)
|
|
330
|
+
entity = (await self.async_entities(aioclient_mock))[0]
|
|
331
|
+
|
|
332
|
+
assert entity._feature.is_on is False
|
|
333
|
+
assert entity._feature.is_error is True
|
|
334
|
+
|
|
297
335
|
async def test_device_info(self, aioclient_mock):
|
|
298
336
|
await self.allow_get_info(aioclient_mock, self.DEVICE_INFO)
|
|
299
337
|
entity = (await self.async_entities(aioclient_mock))[0]
|
|
@@ -283,6 +283,73 @@ class TestTempSensor(DefaultBoxTest):
|
|
|
283
283
|
|
|
284
284
|
assert entity.native_value == 1.2
|
|
285
285
|
|
|
286
|
+
STATE_SENSOR_ERROR = json.loads(
|
|
287
|
+
"""
|
|
288
|
+
{
|
|
289
|
+
"tempSensor": {
|
|
290
|
+
"sensors": [
|
|
291
|
+
{
|
|
292
|
+
"type": "temperature",
|
|
293
|
+
"id": 0,
|
|
294
|
+
"value": 18349,
|
|
295
|
+
"trend": 3,
|
|
296
|
+
"state": 3,
|
|
297
|
+
"elapsedTimeS": 0
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
"""
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
async def test_update_with_sensor_error_state_ignores_out_of_range_value(
|
|
306
|
+
self, aioclient_mock
|
|
307
|
+
):
|
|
308
|
+
"""Regression test: a broken probe must not prevent device setup."""
|
|
309
|
+
|
|
310
|
+
entity = await self.updated(aioclient_mock, self.STATE_SENSOR_ERROR)
|
|
311
|
+
|
|
312
|
+
assert entity.native_value is None
|
|
313
|
+
assert entity._feature.is_error is True
|
|
314
|
+
|
|
315
|
+
async def test_valid_state_trusts_out_of_nominal_range_value(self, aioclient_mock):
|
|
316
|
+
"""state == 2 means the value is valid, even outside the nominal range."""
|
|
317
|
+
|
|
318
|
+
state = jmerge(
|
|
319
|
+
self.STATE_DEFAULT,
|
|
320
|
+
'{ "tempSensor": { "sensors": [ { "value": 18349, "state": 2 } ] } }',
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
entity = await self.updated(aioclient_mock, state)
|
|
324
|
+
|
|
325
|
+
assert entity._feature.is_error is False
|
|
326
|
+
assert entity.native_value == 183.5
|
|
327
|
+
|
|
328
|
+
async def test_initializing_state_reports_unknown_not_error(self, aioclient_mock):
|
|
329
|
+
"""state == 1 (measurement in progress) must not surface a value or an error."""
|
|
330
|
+
|
|
331
|
+
state = jmerge(
|
|
332
|
+
self.STATE_DEFAULT,
|
|
333
|
+
'{ "tempSensor": { "sensors": [ { "value": 18349, "state": 1 } ] } }',
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
entity = await self.updated(aioclient_mock, state)
|
|
337
|
+
|
|
338
|
+
assert entity._feature.is_error is False
|
|
339
|
+
assert entity.native_value is None
|
|
340
|
+
|
|
341
|
+
async def test_update_recovers_after_sensor_error_clears(self, aioclient_mock):
|
|
342
|
+
"""Entity value is restored once the sensor reports a valid state again."""
|
|
343
|
+
|
|
344
|
+
entity = await self.updated(aioclient_mock, self.STATE_SENSOR_ERROR)
|
|
345
|
+
assert entity._feature.is_error is True
|
|
346
|
+
|
|
347
|
+
product = entity._feature.product
|
|
348
|
+
product._update_last_data(self.STATE_DEFAULT)
|
|
349
|
+
|
|
350
|
+
assert entity._feature.is_error is False
|
|
351
|
+
assert entity.native_value == 25.2
|
|
352
|
+
|
|
286
353
|
|
|
287
354
|
class TestMultiSensorEnergy(DefaultBoxTest):
|
|
288
355
|
"""Tests for multiSensor energy/current sensors (apiLevel 20230606)."""
|
|
@@ -351,6 +418,146 @@ class TestMultiSensorEnergy(DefaultBoxTest):
|
|
|
351
418
|
assert entity._feature.unit == unit
|
|
352
419
|
assert entity.native_value == value
|
|
353
420
|
|
|
421
|
+
async def test_sensor_error_state_reports_error(self, aioclient_mock):
|
|
422
|
+
"""state == 3 is the only value that marks the sensor as errored."""
|
|
423
|
+
|
|
424
|
+
state_json = jmerge(
|
|
425
|
+
self.STATE_DEFAULT,
|
|
426
|
+
'{ "multiSensor": { "sensors": [ { "state": 3 } ] } }',
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
entity = await self.updated(aioclient_mock, state_json, index=0)
|
|
430
|
+
|
|
431
|
+
assert entity.native_value is None
|
|
432
|
+
assert entity._feature.is_error is True
|
|
433
|
+
|
|
434
|
+
async def test_sensor_initializing_state_reports_unknown_not_error(
|
|
435
|
+
self, aioclient_mock
|
|
436
|
+
):
|
|
437
|
+
"""state == 1 (measurement in progress) must not surface a value or an error."""
|
|
438
|
+
|
|
439
|
+
state_json = jmerge(
|
|
440
|
+
self.STATE_DEFAULT,
|
|
441
|
+
'{ "multiSensor": { "sensors": [ { "state": 1 } ] } }',
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
entity = await self.updated(aioclient_mock, state_json, index=0)
|
|
445
|
+
|
|
446
|
+
assert entity._feature.is_error is False
|
|
447
|
+
assert entity.native_value is None
|
|
448
|
+
|
|
449
|
+
@pytest.mark.parametrize("state", [0, 2, 4, 5])
|
|
450
|
+
async def test_sensor_valid_state_reports_value(self, aioclient_mock, state):
|
|
451
|
+
"""States other than 1 (init) and 3 (error) trust the reported value,
|
|
452
|
+
including 4/5 (out of range), where value is the measurement limit."""
|
|
453
|
+
|
|
454
|
+
state_json = jmerge(
|
|
455
|
+
self.STATE_DEFAULT,
|
|
456
|
+
f'{{ "multiSensor": {{ "sensors": [ {{ "state": {state} }} ] }} }}',
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
entity = await self.updated(aioclient_mock, state_json, index=0)
|
|
460
|
+
|
|
461
|
+
assert entity._feature.is_error is False
|
|
462
|
+
assert entity.native_value == 1500
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class TestMultiSensorTank(DefaultBoxTest):
|
|
466
|
+
"""Tests for multiSensor tank level sensors (apiLevel 20230606)."""
|
|
467
|
+
|
|
468
|
+
DEVCLASS = "sensors"
|
|
469
|
+
ENTITY_CLASS = BleBoxSensorEntity
|
|
470
|
+
|
|
471
|
+
DEV_INFO_PATH = "state"
|
|
472
|
+
|
|
473
|
+
DEVICE_INFO = json.loads(
|
|
474
|
+
"""
|
|
475
|
+
{
|
|
476
|
+
"device": {
|
|
477
|
+
"deviceName": "My tankSensor",
|
|
478
|
+
"type": "multiSensor",
|
|
479
|
+
"product": "tankSensor",
|
|
480
|
+
"hv": "tS-1.0",
|
|
481
|
+
"fv": "0.1",
|
|
482
|
+
"universe": 0,
|
|
483
|
+
"apiLevel": "20230606",
|
|
484
|
+
"id": "aa00112233bb",
|
|
485
|
+
"ip": "172.0.0.1",
|
|
486
|
+
"availableFv": null
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
"""
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def patch_version(apiLevel):
|
|
493
|
+
return f'{{ "device": {{ "apiLevel": {apiLevel} }} }}'
|
|
494
|
+
|
|
495
|
+
DEVICE_INFO_FUTURE = jmerge(DEVICE_INFO, patch_version(future_date()))
|
|
496
|
+
DEVICE_INFO_LATEST = jmerge(
|
|
497
|
+
DEVICE_INFO, patch_version(get_latest_api_level("multiSensor"))
|
|
498
|
+
)
|
|
499
|
+
DEVICE_INFO_UNSUPPORTED = jmerge(DEVICE_INFO, patch_version(20180603))
|
|
500
|
+
DEVICE_INFO_UNSPECIFIED_API = None
|
|
501
|
+
|
|
502
|
+
STATE_DEFAULT = json.loads(
|
|
503
|
+
"""
|
|
504
|
+
{
|
|
505
|
+
"multiSensor": {
|
|
506
|
+
"sensors": [
|
|
507
|
+
{"type": "gaugePressure", "id": 0, "value": 98, "state": 2},
|
|
508
|
+
{"type": "liquidHeight", "id": 1, "value": 100, "state": 2},
|
|
509
|
+
{"type": "fillLevel", "id": 2, "value": 205, "state": 2},
|
|
510
|
+
{"type": "volume", "id": 3, "value": 2000, "state": 2}
|
|
511
|
+
]
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
"""
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
DEVICE_EXTENDED_INFO = STATE_DEFAULT
|
|
518
|
+
DEVICE_EXTENDED_INFO_PATH = "/state/extended"
|
|
519
|
+
|
|
520
|
+
@pytest.mark.parametrize(
|
|
521
|
+
"index,unit,value",
|
|
522
|
+
[
|
|
523
|
+
(0, "mbar", 98),
|
|
524
|
+
(1, "cm", 100),
|
|
525
|
+
(2, "percentage", 20.5),
|
|
526
|
+
(3, "L", 2000),
|
|
527
|
+
],
|
|
528
|
+
)
|
|
529
|
+
async def test_sensor_value(self, aioclient_mock, index, unit, value):
|
|
530
|
+
entity = await self.updated(aioclient_mock, self.STATE_DEFAULT, index=index)
|
|
531
|
+
assert entity._feature.unit == unit
|
|
532
|
+
assert entity.native_value == value
|
|
533
|
+
assert entity._feature.needs_configuration is False
|
|
534
|
+
|
|
535
|
+
@pytest.mark.parametrize("value", [None, 9999])
|
|
536
|
+
async def test_not_configured_state_reports_error(self, aioclient_mock, value):
|
|
537
|
+
"""state == 6 (probe not configured) carries a meaningless or null value."""
|
|
538
|
+
|
|
539
|
+
state_json = jmerge(
|
|
540
|
+
self.STATE_DEFAULT,
|
|
541
|
+
json.dumps({"multiSensor": {"sensors": [{"state": 6, "value": value}]}}),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
entity = await self.updated(aioclient_mock, state_json, index=0)
|
|
545
|
+
|
|
546
|
+
assert entity.native_value is None
|
|
547
|
+
assert entity._feature.is_error is True
|
|
548
|
+
assert entity._feature.needs_configuration is True
|
|
549
|
+
|
|
550
|
+
async def test_error_state_does_not_need_configuration(self, aioclient_mock):
|
|
551
|
+
state_json = jmerge(
|
|
552
|
+
self.STATE_DEFAULT,
|
|
553
|
+
'{ "multiSensor": { "sensors": [ { "state": 3 } ] } }',
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
entity = await self.updated(aioclient_mock, state_json, index=0)
|
|
557
|
+
|
|
558
|
+
assert entity._feature.is_error is True
|
|
559
|
+
assert entity._feature.needs_configuration is False
|
|
560
|
+
|
|
354
561
|
|
|
355
562
|
class TestAirSensor(DefaultBoxTest):
|
|
356
563
|
"""Tests for sensors representing BleBox airSensor."""
|
|
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
|
|
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
|