diff --git a/blebox_uniapi/box_types.py b/blebox_uniapi/box_types.py index 305a7b1..2816b3c 100644 --- a/blebox_uniapi/box_types.py +++ b/blebox_uniapi/box_types.py @@ -795,6 +795,14 @@ def get_latest_api_level(product_type: str) -> Union[dict, int]: "co2.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2']|[0]|state", "co2Definition": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2Definition']|[0]|value", "co2Definition.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2Definition']|[0]|state", + "gaugePressure": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'gaugePressure']|[0]|value", + "gaugePressure.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'gaugePressure']|[0]|state", + "liquidHeight": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'liquidHeight']|[0]|value", + "liquidHeight.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'liquidHeight']|[0]|state", + "fillLevel": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'fillLevel']|[0]|value", + "fillLevel.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'fillLevel']|[0]|state", + "volume": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'volume']|[0]|value", + "volume.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'volume']|[0]|state", }, ] ], diff --git a/blebox_uniapi/sensor.py b/blebox_uniapi/sensor.py index 3d9044b..940d657 100644 --- a/blebox_uniapi/sensor.py +++ b/blebox_uniapi/sensor.py @@ -105,6 +105,7 @@ class BleboxSensorState(IntEnum): ERROR = 3 ABOVE_RANGE = 4 BELOW_RANGE = 5 + NOT_CONFIGURED = 6 class BaseSensor(Feature): @@ -114,6 +115,7 @@ class BaseSensor(Feature): _sensor_type: Optional[str] _sensor_id: Optional[int] _error: bool = False + _needs_configuration: bool = False def __init__( self, @@ -145,17 +147,22 @@ def native_value(self): def is_error(self) -> bool: return self._error + @property + def needs_configuration(self) -> bool: + return self._needs_configuration + def _read_state(self, name: str) -> Optional[int]: if self._product.last_data is None: return None raw = self.raw_value(f"{name}.state") - if not isinstance(raw, (int, float)): - return None - return int(raw) + state = int(raw) if isinstance(raw, (int, float)) else None + self._needs_configuration = state == BleboxSensorState.NOT_CONFIGURED + return state @staticmethod def _state_is_error(state: Optional[int]) -> bool: - return state == BleboxSensorState.ERROR + # note: unconfigured probe reports garbage values + return state in (BleboxSensorState.ERROR, BleboxSensorState.NOT_CONFIGURED) @staticmethod def _state_is_initializing(state: Optional[int]) -> bool: @@ -201,6 +208,10 @@ def __str__(self): @SensorFactory.register("openStatus", unit="") @SensorFactory.register("co2", unit="ppm") @SensorFactory.register("co2Definition", unit="") +@SensorFactory.register("gaugePressure", unit="mbar") +@SensorFactory.register("liquidHeight", unit="cm") +@SensorFactory.register("fillLevel", unit="percentage", scale=10) +@SensorFactory.register("volume", unit="L") class GenericSensor(BaseSensor): def __init__( # base sensor params diff --git a/tests/test_sensor.py b/tests/test_sensor.py index a63cab2..d27d208 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -462,6 +462,103 @@ async def test_sensor_valid_state_reports_value(self, aioclient_mock, state): assert entity.native_value == 1500 +class TestMultiSensorTank(DefaultBoxTest): + """Tests for multiSensor tank level sensors (apiLevel 20230606).""" + + DEVCLASS = "sensors" + ENTITY_CLASS = BleBoxSensorEntity + + DEV_INFO_PATH = "state" + + DEVICE_INFO = json.loads( + """ + { + "device": { + "deviceName": "My tankSensor", + "type": "multiSensor", + "product": "tankSensor", + "hv": "tS-1.0", + "fv": "0.1", + "universe": 0, + "apiLevel": "20230606", + "id": "aa00112233bb", + "ip": "172.0.0.1", + "availableFv": null + } + } + """ + ) + + def patch_version(apiLevel): + return f'{{ "device": {{ "apiLevel": {apiLevel} }} }}' + + DEVICE_INFO_FUTURE = jmerge(DEVICE_INFO, patch_version(future_date())) + DEVICE_INFO_LATEST = jmerge( + DEVICE_INFO, patch_version(get_latest_api_level("multiSensor")) + ) + DEVICE_INFO_UNSUPPORTED = jmerge(DEVICE_INFO, patch_version(20180603)) + DEVICE_INFO_UNSPECIFIED_API = None + + STATE_DEFAULT = json.loads( + """ + { + "multiSensor": { + "sensors": [ + {"type": "gaugePressure", "id": 0, "value": 98, "state": 2}, + {"type": "liquidHeight", "id": 1, "value": 100, "state": 2}, + {"type": "fillLevel", "id": 2, "value": 205, "state": 2}, + {"type": "volume", "id": 3, "value": 2000, "state": 2} + ] + } + } + """ + ) + + DEVICE_EXTENDED_INFO = STATE_DEFAULT + DEVICE_EXTENDED_INFO_PATH = "/state/extended" + + @pytest.mark.parametrize( + "index,unit,value", + [ + (0, "mbar", 98), + (1, "cm", 100), + (2, "percentage", 20.5), + (3, "L", 2000), + ], + ) + async def test_sensor_value(self, aioclient_mock, index, unit, value): + entity = await self.updated(aioclient_mock, self.STATE_DEFAULT, index=index) + assert entity._feature.unit == unit + assert entity.native_value == value + assert entity._feature.needs_configuration is False + + @pytest.mark.parametrize("value", [None, 9999]) + async def test_not_configured_state_reports_error(self, aioclient_mock, value): + """state == 6 (probe not configured) carries a meaningless or null value.""" + + state_json = jmerge( + self.STATE_DEFAULT, + json.dumps({"multiSensor": {"sensors": [{"state": 6, "value": value}]}}), + ) + + entity = await self.updated(aioclient_mock, state_json, index=0) + + assert entity.native_value is None + assert entity._feature.is_error is True + assert entity._feature.needs_configuration is True + + async def test_error_state_does_not_need_configuration(self, aioclient_mock): + state_json = jmerge( + self.STATE_DEFAULT, + '{ "multiSensor": { "sensors": [ { "state": 3 } ] } }', + ) + + entity = await self.updated(aioclient_mock, state_json, index=0) + + assert entity._feature.is_error is True + assert entity._feature.needs_configuration is False + + class TestAirSensor(DefaultBoxTest): """Tests for sensors representing BleBox airSensor."""