mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 08:06:00 +01:00
Fix add checks for None values and check if DHW is available (#151376)
This commit is contained in:
committed by
GitHub
parent
d7e6f84d28
commit
581f8a9378
@@ -81,11 +81,15 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
if self.coordinator.data.state.current_temperature is None:
|
||||
return None
|
||||
return self.coordinator.data.state.current_temperature.value
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.coordinator.data.state.target_temperature is None:
|
||||
return None
|
||||
return self.coordinator.data.state.target_temperature.value
|
||||
|
||||
@property
|
||||
|
||||
@@ -25,7 +25,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize BSBLan flow."""
|
||||
self.host: str | None = None
|
||||
self.host: str = ""
|
||||
self.port: int = DEFAULT_PORT
|
||||
self.mac: str | None = None
|
||||
self.passkey: str | None = None
|
||||
|
||||
@@ -28,6 +28,7 @@ class BSBLanSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes BSB-Lan sensor entity."""
|
||||
|
||||
value_fn: Callable[[BSBLanCoordinatorData], StateType]
|
||||
exists_fn: Callable[[BSBLanCoordinatorData], bool] = lambda data: True
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = (
|
||||
@@ -37,7 +38,12 @@ SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.sensor.current_temperature.value,
|
||||
value_fn=lambda data: (
|
||||
data.sensor.current_temperature.value
|
||||
if data.sensor.current_temperature is not None
|
||||
else None
|
||||
),
|
||||
exists_fn=lambda data: data.sensor.current_temperature is not None,
|
||||
),
|
||||
BSBLanSensorEntityDescription(
|
||||
key="outside_temperature",
|
||||
@@ -45,7 +51,12 @@ SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.sensor.outside_temperature.value,
|
||||
value_fn=lambda data: (
|
||||
data.sensor.outside_temperature.value
|
||||
if data.sensor.outside_temperature is not None
|
||||
else None
|
||||
),
|
||||
exists_fn=lambda data: data.sensor.outside_temperature is not None,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -57,7 +68,16 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up BSB-Lan sensor based on a config entry."""
|
||||
data = entry.runtime_data
|
||||
async_add_entities(BSBLanSensor(data, description) for description in SENSOR_TYPES)
|
||||
|
||||
# Only create sensors for available data points
|
||||
entities = [
|
||||
BSBLanSensor(data, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.exists_fn(data.coordinator.data)
|
||||
]
|
||||
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class BSBLanSensor(BSBLanEntity, SensorEntity):
|
||||
|
||||
@@ -41,6 +41,18 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up BSBLAN water heater based on a config entry."""
|
||||
data = entry.runtime_data
|
||||
|
||||
# Only create water heater entity if DHW (Domestic Hot Water) is available
|
||||
# Check if we have any DHW-related data indicating water heater support
|
||||
dhw_data = data.coordinator.data.dhw
|
||||
if (
|
||||
dhw_data.operating_mode is None
|
||||
and dhw_data.nominal_setpoint is None
|
||||
and dhw_data.dhw_actual_value_top_temperature is None
|
||||
):
|
||||
# No DHW functionality available, skip water heater setup
|
||||
return
|
||||
|
||||
async_add_entities([BSBLANWaterHeater(data)])
|
||||
|
||||
|
||||
@@ -61,23 +73,31 @@ class BSBLANWaterHeater(BSBLanEntity, WaterHeaterEntity):
|
||||
|
||||
# Set temperature limits based on device capabilities
|
||||
self._attr_temperature_unit = data.coordinator.client.get_temperature_unit
|
||||
self._attr_min_temp = data.coordinator.data.dhw.reduced_setpoint.value
|
||||
self._attr_max_temp = data.coordinator.data.dhw.nominal_setpoint_max.value
|
||||
if data.coordinator.data.dhw.reduced_setpoint is not None:
|
||||
self._attr_min_temp = data.coordinator.data.dhw.reduced_setpoint.value
|
||||
if data.coordinator.data.dhw.nominal_setpoint_max is not None:
|
||||
self._attr_max_temp = data.coordinator.data.dhw.nominal_setpoint_max.value
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str | None:
|
||||
"""Return current operation."""
|
||||
if self.coordinator.data.dhw.operating_mode is None:
|
||||
return None
|
||||
current_mode = self.coordinator.data.dhw.operating_mode.desc
|
||||
return OPERATION_MODES.get(current_mode)
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
if self.coordinator.data.dhw.dhw_actual_value_top_temperature is None:
|
||||
return None
|
||||
return self.coordinator.data.dhw.dhw_actual_value_top_temperature.value
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.coordinator.data.dhw.nominal_setpoint is None:
|
||||
return None
|
||||
return self.coordinator.data.dhw.nominal_setpoint.value
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -91,6 +91,50 @@ async def test_climate_entity_properties(
|
||||
assert state.attributes["preset_mode"] == PRESET_ECO
|
||||
|
||||
|
||||
async def test_climate_without_current_temperature_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test climate entity when current temperature sensor is not available."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
# Set current_temperature to None to simulate no temperature sensor
|
||||
mock_bsblan.state.return_value.current_temperature = None
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Should not crash and current_temperature should be None in attributes
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes["current_temperature"] is None
|
||||
|
||||
|
||||
async def test_climate_without_target_temperature_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test climate entity when target temperature sensor is not available."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
# Set target_temperature to None to simulate no temperature sensor
|
||||
mock_bsblan.state.return_value.target_temperature = None
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Should not crash and target temperature should be None in attributes
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes["temperature"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode",
|
||||
[HVACMode.HEAT, HVACMode.AUTO, HVACMode.OFF],
|
||||
|
||||
@@ -28,3 +28,45 @@ async def test_sensor_entity_properties(
|
||||
"""Test the sensor entity properties."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR])
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_sensors_not_created_when_data_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test sensors are not created when sensor data is not available."""
|
||||
# Set all sensor data to None to simulate no sensors available
|
||||
mock_bsblan.sensor.return_value.current_temperature = None
|
||||
mock_bsblan.sensor.return_value.outside_temperature = None
|
||||
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR])
|
||||
|
||||
# Should not create any sensor entities
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
sensor_entities = [entry for entry in entity_entries if entry.domain == "sensor"]
|
||||
assert len(sensor_entities) == 0
|
||||
|
||||
|
||||
async def test_partial_sensors_created_when_some_data_available(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test only available sensors are created when some sensor data is available."""
|
||||
# Only current temperature available, outside temperature not
|
||||
mock_bsblan.sensor.return_value.outside_temperature = None
|
||||
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR])
|
||||
|
||||
# Should create only the current temperature sensor
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
sensor_entities = [entry for entry in entity_entries if entry.domain == "sensor"]
|
||||
assert len(sensor_entities) == 1
|
||||
assert sensor_entities[0].entity_id == ENTITY_CURRENT_TEMP
|
||||
|
||||
@@ -50,6 +50,33 @@ async def test_water_heater_states(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_water_heater_no_dhw_capability(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that no water heater entity is created when DHW capability is missing."""
|
||||
# Mock DHW data to simulate no water heater capability
|
||||
mock_bsblan.hot_water_state.return_value.operating_mode = None
|
||||
mock_bsblan.hot_water_state.return_value.nominal_setpoint = None
|
||||
mock_bsblan.hot_water_state.return_value.dhw_actual_value_top_temperature = None
|
||||
|
||||
await setup_with_selected_platforms(
|
||||
hass, mock_config_entry, [Platform.WATER_HEATER]
|
||||
)
|
||||
|
||||
# Verify no water heater entity was created
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
water_heater_entities = [
|
||||
entity for entity in entities if entity.domain == Platform.WATER_HEATER
|
||||
]
|
||||
|
||||
assert len(water_heater_entities) == 0
|
||||
|
||||
|
||||
async def test_water_heater_entity_properties(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
@@ -208,3 +235,31 @@ async def test_operation_mode_error(
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_water_heater_no_sensors(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test water heater when sensors are not available."""
|
||||
await setup_with_selected_platforms(
|
||||
hass, mock_config_entry, [Platform.WATER_HEATER]
|
||||
)
|
||||
|
||||
# Set all sensors to None to simulate missing sensors
|
||||
mock_bsblan.hot_water_state.return_value.operating_mode = None
|
||||
mock_bsblan.hot_water_state.return_value.dhw_actual_value_top_temperature = None
|
||||
mock_bsblan.hot_water_state.return_value.nominal_setpoint = None
|
||||
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Should not crash and properties should return None
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get("current_operation") is None
|
||||
assert state.attributes.get("current_temperature") is None
|
||||
assert state.attributes.get("temperature") is None
|
||||
|
||||
Reference in New Issue
Block a user