mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
Add waterheater platform bsblan (#129053)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
65652c0adb
commit
37edf982ca
@ -18,7 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from .const import CONF_PASSKEY
|
from .const import CONF_PASSKEY
|
||||||
from .coordinator import BSBLanUpdateCoordinator
|
from .coordinator import BSBLanUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
|
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
|
||||||
|
|
||||||
type BSBLanConfigEntry = ConfigEntry[BSBLanData]
|
type BSBLanConfigEntry = ConfigEntry[BSBLanData]
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.components.climate import (
|
|||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
from homeassistant.const import ATTR_TEMPERATURE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
@ -75,26 +75,19 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
|||||||
super().__init__(data.coordinator, data)
|
super().__init__(data.coordinator, data)
|
||||||
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
|
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
|
||||||
|
|
||||||
self._attr_min_temp = float(data.static.min_temp.value)
|
self._attr_min_temp = data.static.min_temp.value
|
||||||
self._attr_max_temp = float(data.static.max_temp.value)
|
self._attr_max_temp = data.static.max_temp.value
|
||||||
if data.static.min_temp.unit in ("°C", "°C"):
|
self._attr_temperature_unit = data.coordinator.client.get_temperature_unit
|
||||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
|
||||||
else:
|
|
||||||
self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self) -> float | None:
|
def current_temperature(self) -> float | None:
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
if self.coordinator.data.state.current_temperature.value == "---":
|
return self.coordinator.data.state.current_temperature.value
|
||||||
# device returns no current temperature
|
|
||||||
return None
|
|
||||||
|
|
||||||
return float(self.coordinator.data.state.current_temperature.value)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self) -> float | None:
|
def target_temperature(self) -> float | None:
|
||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
return float(self.coordinator.data.state.target_temperature.value)
|
return self.coordinator.data.state.target_temperature.value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> HVACMode | None:
|
def hvac_mode(self) -> HVACMode | None:
|
||||||
|
@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
from bsblan import BSBLAN, BSBLANConnectionError, Sensor, State
|
from bsblan import BSBLAN, BSBLANConnectionError, HotWaterState, Sensor, State
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
@ -20,6 +20,7 @@ class BSBLanCoordinatorData:
|
|||||||
|
|
||||||
state: State
|
state: State
|
||||||
sensor: Sensor
|
sensor: Sensor
|
||||||
|
dhw: HotWaterState
|
||||||
|
|
||||||
|
|
||||||
class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
|
class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
|
||||||
@ -59,6 +60,7 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
|
|||||||
|
|
||||||
state = await self.client.state()
|
state = await self.client.state()
|
||||||
sensor = await self.client.sensor()
|
sensor = await self.client.sensor()
|
||||||
|
dhw = await self.client.hot_water_state()
|
||||||
except BSBLANConnectionError as err:
|
except BSBLANConnectionError as err:
|
||||||
host = self.config_entry.data[CONF_HOST] if self.config_entry else "unknown"
|
host = self.config_entry.data[CONF_HOST] if self.config_entry else "unknown"
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
@ -66,4 +68,4 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
|
|||||||
) from err
|
) from err
|
||||||
|
|
||||||
self.update_interval = self._get_update_interval()
|
self.update_interval = self._get_update_interval()
|
||||||
return BSBLanCoordinatorData(state=state, sensor=sensor)
|
return BSBLanCoordinatorData(state=state, sensor=sensor, dhw=dhw)
|
||||||
|
@ -72,11 +72,9 @@ class BSBLanSensor(BSBLanEntity, SensorEntity):
|
|||||||
super().__init__(data.coordinator, data)
|
super().__init__(data.coordinator, data)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{data.device.MAC}-{description.key}"
|
self._attr_unique_id = f"{data.device.MAC}-{description.key}"
|
||||||
|
self._attr_temperature_unit = data.coordinator.client.get_temperature_unit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
value = self.entity_description.value_fn(self.coordinator.data)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
if value == "---":
|
|
||||||
return None
|
|
||||||
return value
|
|
||||||
|
@ -31,6 +31,12 @@
|
|||||||
},
|
},
|
||||||
"set_data_error": {
|
"set_data_error": {
|
||||||
"message": "An error occurred while sending the data to the BSBLAN device"
|
"message": "An error occurred while sending the data to the BSBLAN device"
|
||||||
|
},
|
||||||
|
"set_temperature_error": {
|
||||||
|
"message": "An error occurred while setting the temperature"
|
||||||
|
},
|
||||||
|
"set_operation_mode_error": {
|
||||||
|
"message": "An error occurred while setting the operation mode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
107
homeassistant/components/bsblan/water_heater.py
Normal file
107
homeassistant/components/bsblan/water_heater.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
"""BSBLAN platform to control a compatible Water Heater Device."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from bsblan import BSBLANError
|
||||||
|
|
||||||
|
from homeassistant.components.water_heater import (
|
||||||
|
STATE_ECO,
|
||||||
|
STATE_OFF,
|
||||||
|
WaterHeaterEntity,
|
||||||
|
WaterHeaterEntityFeature,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_TEMPERATURE, STATE_ON
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import BSBLanConfigEntry, BSBLanData
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .entity import BSBLanEntity
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
# Mapping between BSBLan and HA operation modes
|
||||||
|
OPERATION_MODES = {
|
||||||
|
"Eco": STATE_ECO, # Energy saving mode
|
||||||
|
"Off": STATE_OFF, # Protection mode
|
||||||
|
"On": STATE_ON, # Continuous comfort mode
|
||||||
|
}
|
||||||
|
|
||||||
|
OPERATION_MODES_REVERSE = {v: k for k, v in OPERATION_MODES.items()}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: BSBLanConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up BSBLAN water heater based on a config entry."""
|
||||||
|
data = entry.runtime_data
|
||||||
|
async_add_entities([BSBLANWaterHeater(data)])
|
||||||
|
|
||||||
|
|
||||||
|
class BSBLANWaterHeater(BSBLanEntity, WaterHeaterEntity):
|
||||||
|
"""Defines a BSBLAN water heater entity."""
|
||||||
|
|
||||||
|
_attr_name = None
|
||||||
|
_attr_supported_features = (
|
||||||
|
WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||||
|
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, data: BSBLanData) -> None:
|
||||||
|
"""Initialize BSBLAN water heater."""
|
||||||
|
super().__init__(data.coordinator, data)
|
||||||
|
self._attr_unique_id = format_mac(data.device.MAC)
|
||||||
|
self._attr_operation_list = list(OPERATION_MODES_REVERSE.keys())
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_operation(self) -> str | None:
|
||||||
|
"""Return current operation."""
|
||||||
|
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."""
|
||||||
|
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."""
|
||||||
|
return self.coordinator.data.dhw.nominal_setpoint.value
|
||||||
|
|
||||||
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
|
"""Set new target temperature."""
|
||||||
|
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||||
|
try:
|
||||||
|
await self.coordinator.client.set_hot_water(nominal_setpoint=temperature)
|
||||||
|
except BSBLANError as err:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="set_temperature_error",
|
||||||
|
) from err
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||||
|
"""Set new operation mode."""
|
||||||
|
bsblan_mode = OPERATION_MODES_REVERSE.get(operation_mode)
|
||||||
|
try:
|
||||||
|
await self.coordinator.client.set_hot_water(operating_mode=bsblan_mode)
|
||||||
|
except BSBLANError as err:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="set_operation_mode_error",
|
||||||
|
) from err
|
||||||
|
|
||||||
|
await self.coordinator.async_request_refresh()
|
@ -3,7 +3,7 @@
|
|||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
from bsblan import Device, Info, Sensor, State, StaticState
|
from bsblan import Device, HotWaterState, Info, Sensor, State, StaticState
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN
|
from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN
|
||||||
@ -58,6 +58,11 @@ def mock_bsblan() -> Generator[MagicMock]:
|
|||||||
bsblan.sensor.return_value = Sensor.from_json(
|
bsblan.sensor.return_value = Sensor.from_json(
|
||||||
load_fixture("sensor.json", DOMAIN)
|
load_fixture("sensor.json", DOMAIN)
|
||||||
)
|
)
|
||||||
|
bsblan.hot_water_state.return_value = HotWaterState.from_json(
|
||||||
|
load_fixture("dhw_state.json", DOMAIN)
|
||||||
|
)
|
||||||
|
# mock get_temperature_unit property
|
||||||
|
bsblan.get_temperature_unit = "°C"
|
||||||
|
|
||||||
yield bsblan
|
yield bsblan
|
||||||
|
|
||||||
|
110
tests/components/bsblan/fixtures/dhw_state.json
Normal file
110
tests/components/bsblan/fixtures/dhw_state.json
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"operating_mode": {
|
||||||
|
"name": "DHW operating mode",
|
||||||
|
"error": 0,
|
||||||
|
"value": "On",
|
||||||
|
"desc": "On",
|
||||||
|
"dataType": 1,
|
||||||
|
"readonly": 0,
|
||||||
|
"unit": ""
|
||||||
|
},
|
||||||
|
"nominal_setpoint": {
|
||||||
|
"name": "DHW nominal setpoint",
|
||||||
|
"error": 0,
|
||||||
|
"value": "50.0",
|
||||||
|
"desc": "",
|
||||||
|
"dataType": 0,
|
||||||
|
"readonly": 0,
|
||||||
|
"unit": "°C"
|
||||||
|
},
|
||||||
|
"nominal_setpoint_max": {
|
||||||
|
"name": "DHW nominal setpoint maximum",
|
||||||
|
"error": 0,
|
||||||
|
"value": "65.0",
|
||||||
|
"desc": "",
|
||||||
|
"dataType": 0,
|
||||||
|
"readonly": 0,
|
||||||
|
"unit": "°C"
|
||||||
|
},
|
||||||
|
"reduced_setpoint": {
|
||||||
|
"name": "DHW reduced setpoint",
|
||||||
|
"error": 0,
|
||||||
|
"value": "40.0",
|
||||||
|
"desc": "",
|
||||||
|
"dataType": 0,
|
||||||
|
"readonly": 0,
|
||||||
|
"unit": "°C"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"name": "DHW release programme",
|
||||||
|
"error": 0,
|
||||||
|
"value": "1",
|
||||||
|
"desc": "Released",
|
||||||
|
"dataType": 1,
|
||||||
|
"readonly": 0,
|
||||||
|
"unit": ""
|
||||||
|
},
|
||||||
|
"legionella_function": {
|
||||||
|
"name": "Legionella function fixed weekday",
|
||||||
|
"error": 0,
|
||||||
|
"value": "0",
|
||||||
|
"desc": "Off",
|
||||||
|
"dataType": 1,
|
||||||
|
"readonly": 0,
|
||||||
|
"unit": ""
|
||||||
|
},
|
||||||
|
"legionella_setpoint": {
|
||||||
|
"name": "Legionella function setpoint",
|
||||||
|
"error": 0,
|
||||||
|
"value": "60.0",
|
||||||
|
"desc": "",
|
||||||
|
"dataType": 0,
|
||||||
|
"readonly": 0,
|
||||||
|
"unit": "°C"
|
||||||
|
},
|
||||||
|
"legionella_periodicity": {
|
||||||
|
"name": "Legionella function periodicity",
|
||||||
|
"error": 0,
|
||||||
|
"value": "7",
|
||||||
|
"desc": "Weekly",
|
||||||
|
"dataType": 0,
|
||||||
|
"readonly": 0,
|
||||||
|
"unit": "days"
|
||||||
|
},
|
||||||
|
"legionella_function_day": {
|
||||||
|
"name": "Legionella function day",
|
||||||
|
"error": 0,
|
||||||
|
"value": "6",
|
||||||
|
"desc": "Saturday",
|
||||||
|
"dataType": 1,
|
||||||
|
"readonly": 0,
|
||||||
|
"unit": ""
|
||||||
|
},
|
||||||
|
"legionella_function_time": {
|
||||||
|
"name": "Legionella function time",
|
||||||
|
"error": 0,
|
||||||
|
"value": "12:00",
|
||||||
|
"desc": "",
|
||||||
|
"dataType": 2,
|
||||||
|
"readonly": 0,
|
||||||
|
"unit": ""
|
||||||
|
},
|
||||||
|
"dhw_actual_value_top_temperature": {
|
||||||
|
"name": "DHW temperature actual value",
|
||||||
|
"error": 0,
|
||||||
|
"value": "48.5",
|
||||||
|
"desc": "",
|
||||||
|
"dataType": 0,
|
||||||
|
"readonly": 1,
|
||||||
|
"unit": "°C"
|
||||||
|
},
|
||||||
|
"state_dhw_pump": {
|
||||||
|
"name": "State DHW circulation pump",
|
||||||
|
"error": 0,
|
||||||
|
"value": "0",
|
||||||
|
"desc": "Off",
|
||||||
|
"dataType": 1,
|
||||||
|
"readonly": 1,
|
||||||
|
"unit": ""
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
# name: test_celsius_fahrenheit[static.json][climate.bsb_lan-entry]
|
# name: test_celsius_fahrenheit[climate.bsb_lan-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
}),
|
}),
|
||||||
@ -44,7 +44,7 @@
|
|||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_celsius_fahrenheit[static.json][climate.bsb_lan-state]
|
# name: test_celsius_fahrenheit[climate.bsb_lan-state]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'current_temperature': 18.6,
|
'current_temperature': 18.6,
|
||||||
@ -72,79 +72,6 @@
|
|||||||
'state': 'heat',
|
'state': 'heat',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_celsius_fahrenheit[static_F.json][climate.bsb_lan-entry]
|
|
||||||
EntityRegistryEntrySnapshot({
|
|
||||||
'aliases': set({
|
|
||||||
}),
|
|
||||||
'area_id': None,
|
|
||||||
'capabilities': dict({
|
|
||||||
'hvac_modes': list([
|
|
||||||
<HVACMode.AUTO: 'auto'>,
|
|
||||||
<HVACMode.HEAT: 'heat'>,
|
|
||||||
<HVACMode.OFF: 'off'>,
|
|
||||||
]),
|
|
||||||
'max_temp': -6.7,
|
|
||||||
'min_temp': -13.3,
|
|
||||||
'preset_modes': list([
|
|
||||||
'eco',
|
|
||||||
'none',
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
'config_entry_id': <ANY>,
|
|
||||||
'device_class': None,
|
|
||||||
'device_id': <ANY>,
|
|
||||||
'disabled_by': None,
|
|
||||||
'domain': 'climate',
|
|
||||||
'entity_category': None,
|
|
||||||
'entity_id': 'climate.bsb_lan',
|
|
||||||
'has_entity_name': True,
|
|
||||||
'hidden_by': None,
|
|
||||||
'icon': None,
|
|
||||||
'id': <ANY>,
|
|
||||||
'labels': set({
|
|
||||||
}),
|
|
||||||
'name': None,
|
|
||||||
'options': dict({
|
|
||||||
}),
|
|
||||||
'original_device_class': None,
|
|
||||||
'original_icon': None,
|
|
||||||
'original_name': None,
|
|
||||||
'platform': 'bsblan',
|
|
||||||
'previous_unique_id': None,
|
|
||||||
'supported_features': <ClimateEntityFeature: 401>,
|
|
||||||
'translation_key': None,
|
|
||||||
'unique_id': '00:80:41:19:69:90-climate',
|
|
||||||
'unit_of_measurement': None,
|
|
||||||
})
|
|
||||||
# ---
|
|
||||||
# name: test_celsius_fahrenheit[static_F.json][climate.bsb_lan-state]
|
|
||||||
StateSnapshot({
|
|
||||||
'attributes': ReadOnlyDict({
|
|
||||||
'current_temperature': -7.4,
|
|
||||||
'friendly_name': 'BSB-LAN',
|
|
||||||
'hvac_modes': list([
|
|
||||||
<HVACMode.AUTO: 'auto'>,
|
|
||||||
<HVACMode.HEAT: 'heat'>,
|
|
||||||
<HVACMode.OFF: 'off'>,
|
|
||||||
]),
|
|
||||||
'max_temp': -6.7,
|
|
||||||
'min_temp': -13.3,
|
|
||||||
'preset_mode': 'none',
|
|
||||||
'preset_modes': list([
|
|
||||||
'eco',
|
|
||||||
'none',
|
|
||||||
]),
|
|
||||||
'supported_features': <ClimateEntityFeature: 401>,
|
|
||||||
'temperature': -7.5,
|
|
||||||
}),
|
|
||||||
'context': <ANY>,
|
|
||||||
'entity_id': 'climate.bsb_lan',
|
|
||||||
'last_changed': <ANY>,
|
|
||||||
'last_reported': <ANY>,
|
|
||||||
'last_updated': <ANY>,
|
|
||||||
'state': 'heat',
|
|
||||||
})
|
|
||||||
# ---
|
|
||||||
# name: test_climate_entity_properties[climate.bsb_lan-entry]
|
# name: test_climate_entity_properties[climate.bsb_lan-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
|
68
tests/components/bsblan/snapshots/test_water_heater.ambr
Normal file
68
tests/components/bsblan/snapshots/test_water_heater.ambr
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_water_heater_states[dhw_state.json][water_heater.bsb_lan-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'max_temp': 65.0,
|
||||||
|
'min_temp': 40.0,
|
||||||
|
'operation_list': list([
|
||||||
|
'eco',
|
||||||
|
'off',
|
||||||
|
'on',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'water_heater',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'water_heater.bsb_lan',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': None,
|
||||||
|
'platform': 'bsblan',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': <WaterHeaterEntityFeature: 3>,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': '00:80:41:19:69:90',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_water_heater_states[dhw_state.json][water_heater.bsb_lan-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'current_temperature': 48.5,
|
||||||
|
'friendly_name': 'BSB-LAN',
|
||||||
|
'max_temp': 65.0,
|
||||||
|
'min_temp': 40.0,
|
||||||
|
'operation_list': list([
|
||||||
|
'eco',
|
||||||
|
'off',
|
||||||
|
'on',
|
||||||
|
]),
|
||||||
|
'operation_mode': 'on',
|
||||||
|
'supported_features': <WaterHeaterEntityFeature: 3>,
|
||||||
|
'target_temp_high': None,
|
||||||
|
'target_temp_low': None,
|
||||||
|
'temperature': 50.0,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'water_heater.bsb_lan',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'on',
|
||||||
|
})
|
||||||
|
# ---
|
@ -3,12 +3,11 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
from bsblan import BSBLANError, StaticState
|
from bsblan import BSBLANError
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.bsblan.const import DOMAIN
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ATTR_HVAC_MODE,
|
ATTR_HVAC_MODE,
|
||||||
ATTR_PRESET_MODE,
|
ATTR_PRESET_MODE,
|
||||||
@ -27,37 +26,19 @@ import homeassistant.helpers.entity_registry as er
|
|||||||
|
|
||||||
from . import setup_with_selected_platforms
|
from . import setup_with_selected_platforms
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||||
MockConfigEntry,
|
|
||||||
async_fire_time_changed,
|
|
||||||
load_json_object_fixture,
|
|
||||||
snapshot_platform,
|
|
||||||
)
|
|
||||||
|
|
||||||
ENTITY_ID = "climate.bsb_lan"
|
ENTITY_ID = "climate.bsb_lan"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("static_file"),
|
|
||||||
[
|
|
||||||
("static.json"),
|
|
||||||
("static_F.json"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def test_celsius_fahrenheit(
|
async def test_celsius_fahrenheit(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_bsblan: AsyncMock,
|
mock_bsblan: AsyncMock,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
static_file: str,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Celsius and Fahrenheit temperature units."""
|
"""Test Celsius and Fahrenheit temperature units."""
|
||||||
|
|
||||||
static_data = load_json_object_fixture(static_file, DOMAIN)
|
|
||||||
|
|
||||||
mock_bsblan.static_values.return_value = StaticState.from_dict(static_data)
|
|
||||||
|
|
||||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||||
|
|
||||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
@ -75,21 +56,9 @@ async def test_climate_entity_properties(
|
|||||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
# Test when current_temperature is "---"
|
|
||||||
mock_current_temp = MagicMock()
|
|
||||||
mock_current_temp.value = "---"
|
|
||||||
mock_bsblan.state.return_value.current_temperature = mock_current_temp
|
|
||||||
|
|
||||||
freezer.tick(timedelta(minutes=1))
|
|
||||||
async_fire_time_changed(hass)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
state = hass.states.get(ENTITY_ID)
|
|
||||||
assert state.attributes["current_temperature"] is None
|
|
||||||
|
|
||||||
# Test target_temperature
|
# Test target_temperature
|
||||||
mock_target_temp = MagicMock()
|
mock_target_temp = MagicMock()
|
||||||
mock_target_temp.value = "23.5"
|
mock_target_temp.value = 23.5
|
||||||
mock_bsblan.state.return_value.target_temperature = mock_target_temp
|
mock_bsblan.state.return_value.target_temperature = mock_target_temp
|
||||||
|
|
||||||
freezer.tick(timedelta(minutes=1))
|
freezer.tick(timedelta(minutes=1))
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
"""Tests for the BSB-Lan sensor platform."""
|
"""Tests for the BSB-Lan sensor platform."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from unittest.mock import AsyncMock
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
|
||||||
|
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.entity_registry as er
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
|
||||||
from . import setup_with_selected_platforms
|
from . import setup_with_selected_platforms
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
from tests.common import MockConfigEntry, snapshot_platform
|
||||||
|
|
||||||
ENTITY_CURRENT_TEMP = "sensor.bsb_lan_current_temperature"
|
ENTITY_CURRENT_TEMP = "sensor.bsb_lan_current_temperature"
|
||||||
ENTITY_OUTSIDE_TEMP = "sensor.bsb_lan_outside_temperature"
|
ENTITY_OUTSIDE_TEMP = "sensor.bsb_lan_outside_temperature"
|
||||||
@ -30,37 +28,3 @@ async def test_sensor_entity_properties(
|
|||||||
"""Test the sensor entity properties."""
|
"""Test the sensor entity properties."""
|
||||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR])
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR])
|
||||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("value", "expected_state"),
|
|
||||||
[
|
|
||||||
(18.6, "18.6"),
|
|
||||||
(None, STATE_UNKNOWN),
|
|
||||||
("---", STATE_UNKNOWN),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def test_current_temperature_scenarios(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_bsblan: AsyncMock,
|
|
||||||
mock_config_entry: MockConfigEntry,
|
|
||||||
freezer: FrozenDateTimeFactory,
|
|
||||||
value,
|
|
||||||
expected_state,
|
|
||||||
) -> None:
|
|
||||||
"""Test various scenarios for current temperature sensor."""
|
|
||||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR])
|
|
||||||
|
|
||||||
# Set up the mock value
|
|
||||||
mock_current_temp = MagicMock()
|
|
||||||
mock_current_temp.value = value
|
|
||||||
mock_bsblan.sensor.return_value.current_temperature = mock_current_temp
|
|
||||||
|
|
||||||
# Trigger an update
|
|
||||||
freezer.tick(timedelta(minutes=1))
|
|
||||||
async_fire_time_changed(hass)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
# Check the state
|
|
||||||
state = hass.states.get(ENTITY_CURRENT_TEMP)
|
|
||||||
assert state.state == expected_state
|
|
||||||
|
210
tests/components/bsblan/test_water_heater.py
Normal file
210
tests/components/bsblan/test_water_heater.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
"""Tests for the BSB-Lan water heater platform."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
from bsblan import BSBLANError
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.water_heater import (
|
||||||
|
ATTR_OPERATION_MODE,
|
||||||
|
DOMAIN as WATER_HEATER_DOMAIN,
|
||||||
|
SERVICE_SET_OPERATION_MODE,
|
||||||
|
SERVICE_SET_TEMPERATURE,
|
||||||
|
STATE_ECO,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
|
||||||
|
from . import setup_with_selected_platforms
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||||
|
|
||||||
|
ENTITY_ID = "water_heater.bsb_lan"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("dhw_file"),
|
||||||
|
[
|
||||||
|
("dhw_state.json"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_water_heater_states(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_bsblan: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
dhw_file: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test water heater states with different configurations."""
|
||||||
|
await setup_with_selected_platforms(
|
||||||
|
hass, mock_config_entry, [Platform.WATER_HEATER]
|
||||||
|
)
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_water_heater_entity_properties(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_bsblan: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test the water heater entity properties."""
|
||||||
|
await setup_with_selected_platforms(
|
||||||
|
hass, mock_config_entry, [Platform.WATER_HEATER]
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(ENTITY_ID)
|
||||||
|
assert state is not None
|
||||||
|
|
||||||
|
# Test when nominal setpoint is "10"
|
||||||
|
mock_setpoint = MagicMock()
|
||||||
|
mock_setpoint.value = 10
|
||||||
|
mock_bsblan.hot_water_state.return_value.nominal_setpoint = mock_setpoint
|
||||||
|
|
||||||
|
freezer.tick(timedelta(minutes=1))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(ENTITY_ID)
|
||||||
|
assert state.attributes.get("temperature") == 10
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("mode", "bsblan_mode"),
|
||||||
|
[
|
||||||
|
(STATE_ECO, "Eco"),
|
||||||
|
(STATE_OFF, "Off"),
|
||||||
|
(STATE_ON, "On"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_set_operation_mode(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_bsblan: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mode: str,
|
||||||
|
bsblan_mode: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting operation mode."""
|
||||||
|
await setup_with_selected_platforms(
|
||||||
|
hass, mock_config_entry, [Platform.WATER_HEATER]
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
domain=WATER_HEATER_DOMAIN,
|
||||||
|
service=SERVICE_SET_OPERATION_MODE,
|
||||||
|
service_data={
|
||||||
|
ATTR_ENTITY_ID: ENTITY_ID,
|
||||||
|
ATTR_OPERATION_MODE: mode,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_bsblan.set_hot_water.assert_called_once_with(operating_mode=bsblan_mode)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_invalid_operation_mode(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_bsblan: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting invalid operation mode."""
|
||||||
|
await setup_with_selected_platforms(
|
||||||
|
hass, mock_config_entry, [Platform.WATER_HEATER]
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
HomeAssistantError,
|
||||||
|
match=r"Operation mode invalid_mode is not valid for water_heater\.bsb_lan\. Valid operation modes are: eco, off, on",
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
domain=WATER_HEATER_DOMAIN,
|
||||||
|
service=SERVICE_SET_OPERATION_MODE,
|
||||||
|
service_data={
|
||||||
|
ATTR_ENTITY_ID: ENTITY_ID,
|
||||||
|
ATTR_OPERATION_MODE: "invalid_mode",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_temperature(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_bsblan: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting temperature."""
|
||||||
|
await setup_with_selected_platforms(
|
||||||
|
hass, mock_config_entry, [Platform.WATER_HEATER]
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
domain=WATER_HEATER_DOMAIN,
|
||||||
|
service=SERVICE_SET_TEMPERATURE,
|
||||||
|
service_data={
|
||||||
|
ATTR_ENTITY_ID: ENTITY_ID,
|
||||||
|
ATTR_TEMPERATURE: 50,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_bsblan.set_hot_water.assert_called_once_with(nominal_setpoint=50)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_temperature_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_bsblan: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test setting temperature with API failure."""
|
||||||
|
await setup_with_selected_platforms(
|
||||||
|
hass, mock_config_entry, [Platform.WATER_HEATER]
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_bsblan.set_hot_water.side_effect = BSBLANError("Test error")
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
HomeAssistantError, match="An error occurred while setting the temperature"
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
domain=WATER_HEATER_DOMAIN,
|
||||||
|
service=SERVICE_SET_TEMPERATURE,
|
||||||
|
service_data={
|
||||||
|
ATTR_ENTITY_ID: ENTITY_ID,
|
||||||
|
ATTR_TEMPERATURE: 50,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_operation_mode_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_bsblan: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test operation mode setting with API failure."""
|
||||||
|
await setup_with_selected_platforms(
|
||||||
|
hass, mock_config_entry, [Platform.WATER_HEATER]
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_bsblan.set_hot_water.side_effect = BSBLANError("Test error")
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
HomeAssistantError, match="An error occurred while setting the operation mode"
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
domain=WATER_HEATER_DOMAIN,
|
||||||
|
service=SERVICE_SET_OPERATION_MODE,
|
||||||
|
service_data={
|
||||||
|
ATTR_ENTITY_ID: ENTITY_ID,
|
||||||
|
ATTR_OPERATION_MODE: STATE_ECO,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user