blebox-uniapi 2.5.6__tar.gz → 2.5.7__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.7}/PKG-INFO +1 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/__init__.py +1 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/box_types.py +29 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/climate.py +38 -7
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/sensor.py +89 -20
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/PKG-INFO +1 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/setup.cfg +1 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/setup.py +1 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_climate.py +39 -1
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_sensor.py +110 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/CONTRIBUTING.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/HISTORY.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/LICENSE +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/MANIFEST.in +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/README.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/binary_sensor.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/box.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/button.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/cover.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/error.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/feature.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/jfollow.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/light.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/session.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/switch.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/update.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/SOURCES.txt +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/not-zip-safe +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/requires.txt +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/top_level.txt +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/Makefile +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/authors.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/conf.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/contributing.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/history.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/index.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/installation.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/make.bat +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/readme.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/usage.rst +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_box.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_box_types.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_button.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_cover.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_light.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_light_unit.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_session.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_switch.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_unknown.py +0 -0
- {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_update.py +0 -0
|
@@ -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,39 @@ 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",
|
|
770
798
|
},
|
|
771
799
|
]
|
|
772
800
|
],
|
|
@@ -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,24 @@ 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
|
+
|
|
109
|
+
|
|
100
110
|
class BaseSensor(Feature):
|
|
101
111
|
_unit: str
|
|
102
112
|
_device_class: str
|
|
103
|
-
_native_value: Union[float, int, str]
|
|
113
|
+
_native_value: Optional[Union[float, int, str]] = None
|
|
104
114
|
_sensor_type: Optional[str]
|
|
105
115
|
_sensor_id: Optional[int]
|
|
116
|
+
_error: bool = False
|
|
106
117
|
|
|
107
118
|
def __init__(
|
|
108
119
|
self,
|
|
@@ -130,6 +141,26 @@ class BaseSensor(Feature):
|
|
|
130
141
|
def native_value(self):
|
|
131
142
|
return self._native_value
|
|
132
143
|
|
|
144
|
+
@property
|
|
145
|
+
def is_error(self) -> bool:
|
|
146
|
+
return self._error
|
|
147
|
+
|
|
148
|
+
def _read_state(self, name: str) -> Optional[int]:
|
|
149
|
+
if self._product.last_data is None:
|
|
150
|
+
return None
|
|
151
|
+
raw = self.raw_value(f"{name}.state")
|
|
152
|
+
if not isinstance(raw, (int, float)):
|
|
153
|
+
return None
|
|
154
|
+
return int(raw)
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _state_is_error(state: Optional[int]) -> bool:
|
|
158
|
+
return state == BleboxSensorState.ERROR
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _state_is_initializing(state: Optional[int]) -> bool:
|
|
162
|
+
return state == BleboxSensorState.INITIALIZING
|
|
163
|
+
|
|
133
164
|
@property
|
|
134
165
|
def sensor_id(self):
|
|
135
166
|
return self._sensor_id
|
|
@@ -201,9 +232,22 @@ class GenericSensor(BaseSensor):
|
|
|
201
232
|
if product.last_data is None:
|
|
202
233
|
return
|
|
203
234
|
|
|
235
|
+
state = self._read_state(self._device_class)
|
|
236
|
+
if self._state_is_error(state):
|
|
237
|
+
self._error = True
|
|
238
|
+
self._native_value = None
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
self._error = False
|
|
242
|
+
|
|
243
|
+
if self._state_is_initializing(state):
|
|
244
|
+
self._native_value = None
|
|
245
|
+
return
|
|
246
|
+
|
|
204
247
|
raw = self.raw_value(self._device_class)
|
|
205
|
-
if not isinstance(raw,
|
|
206
|
-
|
|
248
|
+
if not isinstance(raw, (int, float)):
|
|
249
|
+
self._native_value = None
|
|
250
|
+
return
|
|
207
251
|
|
|
208
252
|
native = raw / self._scale
|
|
209
253
|
if self._precision:
|
|
@@ -234,7 +278,7 @@ class PowerConsumption(GenericSensor):
|
|
|
234
278
|
|
|
235
279
|
@SensorFactory.register("temperature")
|
|
236
280
|
class Temperature(BaseSensor):
|
|
237
|
-
_current: Union[float, int
|
|
281
|
+
_current: Optional[Union[float, int]]
|
|
238
282
|
|
|
239
283
|
def __init__(
|
|
240
284
|
self,
|
|
@@ -249,21 +293,35 @@ class Temperature(BaseSensor):
|
|
|
249
293
|
self._device_class = "temperature"
|
|
250
294
|
|
|
251
295
|
@property
|
|
252
|
-
def current(self) -> Union[float, int
|
|
296
|
+
def current(self) -> Optional[Union[float, int]]:
|
|
253
297
|
return self._current
|
|
254
298
|
|
|
255
|
-
def _read_temperature(self, field: str) -> Union[float, int
|
|
299
|
+
def _read_temperature(self, field: str) -> Optional[Union[float, int]]:
|
|
256
300
|
product = self._product
|
|
257
301
|
if product.last_data is not None:
|
|
258
302
|
raw = self.raw_value(field)
|
|
259
|
-
if raw
|
|
260
|
-
|
|
261
|
-
return round(product.expect_int(alias, raw, 12500, -5500) / 100.0, 1)
|
|
303
|
+
if isinstance(raw, (int, float)):
|
|
304
|
+
return round(raw / 100.0, 1)
|
|
262
305
|
return None
|
|
263
306
|
|
|
264
307
|
def after_update(self) -> None:
|
|
265
|
-
|
|
266
|
-
|
|
308
|
+
state = self._read_state("temperature")
|
|
309
|
+
if self._state_is_error(state):
|
|
310
|
+
self._error = True
|
|
311
|
+
self._current = None
|
|
312
|
+
self._native_value = None
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
self._error = False
|
|
316
|
+
|
|
317
|
+
if self._state_is_initializing(state):
|
|
318
|
+
self._current = None
|
|
319
|
+
self._native_value = None
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
current = self._read_temperature("temperature")
|
|
323
|
+
self._current = current
|
|
324
|
+
self._native_value = current
|
|
267
325
|
|
|
268
326
|
|
|
269
327
|
@SensorFactory.register("airSensor")
|
|
@@ -282,14 +340,25 @@ class AirQuality(BaseSensor):
|
|
|
282
340
|
self._unit = "concentration_of_mp"
|
|
283
341
|
self._device_class = alias
|
|
284
342
|
|
|
285
|
-
def _pm_value(self, name: str) -> Optional[int]:
|
|
343
|
+
def _pm_value(self, name: str) -> Optional[Union[int, float]]:
|
|
286
344
|
product = self._product
|
|
287
345
|
if product.last_data is not None:
|
|
288
346
|
raw = self.raw_value(name)
|
|
289
|
-
if raw
|
|
290
|
-
|
|
291
|
-
return product.expect_int(alias, raw, 3000, 0)
|
|
347
|
+
if isinstance(raw, (int, float)):
|
|
348
|
+
return raw
|
|
292
349
|
return None
|
|
293
350
|
|
|
294
351
|
def after_update(self) -> None:
|
|
352
|
+
state = self._read_state(self.device_class)
|
|
353
|
+
if self._state_is_error(state):
|
|
354
|
+
self._error = True
|
|
355
|
+
self._native_value = None
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
self._error = False
|
|
359
|
+
|
|
360
|
+
if self._state_is_initializing(state):
|
|
361
|
+
self._native_value = None
|
|
362
|
+
return
|
|
363
|
+
|
|
295
364
|
self._native_value = self._pm_value(f"{self.device_class}.value")
|
|
@@ -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,49 @@ 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
|
+
|
|
354
464
|
|
|
355
465
|
class TestAirSensor(DefaultBoxTest):
|
|
356
466
|
"""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
|
|
File without changes
|
|
File without changes
|