From 0a2a699133728e3334a0b513a849127f36531fe3 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 8 Nov 2023 12:40:28 +0100 Subject: [PATCH] Extend climate tests for nibe heatpump (#103522) --- .coveragerc | 1 - .../components/nibe_heatpump/climate.py | 13 +- .../nibe_heatpump/snapshots/test_climate.ambr | 468 +++++++++++++++++- .../components/nibe_heatpump/test_climate.py | 293 ++++++++++- 4 files changed, 745 insertions(+), 30 deletions(-) diff --git a/.coveragerc b/.coveragerc index 0bd6d40ac34..d58eafa442c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -819,7 +819,6 @@ omit = homeassistant/components/nfandroidtv/__init__.py homeassistant/components/nfandroidtv/notify.py homeassistant/components/nibe_heatpump/__init__.py - homeassistant/components/nibe_heatpump/climate.py homeassistant/components/nibe_heatpump/binary_sensor.py homeassistant/components/nibe_heatpump/select.py homeassistant/components/nibe_heatpump/sensor.py diff --git a/homeassistant/components/nibe_heatpump/climate.py b/homeassistant/components/nibe_heatpump/climate.py index 99109ed8609..6280994bd7d 100644 --- a/homeassistant/components/nibe_heatpump/climate.py +++ b/homeassistant/components/nibe_heatpump/climate.py @@ -24,7 +24,6 @@ from homeassistant.components.climate import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -48,10 +47,7 @@ async def async_setup_entry( coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] - main_unit = UNIT_COILGROUPS.get(coordinator.series, {}).get("main") - if not main_unit: - LOGGER.debug("Skipping climates - no main unit found") - return + main_unit = UNIT_COILGROUPS[coordinator.series]["main"] def climate_systems(): for key, group in CLIMATE_COILGROUPS.get(coordinator.series, ()).items(): @@ -128,9 +124,6 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity): @callback def _handle_coordinator_update(self) -> None: - if not self.coordinator.data: - return - def _get_value(coil: Coil) -> int | str | float | None: return self.coordinator.get_coil_value(coil) @@ -179,7 +172,7 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity): else: self._attr_hvac_action = HVACAction.IDLE else: - self._attr_hvac_action = None + self._attr_hvac_action = HVACAction.OFF self.async_write_ha_state() @@ -247,4 +240,4 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity): ) await coordinator.async_write_coil(self._coil_use_room_sensor, "OFF") else: - raise HomeAssistantError(f"{hvac_mode} mode not supported for {self.name}") + raise ValueError(f"{hvac_mode} mode not supported for {self.name}") diff --git a/tests/components/nibe_heatpump/snapshots/test_climate.ambr b/tests/components/nibe_heatpump/snapshots/test_climate.ambr index 3d08565e105..f19fd69c47d 100644 --- a/tests/components/nibe_heatpump/snapshots/test_climate.ambr +++ b/tests/components/nibe_heatpump/snapshots/test_climate.ambr @@ -1,5 +1,391 @@ # serializer version: 1 -# name: test_basic[Model.S320-s1-climate.climate_system_s1][1. initial] +# name: test_active_accessory[Model.F1155-s2-climate.climate_system_s2][initial] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S2', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': 30.0, + 'target_temp_low': 21.0, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s2', + 'last_changed': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_active_accessory[Model.F1155-s2-climate.climate_system_s2][unavailable (not supported)] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Climate System S2', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_step': 0.5, + }), + 'context': , + 'entity_id': 'climate.climate_system_s2', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_active_accessory[Model.F1155-s3-climate.climate_system_s3][initial] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S3', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': 30.0, + 'target_temp_low': 21.0, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s3', + 'last_changed': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_active_accessory[Model.F1155-s3-climate.climate_system_s3][unavailable (not supported)] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Climate System S3', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_step': 0.5, + }), + 'context': , + 'entity_id': 'climate.climate_system_s3', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_active_accessory[Model.S320-s2-climate.climate_system_21][initial] + None +# --- +# name: test_active_accessory[Model.S320-s2-climate.climate_system_s1][initial] + None +# --- +# name: test_basic[Model.F1155-s2-climate.climate_system_s2][cooling] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S2', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': 30.0, + 'target_temp_low': 21.0, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s2', + 'last_changed': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_basic[Model.F1155-s2-climate.climate_system_s2][heating (auto)] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S2', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s2', + 'last_changed': , + 'last_updated': , + 'state': 'auto', + }) +# --- +# name: test_basic[Model.F1155-s2-climate.climate_system_s2][heating (only)] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S2', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'target_temp_step': 0.5, + 'temperature': 21.0, + }), + 'context': , + 'entity_id': 'climate.climate_system_s2', + 'last_changed': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_basic[Model.F1155-s2-climate.climate_system_s2][heating] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S2', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': 30.0, + 'target_temp_low': 21.0, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s2', + 'last_changed': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_basic[Model.F1155-s2-climate.climate_system_s2][idle (mixing valve)] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S2', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': 30.0, + 'target_temp_low': 21.0, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s2', + 'last_changed': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_basic[Model.F1155-s2-climate.climate_system_s2][initial] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S2', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': 30.0, + 'target_temp_low': 21.0, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s2', + 'last_changed': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_basic[Model.F1155-s2-climate.climate_system_s2][off (auto)] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S2', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s2', + 'last_changed': , + 'last_updated': , + 'state': 'auto', + }) +# --- +# name: test_basic[Model.F1155-s2-climate.climate_system_s2][unavailable] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S2', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s2', + 'last_changed': , + 'last_updated': , + 'state': 'auto', + }) +# --- +# name: test_basic[Model.S320-s1-climate.climate_system_s1][cooling] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S1', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': 30.0, + 'target_temp_low': 21.0, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s1', + 'last_changed': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_basic[Model.S320-s1-climate.climate_system_s1][heating (auto)] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S1', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s1', + 'last_changed': , + 'last_updated': , + 'state': 'auto', + }) +# --- +# name: test_basic[Model.S320-s1-climate.climate_system_s1][heating (only)] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S1', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'target_temp_step': 0.5, + 'temperature': 21.0, + }), + 'context': , + 'entity_id': 'climate.climate_system_s1', + 'last_changed': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_basic[Model.S320-s1-climate.climate_system_s1][heating] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 20.5, @@ -25,7 +411,7 @@ 'state': 'heat_cool', }) # --- -# name: test_basic[Model.S320-s1-climate.climate_system_s1][2. idle] +# name: test_basic[Model.S320-s1-climate.climate_system_s1][idle (mixing valve)] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 20.5, @@ -51,3 +437,81 @@ 'state': 'heat_cool', }) # --- +# name: test_basic[Model.S320-s1-climate.climate_system_s1][initial] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S1', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': 30.0, + 'target_temp_low': 21.0, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s1', + 'last_changed': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- +# name: test_basic[Model.S320-s1-climate.climate_system_s1][off (auto)] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S1', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s1', + 'last_changed': , + 'last_updated': , + 'state': 'auto', + }) +# --- +# name: test_basic[Model.S320-s1-climate.climate_system_s1][unavailable] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.5, + 'friendly_name': 'Climate System S1', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 5.0, + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'target_temp_step': 0.5, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.climate_system_s1', + 'last_changed': , + 'last_updated': , + 'state': 'auto', + }) +# --- diff --git a/tests/components/nibe_heatpump/test_climate.py b/tests/components/nibe_heatpump/test_climate.py index d4084ce8123..2b3fe5d8c0e 100644 --- a/tests/components/nibe_heatpump/test_climate.py +++ b/tests/components/nibe_heatpump/test_climate.py @@ -1,13 +1,29 @@ """Test the Nibe Heat Pump config flow.""" from typing import Any -from unittest.mock import patch +from unittest.mock import call, patch -from nibe.coil_groups import CLIMATE_COILGROUPS, UNIT_COILGROUPS +from nibe.coil import CoilData +from nibe.coil_groups import ( + CLIMATE_COILGROUPS, + UNIT_COILGROUPS, + ClimateCoilGroup, + UnitCoilGroup, +) from nibe.heatpump import Model import pytest from syrupy import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, + DOMAIN as PLATFORM_DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from . import MockConnection, async_add_model @@ -20,10 +36,31 @@ async def fixture_single_platform(): yield +def _setup_climate_group( + coils: dict[int, Any], model: Model, climate_id: str +) -> tuple[ClimateCoilGroup, UnitCoilGroup]: + """Initialize coils for a climate group, with some default values.""" + climate = CLIMATE_COILGROUPS[model.series][climate_id] + unit = UNIT_COILGROUPS[model.series]["main"] + + if climate.active_accessory is not None: + coils[climate.active_accessory] = "ON" + coils[climate.current] = 20.5 + coils[climate.setpoint_heat] = 21.0 + coils[climate.setpoint_cool] = 30.0 + coils[climate.mixing_valve_state] = 20 + coils[climate.use_room_sensor] = "ON" + coils[unit.prio] = "OFF" + coils[unit.cooling_with_room_sensor] = "ON" + + return climate, unit + + @pytest.mark.parametrize( ("model", "climate_id", "entity_id"), [ (Model.S320, "s1", "climate.climate_system_s1"), + (Model.F1155, "s2", "climate.climate_system_s2"), ], ) async def test_basic( @@ -37,22 +74,244 @@ async def test_basic( snapshot: SnapshotAssertion, ) -> None: """Test setting of value.""" - climate = CLIMATE_COILGROUPS[model.series][climate_id] - unit = UNIT_COILGROUPS[model.series]["main"] - if climate.active_accessory is not None: - coils[climate.active_accessory] = "ON" - coils[climate.current] = 20.5 - coils[climate.setpoint_heat] = 21.0 - coils[climate.setpoint_cool] = 30.0 - coils[climate.mixing_valve_state] = "ON" - coils[climate.use_room_sensor] = "ON" - coils[unit.prio] = "HEAT" - coils[unit.cooling_with_room_sensor] = "ON" + climate, unit = _setup_climate_group(coils, model, climate_id) await async_add_model(hass, model) - assert hass.states.get(entity_id) == snapshot(name="1. initial") + assert hass.states.get(entity_id) == snapshot(name="initial") - mock_connection.mock_coil_update(unit.prio, "OFF") + mock_connection.mock_coil_update(unit.prio, "COOLING") + assert hass.states.get(entity_id) == snapshot(name="cooling") - assert hass.states.get(entity_id) == snapshot(name="2. idle") + mock_connection.mock_coil_update(unit.prio, "HEAT") + assert hass.states.get(entity_id) == snapshot(name="heating") + + mock_connection.mock_coil_update(climate.mixing_valve_state, 30) + assert hass.states.get(entity_id) == snapshot(name="idle (mixing valve)") + + mock_connection.mock_coil_update(climate.mixing_valve_state, 20) + mock_connection.mock_coil_update(unit.cooling_with_room_sensor, "OFF") + assert hass.states.get(entity_id) == snapshot(name="heating (only)") + + mock_connection.mock_coil_update(climate.use_room_sensor, "OFF") + assert hass.states.get(entity_id) == snapshot(name="heating (auto)") + + mock_connection.mock_coil_update(unit.prio, None) + assert hass.states.get(entity_id) == snapshot(name="off (auto)") + + coils.clear() + assert hass.states.get(entity_id) == snapshot(name="unavailable") + + +@pytest.mark.parametrize( + ("model", "climate_id", "entity_id"), + [ + (Model.F1155, "s2", "climate.climate_system_s2"), + (Model.F1155, "s3", "climate.climate_system_s3"), + ], +) +async def test_active_accessory( + hass: HomeAssistant, + mock_connection: MockConnection, + model: Model, + climate_id: str, + entity_id: str, + coils: dict[int, Any], + entity_registry_enabled_by_default: None, + snapshot: SnapshotAssertion, +) -> None: + """Test climate groups that can be deactivated by configuration.""" + climate, unit = _setup_climate_group(coils, model, climate_id) + + await async_add_model(hass, model) + + assert hass.states.get(entity_id) == snapshot(name="initial") + + mock_connection.mock_coil_update(climate.active_accessory, "OFF") + assert hass.states.get(entity_id) == snapshot(name="unavailable (not supported)") + + +@pytest.mark.parametrize( + ("model", "climate_id", "entity_id"), + [ + (Model.S320, "s1", "climate.climate_system_s1"), + (Model.F1155, "s2", "climate.climate_system_s2"), + ], +) +async def test_set_temperature( + hass: HomeAssistant, + mock_connection: MockConnection, + model: Model, + climate_id: str, + entity_id: str, + coils: dict[int, Any], + entity_registry_enabled_by_default: None, + snapshot: SnapshotAssertion, +) -> None: + """Test setting temperature.""" + climate, _ = _setup_climate_group(coils, model, climate_id) + + await async_add_model(hass, model) + + coil_setpoint_heat = mock_connection.heatpump.get_coil_by_address( + climate.setpoint_heat + ) + coil_setpoint_cool = mock_connection.heatpump.get_coil_by_address( + climate.setpoint_cool + ) + + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TEMPERATURE: 22, + ATTR_HVAC_MODE: HVACMode.HEAT, + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_connection.write_coil.mock_calls == [ + call(CoilData(coil_setpoint_heat, 22)) + ] + mock_connection.write_coil.reset_mock() + + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TEMPERATURE: 22, + ATTR_HVAC_MODE: HVACMode.COOL, + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_connection.write_coil.mock_calls == [ + call(CoilData(coil_setpoint_cool, 22)) + ] + mock_connection.write_coil.reset_mock() + + with pytest.raises(ValueError): + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TEMPERATURE: 22, + }, + blocking=True, + ) + + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_HIGH: 30, + ATTR_TARGET_TEMP_LOW: 22, + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_connection.write_coil.mock_calls == [ + call(CoilData(coil_setpoint_heat, 22)), + call(CoilData(coil_setpoint_cool, 30)), + ] + + mock_connection.write_coil.reset_mock() + + +@pytest.mark.parametrize( + ("hvac_mode", "cooling_with_room_sensor", "use_room_sensor"), + [ + (HVACMode.HEAT_COOL, "ON", "ON"), + (HVACMode.HEAT, "OFF", "ON"), + (HVACMode.AUTO, "OFF", "OFF"), + ], +) +@pytest.mark.parametrize( + ("model", "climate_id", "entity_id"), + [ + (Model.S320, "s1", "climate.climate_system_s1"), + (Model.F1155, "s2", "climate.climate_system_s2"), + ], +) +async def test_set_hvac_mode( + hass: HomeAssistant, + mock_connection: MockConnection, + model: Model, + climate_id: str, + entity_id: str, + cooling_with_room_sensor: str, + use_room_sensor: str, + hvac_mode: HVACMode, + coils: dict[int, Any], + entity_registry_enabled_by_default: None, +) -> None: + """Test setting a hvac mode.""" + climate, unit = _setup_climate_group(coils, model, climate_id) + + await async_add_model(hass, model) + + coil_use_room_sensor = mock_connection.heatpump.get_coil_by_address( + climate.use_room_sensor + ) + coil_cooling_with_room_sensor = mock_connection.heatpump.get_coil_by_address( + unit.cooling_with_room_sensor + ) + + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_HVAC_MODE: hvac_mode, + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_connection.write_coil.mock_calls == [ + call(CoilData(coil_cooling_with_room_sensor, cooling_with_room_sensor)), + call(CoilData(coil_use_room_sensor, use_room_sensor)), + ] + + +@pytest.mark.parametrize( + ("model", "climate_id", "entity_id"), + [ + (Model.S320, "s1", "climate.climate_system_s1"), + (Model.F1155, "s2", "climate.climate_system_s2"), + ], +) +async def test_set_invalid_hvac_mode( + hass: HomeAssistant, + mock_connection: MockConnection, + model: Model, + climate_id: str, + entity_id: str, + coils: dict[int, Any], + entity_registry_enabled_by_default: None, +) -> None: + """Test setting an invalid hvac mode.""" + _setup_climate_group(coils, model, climate_id) + + await async_add_model(hass, model) + + with pytest.raises(ValueError): + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_HVAC_MODE: HVACMode.DRY, + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_connection.write_coil.mock_calls == []