Mark Blebox sensor and climate entities unavailable on error state (#179128)

This commit is contained in:
bkobus-bbx
2026-08-14 13:17:44 +02:00
committed by GitHub
parent 2b274091d3
commit 4b665ff8cf
5 changed files with 126 additions and 15 deletions
@@ -59,6 +59,12 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
@property
@override
def available(self) -> bool:
"""Return if entity is available."""
return super().available and not self._feature.is_error
@property
@override
def hvac_modes(self) -> list[HVACMode]:
@@ -238,6 +238,12 @@ class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEn
self._attr_translation_key = f"{description.translation_key}_n"
self._attr_translation_placeholders = {"index": str(index)}
@property
@override
def available(self) -> bool:
"""Return if entity is available."""
return super().available and not self._feature.is_error
@property
@override
def native_value(self) -> StateType:
+21
View File
@@ -42,9 +42,30 @@ def setup_product_mock(category, feature_mocks, path=None):
def mock_only_feature(spec, set_spec: bool = True, **kwargs):
"""Mock just the feature, without the product setup."""
if issubclass(spec, blebox_uniapi.sensor.BaseSensor):
# Autospec would leave is_error as a truthy Mock, making entities unavailable.
kwargs.setdefault("is_error", False)
return mock.create_autospec(spec, set_spec, True, **kwargs)
def setup_multi_feature_product(category, features, name, model):
"""Mock a product exposing several features of the same category."""
product = setup_product_mock(category, features)
type(product).name = PropertyMock(return_value=name)
type(product).model = PropertyMock(return_value=model)
type(product).product = PropertyMock(return_value=model)
type(product).brand = PropertyMock(return_value="BleBox")
type(product).firmware_version = PropertyMock(return_value="1.23")
type(product).unique_id = PropertyMock(return_value="aabbcc112233")
for feature in features:
type(feature).product = PropertyMock(return_value=product)
type(feature).name = PropertyMock(return_value=None)
feature.async_update = AsyncMock()
return product
def mock_feature(category, spec, set_spec: bool = True, **kwargs):
"""Mock a feature along with whole product setup."""
feature_mock = mock_only_feature(spec, set_spec, **kwargs)
+16 -1
View File
@@ -1,7 +1,7 @@
"""BleBox climate entities tests."""
import logging
from unittest.mock import AsyncMock, PropertyMock
from unittest.mock import AsyncMock, Mock, PropertyMock
import blebox_uniapi
import pytest
@@ -25,6 +25,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
@@ -137,6 +138,20 @@ async def test_update(saunabox, hass: HomeAssistant, config) -> None:
assert state.state == HVACMode.OFF
async def test_update_with_safety_off_state_is_unavailable(
hass: HomeAssistant, thermobox: tuple[Mock, str]
) -> None:
"""A thermostat switched off by its safety system must become unavailable."""
feature_mock, entity_id = thermobox
feature_mock.is_error = True
feature_mock.current = None
await async_setup_entity(hass, entity_id)
state = hass.states.get(entity_id)
assert state.state == STATE_UNAVAILABLE
async def test_set_hvac_mode_heat(saunabox, hass: HomeAssistant) -> None:
"""Test that setting HVAC mode to heat calls async_on."""
+77 -14
View File
@@ -1,7 +1,7 @@
"""Blebox sensors tests."""
import logging
from unittest.mock import AsyncMock, PropertyMock
from unittest.mock import AsyncMock, Mock, PropertyMock
import blebox_uniapi
import pytest
@@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfDensity,
UnitOfTemperature,
@@ -25,7 +26,7 @@ from .conftest import (
async_setup_entity,
mock_feature,
mock_only_feature,
setup_product_mock,
setup_multi_feature_product,
)
from tests.common import MockConfigEntry
@@ -133,6 +134,41 @@ async def test_update_failure(
assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_update_with_error_state_is_unavailable(
hass: HomeAssistant, tempsensor: tuple[Mock, str]
) -> None:
"""A sensor reporting an error state must become unavailable."""
feature_mock, entity_id = tempsensor
feature_mock.is_error = True
feature_mock.native_value = None
await async_setup_entity(hass, entity_id)
state = hass.states.get(entity_id)
assert state.state == STATE_UNAVAILABLE
async def test_update_recovers_after_error_state_clears(
hass: HomeAssistant, tempsensor: tuple[Mock, str], config_entry: MockConfigEntry
) -> None:
"""Entity becomes available again once the sensor reports a valid state."""
feature_mock, entity_id = tempsensor
feature_mock.is_error = True
feature_mock.native_value = None
await async_setup_config_entry(hass, config_entry)
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
feature_mock.is_error = False
feature_mock.native_value = 21.5
await config_entry.runtime_data.async_refresh()
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == "21.5"
async def test_airsensor_init(
airsensor, hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
@@ -201,18 +237,7 @@ async def test_multi_sensor_multiple_have_channel_suffix(
for i in range(4)
]
product = setup_product_mock("sensors", features)
type(product).name = PropertyMock(return_value="My smart meter")
type(product).model = PropertyMock(return_value="smartMeter")
type(product).product = PropertyMock(return_value="smartMeter")
type(product).brand = PropertyMock(return_value="BleBox")
type(product).firmware_version = PropertyMock(return_value="1.23")
type(product).unique_id = PropertyMock(return_value="aabbcc112233")
for feature in features:
type(feature).product = PropertyMock(return_value=product)
type(feature).name = PropertyMock(return_value=None)
feature.async_update = AsyncMock()
setup_multi_feature_product("sensors", features, "My smart meter", "smartMeter")
entity_ids = [
"sensor.my_smart_meter_voltage",
@@ -228,6 +253,44 @@ async def test_multi_sensor_multiple_have_channel_suffix(
assert hass.states.get(entity_ids[3]).name == "My smart meter Voltage 3"
async def test_sensor_error_does_not_affect_sibling_sensors(
hass: HomeAssistant,
) -> None:
"""A single errored sensor must not make sibling sensors unavailable."""
ok_feature = mock_only_feature(
blebox_uniapi.sensor.GenericSensor,
unique_id="BleBox-multiSensor-aabbcc-humidity_0",
full_name="multiSensor-humidity_0",
device_class="humidity",
unit="percentage",
native_value=42,
sensor_id=0,
index=0,
)
error_feature = mock_only_feature(
blebox_uniapi.sensor.Temperature,
unique_id="BleBox-multiSensor-aabbcc-temperature_1",
full_name="multiSensor-temperature_1",
device_class="temperature",
unit="celsius",
current=None,
native_value=None,
is_error=True,
sensor_id=1,
index=1,
)
setup_multi_feature_product(
"sensors", [ok_feature, error_feature], "My multi sensor", "multiSensor"
)
ok_entity_id = "sensor.my_multi_sensor_humidity"
error_entity_id = "sensor.my_multi_sensor_temperature"
await async_setup_entities(hass, [ok_entity_id, error_entity_id])
assert hass.states.get(ok_entity_id).state == "42"
assert hass.states.get(error_entity_id).state == STATE_UNAVAILABLE
async def test_airsensor_update(airsensor, hass: HomeAssistant) -> None:
"""Test air quality sensor state after update."""