Add waterheater platform bsblan (#129053)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>pull/131253/head
parent
65652c0adb
commit
37edf982ca
|
@ -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]
|
||||
|
||||
|
|
|
@ -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 ("°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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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 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
|
||||
|
||||
|
|
|
@ -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
|
||||
# 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({
|
||||
|
|
|
@ -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 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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…
Reference in New Issue