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.
Files changed (51) hide show
  1. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/PKG-INFO +1 -1
  2. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/__init__.py +1 -1
  3. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/box_types.py +29 -1
  4. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/climate.py +38 -7
  5. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/sensor.py +89 -20
  6. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/PKG-INFO +1 -1
  7. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/setup.cfg +1 -1
  8. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/setup.py +1 -1
  9. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_climate.py +39 -1
  10. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_sensor.py +110 -0
  11. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/CONTRIBUTING.rst +0 -0
  12. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/HISTORY.rst +0 -0
  13. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/LICENSE +0 -0
  14. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/MANIFEST.in +0 -0
  15. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/README.rst +0 -0
  16. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/binary_sensor.py +0 -0
  17. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/box.py +0 -0
  18. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/button.py +0 -0
  19. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/cover.py +0 -0
  20. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/error.py +0 -0
  21. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/feature.py +0 -0
  22. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/jfollow.py +0 -0
  23. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/light.py +0 -0
  24. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/session.py +0 -0
  25. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/switch.py +0 -0
  26. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi/update.py +0 -0
  27. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/SOURCES.txt +0 -0
  28. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
  29. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/not-zip-safe +0 -0
  30. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/requires.txt +0 -0
  31. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/top_level.txt +0 -0
  32. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/Makefile +0 -0
  33. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/authors.rst +0 -0
  34. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/conf.py +0 -0
  35. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/contributing.rst +0 -0
  36. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/history.rst +0 -0
  37. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/index.rst +0 -0
  38. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/installation.rst +0 -0
  39. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/make.bat +0 -0
  40. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/readme.rst +0 -0
  41. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/docs/usage.rst +0 -0
  42. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_box.py +0 -0
  43. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_box_types.py +0 -0
  44. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_button.py +0 -0
  45. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_cover.py +0 -0
  46. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_light.py +0 -0
  47. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_light_unit.py +0 -0
  48. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_session.py +0 -0
  49. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_switch.py +0 -0
  50. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_unknown.py +0 -0
  51. {blebox_uniapi-2.5.6 → blebox_uniapi-2.5.7}/tests/test_update.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blebox_uniapi
3
- Version: 2.5.6
3
+ Version: 2.5.7
4
4
  Summary: Python API for accessing BleBox smart home devices
5
5
  Home-page: https://github.com/blebox/blebox_uniapi
6
6
  Author: BleBox
@@ -2,4 +2,4 @@
2
2
 
3
3
  __author__ = """BleBox"""
4
4
  __email__ = "opensource@blebox.eu"
5
- __version__ = "2.5.6"
5
+ __version__ = "2.5.7"
@@ -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, 3, 0) in (
94
- 1,
95
- 3,
96
- ) # 1: On, 3: Boost(thermoBox max temp of mode)
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
- self._current = self._read_temperature("temperature")
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 numbers
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
- **methods,
72
- device_class: methods[device_class](sensor_id),
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, numbers.Number):
206
- raw = float("nan")
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, None]
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, None]:
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, None]:
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 is not None:
260
- alias = self._alias
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
- self._current = self._read_temperature("temperature")
266
- self._native_value = self._read_temperature("temperature")
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 is not None:
290
- alias = self._alias
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blebox_uniapi
3
- Version: 2.5.6
3
+ Version: 2.5.7
4
4
  Summary: Python API for accessing BleBox smart home devices
5
5
  Home-page: https://github.com/blebox/blebox_uniapi
6
6
  Author: BleBox
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 2.5.6
2
+ current_version = 2.5.7
3
3
  commit = True
4
4
  tag = True
5
5
  tag_name = v{new_version}
@@ -23,7 +23,7 @@ test_requirements = [
23
23
  ]
24
24
 
25
25
  setup(
26
- version="2.5.6",
26
+ version="2.5.7",
27
27
  author="BleBox",
28
28
  author_email="opensource@blebox.eu",
29
29
  python_requires=">=3.9",
@@ -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