From 712ba2fdca8604d0ed56e8ae84bc657c50bc6cca Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 31 Jan 2024 02:38:32 +0100 Subject: [PATCH] Add alexa PowerController on enabled features for climate entities (#109174) Depend alexa PowerController on enabled features for climate entities --- homeassistant/components/alexa/entities.py | 8 ++ homeassistant/components/alexa/handlers.py | 4 + tests/components/alexa/test_capabilities.py | 52 +++++++- tests/components/alexa/test_smart_home.py | 133 +++++++++++++++++++- 4 files changed, 195 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 939644b4600..ddc0bc70987 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -478,6 +478,14 @@ class ClimateCapabilities(AlexaEntity): self.entity.domain == climate.DOMAIN and climate.HVACMode.OFF in (self.entity.attributes.get(climate.ATTR_HVAC_MODES) or []) + or self.entity.domain == climate.DOMAIN + and ( + supported_features + & ( + climate.ClimateEntityFeature.TURN_ON + | climate.ClimateEntityFeature.TURN_OFF + ) + ) or self.entity.domain == water_heater.DOMAIN and (supported_features & water_heater.WaterHeaterEntityFeature.ON_OFF) ): diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 398c6218193..b5b72bc6dc5 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -178,6 +178,8 @@ async def async_api_turn_on( service = SERVICE_TURN_ON if domain == cover.DOMAIN: service = cover.SERVICE_OPEN_COVER + elif domain == climate.DOMAIN: + service = climate.SERVICE_TURN_ON elif domain == fan.DOMAIN: service = fan.SERVICE_TURN_ON elif domain == humidifier.DOMAIN: @@ -227,6 +229,8 @@ async def async_api_turn_off( service = SERVICE_TURN_OFF if entity.domain == cover.DOMAIN: service = cover.SERVICE_CLOSE_COVER + elif domain == climate.DOMAIN: + service = climate.SERVICE_TURN_OFF elif domain == fan.DOMAIN: service = fan.SERVICE_TURN_OFF elif domain == humidifier.DOMAIN: diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index b83bdb794a8..5011fee8838 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -5,7 +5,11 @@ from unittest.mock import patch import pytest from homeassistant.components.alexa import smart_home -from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE, HVACMode +from homeassistant.components.climate import ( + ATTR_CURRENT_TEMPERATURE, + ClimateEntityFeature, + HVACMode, +) from homeassistant.components.lock import STATE_JAMMED, STATE_LOCKING, STATE_UNLOCKING from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.valve import ValveEntityFeature @@ -923,6 +927,52 @@ async def test_report_climate_state(hass: HomeAssistant) -> None: assert msg["event"]["payload"]["type"] == "INTERNAL_ERROR" +async def test_report_on_off_climate_state(hass: HomeAssistant) -> None: + """Test ThermostatController with on/off features reports state correctly.""" + on_off_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + ) + for auto_modes in (HVACMode.HEAT,): + hass.states.async_set( + "climate.onoff", + auto_modes, + { + "friendly_name": "Climate Downstairs", + "supported_features": on_off_features, + ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.onoff") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT") + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + for off_modes in [HVACMode.OFF]: + hass.states.async_set( + "climate.onoff", + off_modes, + { + "friendly_name": "Climate Downstairs", + "supported_features": on_off_features, + ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.onoff") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "OFF") + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + async def test_report_water_heater_state(hass: HomeAssistant) -> None: """Test ThermostatController also reports state correctly for water heaters.""" for operation_mode in (STATE_ECO, STATE_GAS, STATE_HEAT_PUMP): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index ff8fef43a66..97b8bac4cd1 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -6,6 +6,7 @@ import pytest from homeassistant.components.alexa import smart_home, state_report import homeassistant.components.camera as camera +from homeassistant.components.climate import ClimateEntityFeature from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.vacuum import VacuumEntityFeature @@ -20,7 +21,7 @@ from homeassistant.const import ( from homeassistant.core import Context, Event, HomeAssistant from homeassistant.helpers import entityfilter from homeassistant.setup import async_setup_component -from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM +from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from .test_common import ( MockConfig, @@ -3118,6 +3119,136 @@ async def test_thermostat(hass: HomeAssistant) -> None: assert call.data["preset_mode"] == "eco" +async def test_onoff_thermostat(hass: HomeAssistant) -> None: + """Test onoff thermostat discovery.""" + on_off_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + ) + hass.config.units = METRIC_SYSTEM + device = ( + "climate.test_thermostat", + "cool", + { + "temperature": 20.0, + "target_temp_high": None, + "target_temp_low": None, + "current_temperature": 19.0, + "friendly_name": "Test Thermostat", + "supported_features": on_off_features, + "hvac_modes": ["auto"], + "min_temp": 7, + "max_temp": 30, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "climate#test_thermostat" + assert appliance["displayCategories"][0] == "THERMOSTAT" + assert appliance["friendlyName"] == "Test Thermostat" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.ThermostatController", + "Alexa.TemperatureSensor", + "Alexa.EndpointHealth", + "Alexa", + ) + + properties = await reported_properties(hass, "climate#test_thermostat") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "COOL") + properties.assert_equal( + "Alexa.ThermostatController", + "targetSetpoint", + {"value": 20.0, "scale": "CELSIUS"}, + ) + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 19.0, "scale": "CELSIUS"} + ) + + thermostat_capability = get_capability(capabilities, "Alexa.ThermostatController") + assert thermostat_capability is not None + configuration = thermostat_capability["configuration"] + assert configuration["supportsScheduling"] is False + + supported_modes = ["AUTO"] + for mode in supported_modes: + assert mode in configuration["supportedModes"] + + call, msg = await assert_request_calls_service( + "Alexa.ThermostatController", + "SetTargetTemperature", + "climate#test_thermostat", + "climate.set_temperature", + hass, + payload={"targetSetpoint": {"value": 21.0, "scale": "CELSIUS"}}, + ) + assert call.data["temperature"] == 21.0 + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal( + "Alexa.ThermostatController", + "targetSetpoint", + {"value": 21.0, "scale": "CELSIUS"}, + ) + + msg = await assert_request_fails( + "Alexa.ThermostatController", + "SetTargetTemperature", + "climate#test_thermostat", + "climate.set_temperature", + hass, + payload={"targetSetpoint": {"value": 0.0, "scale": "CELSIUS"}}, + ) + assert msg["event"]["payload"]["type"] == "TEMPERATURE_VALUE_OUT_OF_RANGE" + + await assert_request_calls_service( + "Alexa.PowerController", + "TurnOn", + "climate#test_thermostat", + "climate.turn_on", + hass, + ) + await assert_request_calls_service( + "Alexa.PowerController", + "TurnOff", + "climate#test_thermostat", + "climate.turn_off", + hass, + ) + + # Test the power controller is not enabled when there is no `off` mode + device = ( + "climate.test_thermostat", + "cool", + { + "temperature": 20.0, + "target_temp_high": None, + "target_temp_low": None, + "current_temperature": 19.0, + "friendly_name": "Test Thermostat", + "supported_features": ClimateEntityFeature.TARGET_TEMPERATURE, + "hvac_modes": ["auto"], + "min_temp": 7, + "max_temp": 30, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "climate#test_thermostat" + assert appliance["displayCategories"][0] == "THERMOSTAT" + assert appliance["friendlyName"] == "Test Thermostat" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.ThermostatController", + "Alexa.TemperatureSensor", + "Alexa.EndpointHealth", + "Alexa", + ) + + async def test_water_heater(hass: HomeAssistant) -> None: """Test water_heater discovery.""" hass.config.units = US_CUSTOMARY_SYSTEM