Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions blebox_uniapi/box_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
]
],
Expand Down
19 changes: 15 additions & 4 deletions blebox_uniapi/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class BleboxSensorState(IntEnum):
ERROR = 3
ABOVE_RANGE = 4
BELOW_RANGE = 5
NOT_CONFIGURED = 6


class BaseSensor(Feature):
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
97 changes: 97 additions & 0 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
Loading