"""Test the MELCloud Home climate platform.""" from collections.abc import Callable from typing import Any from unittest.mock import AsyncMock, patch from aiomelcloudhome import ( ATAFanSpeed, ATAOperationMode, ATAVaneHorizontal, ATAVaneVertical, ATWZoneMode, UserContext, ) import pytest from homeassistant.components.climate import ( ATTR_FAN_MODE, ATTR_HVAC_MODE, ATTR_SWING_HORIZONTAL_MODE, ATTR_SWING_MODE, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, SERVICE_SET_SWING_HORIZONTAL_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, HVACMode, ) from homeassistant.components.melcloud_home.const import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_integration from tests.common import ( MockConfigEntry, SnapshotAssertion, async_load_json_object_fixture, snapshot_platform, ) ATA_ENTITY_ID = "climate.living_room_ac" ATW_ZONE1_ENTITY_ID = "climate.heat_pump_zone_1" ATW_ZONE2_ENTITY_ID = "climate.heat_pump_zone_2" @pytest.mark.usefixtures("mock_melcloud_client") async def test_climate_platform( hass: HomeAssistant, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, ) -> None: """Test all climate entity states and attributes from fixture data.""" with patch( "homeassistant.components.melcloud_home.PLATFORMS", [Platform.CLIMATE], ): await setup_integration(hass, mock_config_entry) await snapshot_platform( hass, entity_registry, snapshot, mock_config_entry.entry_id ) @pytest.mark.parametrize( ("apply_capabilities", "expected_hvac_modes"), [ pytest.param( lambda _: None, [ HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO, HVACMode.DRY, HVACMode.FAN_ONLY, ], id="no_capabilities", ), pytest.param( lambda caps: caps, [ HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO, HVACMode.DRY, HVACMode.FAN_ONLY, ], id="all_modes", ), pytest.param( lambda caps: { **caps, "hasCoolOperationMode": False, "hasAutoOperationMode": False, "hasDryOperationMode": False, "hasFanOperationMode": False, }, [HVACMode.OFF, HVACMode.HEAT], id="heat_only", ), ], ) async def test_ata_hvac_modes( hass: HomeAssistant, mock_melcloud_client: AsyncMock, mock_config_entry: MockConfigEntry, apply_capabilities: Callable[[dict[str, Any]], dict[str, Any] | None], expected_hvac_modes: list[HVACMode], ) -> None: """Test ATA hvac_modes for varying unit capabilities.""" context: dict[str, Any] = await async_load_json_object_fixture( hass, "context.json", DOMAIN ) ata_unit = context["buildings"][0]["airToAirUnits"][0] ata_unit["capabilities"] = apply_capabilities(ata_unit["capabilities"]) mock_melcloud_client.get_context.return_value = UserContext.model_validate(context) await setup_integration(hass, mock_config_entry) assert (state := hass.states.get(ATA_ENTITY_ID)) assert state.attributes["hvac_modes"] == expected_hvac_modes @pytest.mark.parametrize( ("hvac_mode", "arguments"), [ pytest.param(HVACMode.OFF, {"power": False}, id="off"), pytest.param( HVACMode.HEAT, {"power": True, "operation_mode": ATAOperationMode.HEAT}, ), pytest.param( HVACMode.COOL, {"power": True, "operation_mode": ATAOperationMode.COOL}, ), pytest.param( HVACMode.AUTO, {"power": True, "operation_mode": ATAOperationMode.AUTOMATIC}, ), pytest.param( HVACMode.DRY, {"power": True, "operation_mode": ATAOperationMode.DRY}, ), pytest.param( HVACMode.FAN_ONLY, {"power": True, "operation_mode": ATAOperationMode.FAN}, ), ], ) @pytest.mark.usefixtures("mock_melcloud_client") async def test_ata_set_hvac_mode( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, hvac_mode: HVACMode, arguments: dict, ) -> None: """Test setting HVAC mode on an ATA unit.""" await setup_integration(hass, mock_config_entry) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, {ATTR_ENTITY_ID: ATA_ENTITY_ID, ATTR_HVAC_MODE: hvac_mode}, blocking=True, ) mock_melcloud_client.control_ata_unit.assert_called_once_with( "ata-unit-uuid-1", **arguments ) @pytest.mark.parametrize( ("fan_mode", "expected_speed"), [ pytest.param("auto", ATAFanSpeed.AUTO), pytest.param("speed_1", ATAFanSpeed.ONE), pytest.param("speed_2", ATAFanSpeed.TWO), pytest.param("speed_3", ATAFanSpeed.THREE), pytest.param("speed_4", ATAFanSpeed.FOUR), pytest.param("speed_5", ATAFanSpeed.FIVE), ], ) @pytest.mark.usefixtures("mock_melcloud_client") async def test_ata_set_fan_mode( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, fan_mode: str, expected_speed: ATAFanSpeed, ) -> None: """Test setting fan speed on an ATA unit.""" await setup_integration(hass, mock_config_entry) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, {ATTR_ENTITY_ID: ATA_ENTITY_ID, ATTR_FAN_MODE: fan_mode}, blocking=True, ) mock_melcloud_client.control_ata_unit.assert_called_once_with( "ata-unit-uuid-1", set_fan_speed=expected_speed ) @pytest.mark.parametrize( ("swing_mode", "expected_vane"), [ pytest.param("auto", ATAVaneVertical.AUTO), pytest.param("swing", ATAVaneVertical.SWING), pytest.param("position_1", ATAVaneVertical.ONE), pytest.param("position_2", ATAVaneVertical.TWO), pytest.param("position_3", ATAVaneVertical.THREE), pytest.param("position_4", ATAVaneVertical.FOUR), pytest.param("position_5", ATAVaneVertical.FIVE), ], ) @pytest.mark.usefixtures("mock_melcloud_client") async def test_ata_set_swing_mode( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, swing_mode: str, expected_vane: ATAVaneVertical, ) -> None: """Test setting vertical vane direction on an ATA unit.""" await setup_integration(hass, mock_config_entry) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, {ATTR_ENTITY_ID: ATA_ENTITY_ID, ATTR_SWING_MODE: swing_mode}, blocking=True, ) mock_melcloud_client.control_ata_unit.assert_called_once_with( "ata-unit-uuid-1", vane_vertical_direction=expected_vane ) @pytest.mark.parametrize( ("swing_mode", "expected_vane"), [ pytest.param("auto", ATAVaneHorizontal.AUTO), pytest.param("swing", ATAVaneHorizontal.SWING), pytest.param("left", ATAVaneHorizontal.LEFT), pytest.param("left_centre", ATAVaneHorizontal.LEFT_CENTRE), pytest.param("centre", ATAVaneHorizontal.CENTRE), pytest.param("right_centre", ATAVaneHorizontal.RIGHT_CENTRE), pytest.param("right", ATAVaneHorizontal.RIGHT), ], ) @pytest.mark.usefixtures("mock_melcloud_client") async def test_ata_set_swing_horizontal_mode( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, swing_mode: str, expected_vane: ATAVaneHorizontal, ) -> None: """Test setting horizontal vane direction on an ATA unit.""" await setup_integration(hass, mock_config_entry) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_HORIZONTAL_MODE, {ATTR_ENTITY_ID: ATA_ENTITY_ID, ATTR_SWING_HORIZONTAL_MODE: swing_mode}, blocking=True, ) mock_melcloud_client.control_ata_unit.assert_called_once_with( "ata-unit-uuid-1", vane_horizontal_direction=expected_vane ) @pytest.mark.usefixtures("mock_melcloud_client") async def test_ata_set_temperature( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, ) -> None: """Test setting target temperature on an ATA unit.""" await setup_integration(hass, mock_config_entry) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: ATA_ENTITY_ID, ATTR_TEMPERATURE: 22.5}, blocking=True, ) mock_melcloud_client.control_ata_unit.assert_called_once_with( "ata-unit-uuid-1", set_temperature=22.5 ) @pytest.mark.parametrize( ("service", "expected_kwargs"), [ pytest.param(SERVICE_TURN_ON, {"power": True}), pytest.param(SERVICE_TURN_OFF, {"power": False}), ], ) @pytest.mark.usefixtures("mock_melcloud_client") async def test_ata_turn_on_off( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, service: str, expected_kwargs: dict, ) -> None: """Test turning an ATA unit on and off.""" await setup_integration(hass, mock_config_entry) await hass.services.async_call( CLIMATE_DOMAIN, service, {ATTR_ENTITY_ID: ATA_ENTITY_ID}, blocking=True, ) mock_melcloud_client.control_ata_unit.assert_called_once_with( "ata-unit-uuid-1", **expected_kwargs ) @pytest.mark.parametrize( ("entity_id", "hvac_mode", "expected_kwargs"), [ pytest.param( ATW_ZONE1_ENTITY_ID, HVACMode.OFF, {"power": False}, id="zone1_off" ), pytest.param( ATW_ZONE1_ENTITY_ID, HVACMode.HEAT, {"power": True, "operation_mode_zone1": ATWZoneMode.HEAT_ROOM_TEMPERATURE}, id="zone1_heat", ), pytest.param( ATW_ZONE2_ENTITY_ID, HVACMode.HEAT, {"power": True, "operation_mode_zone2": ATWZoneMode.HEAT_ROOM_TEMPERATURE}, id="zone2_heat", ), ], ) @pytest.mark.usefixtures("mock_melcloud_client") async def test_atw_set_hvac_mode( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, entity_id: str, hvac_mode: HVACMode, expected_kwargs: dict, ) -> None: """Test setting HVAC mode on an ATW zone.""" await setup_integration(hass, mock_config_entry) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: hvac_mode}, blocking=True, ) mock_melcloud_client.control_atw_unit.assert_called_once_with( "atw-unit-uuid-1", **expected_kwargs ) @pytest.mark.parametrize( ("entity_id", "expected_kwargs"), [ pytest.param(ATW_ZONE1_ENTITY_ID, {"set_temperature_zone1": 23.0}, id="zone1"), pytest.param(ATW_ZONE2_ENTITY_ID, {"set_temperature_zone2": 23.0}, id="zone2"), ], ) @pytest.mark.usefixtures("mock_melcloud_client") async def test_atw_set_temperature( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, entity_id: str, expected_kwargs: dict, ) -> None: """Test setting target temperature on an ATW zone.""" await setup_integration(hass, mock_config_entry) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 23.0}, blocking=True, ) mock_melcloud_client.control_atw_unit.assert_called_once_with( "atw-unit-uuid-1", **expected_kwargs ) @pytest.mark.parametrize( ("service", "expected_kwargs"), [ pytest.param(SERVICE_TURN_ON, {"power": True}), pytest.param(SERVICE_TURN_OFF, {"power": False}), ], ) @pytest.mark.usefixtures("mock_melcloud_client") async def test_atw_turn_on_off( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, service: str, expected_kwargs: dict, ) -> None: """Test turning an ATW zone on and off.""" await setup_integration(hass, mock_config_entry) await hass.services.async_call( CLIMATE_DOMAIN, service, {ATTR_ENTITY_ID: ATW_ZONE1_ENTITY_ID}, blocking=True, ) mock_melcloud_client.control_atw_unit.assert_called_once_with( "atw-unit-uuid-1", **expected_kwargs ) @pytest.mark.parametrize( ("operation_mode", "expected_min", "expected_max"), [ pytest.param("Cool", 16.0, 31.0, id="cool"), pytest.param("Automatic", 16.0, 31.0, id="auto"), pytest.param("Fan", 7, 35, id="fan_only"), ], ) async def test_ata_temperature_range_by_mode( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, operation_mode: str, expected_min: float, expected_max: float, ) -> None: """Test ATA min/max temperature for cool, auto, and fallback HVAC modes.""" context: dict[str, Any] = await async_load_json_object_fixture( hass, "context.json", DOMAIN ) next( setting for setting in context["buildings"][0]["airToAirUnits"][0]["settings"] if setting["name"] == "OperationMode" )["value"] = operation_mode mock_melcloud_client.get_context.return_value = UserContext.model_validate(context) await setup_integration(hass, mock_config_entry) assert (state := hass.states.get(ATA_ENTITY_ID)) assert state.attributes["min_temp"] == expected_min assert state.attributes["max_temp"] == expected_max async def test_ata_no_capabilities_temperature_range( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, ) -> None: """Test fallback temperature range and hvac_modes when ATA unit has no capabilities.""" context: dict[str, Any] = await async_load_json_object_fixture( hass, "context.json", DOMAIN ) context["buildings"][0]["airToAirUnits"][0]["capabilities"] = None mock_melcloud_client.get_context.return_value = UserContext.model_validate(context) await setup_integration(hass, mock_config_entry) assert (state := hass.states.get(ATA_ENTITY_ID)) assert state.attributes["min_temp"] == 7 assert state.attributes["max_temp"] == 35 assert HVACMode.FAN_ONLY in state.attributes["hvac_modes"] async def test_atw_zone_temperature_range( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_melcloud_client: AsyncMock, ) -> None: """Test ATW zone min/max temperature read from unit capabilities.""" context: dict[str, Any] = await async_load_json_object_fixture( hass, "context.json", DOMAIN ) context["buildings"][0]["airToWaterUnits"][0]["capabilities"].update( { "minSetTemperatureZone1": 10.0, "maxSetTemperatureZone1": 30.0, "minSetTemperatureZone2": 12.0, "maxSetTemperatureZone2": 28.0, } ) mock_melcloud_client.get_context.return_value = UserContext.model_validate(context) await setup_integration(hass, mock_config_entry) assert (state1 := hass.states.get(ATW_ZONE1_ENTITY_ID)) assert state1.attributes["min_temp"] == 10.0 assert state1.attributes["max_temp"] == 30.0 assert (state2 := hass.states.get(ATW_ZONE2_ENTITY_ID)) assert state2.attributes["min_temp"] == 12.0 assert state2.attributes["max_temp"] == 28.0