Add waterheater platform bsblan (#129053)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
pull/131253/head
Willem-Jan van Rootselaar 2024-11-22 12:17:53 +01:00 committed by GitHub
parent 65652c0adb
commit 37edf982ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 528 additions and 169 deletions

View File

@ -18,7 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_PASSKEY
from .coordinator import BSBLanUpdateCoordinator
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
type BSBLanConfigEntry = ConfigEntry[BSBLanData]

View File

@ -15,7 +15,7 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.device_registry import format_mac
@ -75,26 +75,19 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
super().__init__(data.coordinator, data)
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
self._attr_min_temp = float(data.static.min_temp.value)
self._attr_max_temp = float(data.static.max_temp.value)
if data.static.min_temp.unit in ("&deg;C", "°C"):
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
else:
self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
self._attr_min_temp = data.static.min_temp.value
self._attr_max_temp = data.static.max_temp.value
self._attr_temperature_unit = data.coordinator.client.get_temperature_unit
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self.coordinator.data.state.current_temperature.value == "---":
# device returns no current temperature
return None
return float(self.coordinator.data.state.current_temperature.value)
return self.coordinator.data.state.current_temperature.value
@property
def target_temperature(self) -> float | None:
"""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
def hvac_mode(self) -> HVACMode | None:

View File

@ -4,7 +4,7 @@ from dataclasses import dataclass
from datetime import timedelta
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.const import CONF_HOST
@ -20,6 +20,7 @@ class BSBLanCoordinatorData:
state: State
sensor: Sensor
dhw: HotWaterState
class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
@ -59,6 +60,7 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
state = await self.client.state()
sensor = await self.client.sensor()
dhw = await self.client.hot_water_state()
except BSBLANConnectionError as err:
host = self.config_entry.data[CONF_HOST] if self.config_entry else "unknown"
raise UpdateFailed(
@ -66,4 +68,4 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
) from err
self.update_interval = self._get_update_interval()
return BSBLanCoordinatorData(state=state, sensor=sensor)
return BSBLanCoordinatorData(state=state, sensor=sensor, dhw=dhw)

View File

@ -72,11 +72,9 @@ class BSBLanSensor(BSBLanEntity, SensorEntity):
super().__init__(data.coordinator, data)
self.entity_description = description
self._attr_unique_id = f"{data.device.MAC}-{description.key}"
self._attr_temperature_unit = data.coordinator.client.get_temperature_unit
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
value = self.entity_description.value_fn(self.coordinator.data)
if value == "---":
return None
return value
return self.entity_description.value_fn(self.coordinator.data)

View File

@ -31,6 +31,12 @@
},
"set_data_error": {
"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": {

View 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()

View File

@ -3,7 +3,7 @@
from collections.abc import Generator
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
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(
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

View 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": "&deg;C"
},
"nominal_setpoint_max": {
"name": "DHW nominal setpoint maximum",
"error": 0,
"value": "65.0",
"desc": "",
"dataType": 0,
"readonly": 0,
"unit": "&deg;C"
},
"reduced_setpoint": {
"name": "DHW reduced setpoint",
"error": 0,
"value": "40.0",
"desc": "",
"dataType": 0,
"readonly": 0,
"unit": "&deg;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": "&deg;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": "&deg;C"
},
"state_dhw_pump": {
"name": "State DHW circulation pump",
"error": 0,
"value": "0",
"desc": "Off",
"dataType": 1,
"readonly": 1,
"unit": ""
}
}

View File

@ -1,5 +1,5 @@
# serializer version: 1
# name: test_celsius_fahrenheit[static.json][climate.bsb_lan-entry]
# name: test_celsius_fahrenheit[climate.bsb_lan-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@ -44,7 +44,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_celsius_fahrenheit[static.json][climate.bsb_lan-state]
# name: test_celsius_fahrenheit[climate.bsb_lan-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 18.6,
@ -72,79 +72,6 @@
'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]
EntityRegistryEntrySnapshot({
'aliases': set({

View 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',
})
# ---

View File

@ -3,12 +3,11 @@
from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock
from bsblan import BSBLANError, StaticState
from bsblan import BSBLANError
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.bsblan.const import DOMAIN
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ATTR_PRESET_MODE,
@ -27,37 +26,19 @@ import homeassistant.helpers.entity_registry as er
from . import setup_with_selected_platforms
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
load_json_object_fixture,
snapshot_platform,
)
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
ENTITY_ID = "climate.bsb_lan"
@pytest.mark.parametrize(
("static_file"),
[
("static.json"),
("static_F.json"),
],
)
async def test_celsius_fahrenheit(
hass: HomeAssistant,
mock_bsblan: AsyncMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
static_file: str,
) -> None:
"""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 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 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
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
freezer.tick(timedelta(minutes=1))

View File

@ -1,19 +1,17 @@
"""Tests for the BSB-Lan sensor platform."""
from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import STATE_UNKNOWN, Platform
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
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_OUTSIDE_TEMP = "sensor.bsb_lan_outside_temperature"
@ -30,37 +28,3 @@ 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)
@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

View 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,
)