From 44aa531a5177ffeed58d704d7710336a4c078ee4 Mon Sep 17 00:00:00 2001 From: Mike Keesey Date: Mon, 17 Jul 2023 17:12:15 -0600 Subject: [PATCH] Alexa temperature adjustment handle multiple setpoint (#95821) * Alexa temperature adjustment handle multiple setpoint In "auto" mode with many thermostats, the thermostats expose both an upper and lower setpoint representing a range of temperatures. When a temperature delta is sent from Alexa (e.g. "lower by 2 degrees), we need to handle the case where the temperature property is not set, but instead the upper and lower setpoint properties are set. In this case, we adjust those properties via service call instead of the singular value. * Updating tests to fix coverage --- homeassistant/components/alexa/handlers.py | 60 +++++++++--- tests/components/alexa/test_smart_home.py | 101 ++++++++++++++++++++- 2 files changed, 146 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index eb23b09627e..c1b99b017e5 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -857,14 +857,55 @@ async def async_api_adjust_target_temp( temp_delta = temperature_from_object( hass, directive.payload["targetSetpointDelta"], interval=True ) - target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta - - if target_temp < min_temp or target_temp > max_temp: - raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp) - - data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp} response = directive.response() + + current_target_temp_high = entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH) + current_target_temp_low = entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) + if current_target_temp_high and current_target_temp_low: + target_temp_high = float(current_target_temp_high) + temp_delta + if target_temp_high < min_temp or target_temp_high > max_temp: + raise AlexaTempRangeError(hass, target_temp_high, min_temp, max_temp) + + target_temp_low = float(current_target_temp_low) + temp_delta + if target_temp_low < min_temp or target_temp_low > max_temp: + raise AlexaTempRangeError(hass, target_temp_low, min_temp, max_temp) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + climate.ATTR_TARGET_TEMP_HIGH: target_temp_high, + climate.ATTR_TARGET_TEMP_LOW: target_temp_low, + } + + response.add_context_property( + { + "name": "upperSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": target_temp_high, "scale": API_TEMP_UNITS[unit]}, + } + ) + response.add_context_property( + { + "name": "lowerSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": target_temp_low, "scale": API_TEMP_UNITS[unit]}, + } + ) + else: + target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta + + if target_temp < min_temp or target_temp > max_temp: + raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp) + + data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp} + response.add_context_property( + { + "name": "targetSetpoint", + "namespace": "Alexa.ThermostatController", + "value": {"value": target_temp, "scale": API_TEMP_UNITS[unit]}, + } + ) + await hass.services.async_call( entity.domain, climate.SERVICE_SET_TEMPERATURE, @@ -872,13 +913,6 @@ async def async_api_adjust_target_temp( blocking=False, context=context, ) - response.add_context_property( - { - "name": "targetSetpoint", - "namespace": "Alexa.ThermostatController", - "value": {"value": target_temp, "scale": API_TEMP_UNITS[unit]}, - } - ) return response diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index a1f77a9b49b..a2dcdedd470 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2176,8 +2176,8 @@ async def test_thermostat(hass: HomeAssistant) -> None: "cool", { "temperature": 70.0, - "target_temp_high": 80.0, - "target_temp_low": 60.0, + "target_temp_high": None, + "target_temp_low": None, "current_temperature": 75.0, "friendly_name": "Test Thermostat", "supported_features": 1 | 2 | 4 | 128, @@ -2439,6 +2439,103 @@ async def test_thermostat(hass: HomeAssistant) -> None: assert call.data["preset_mode"] == "eco" +async def test_thermostat_dual(hass: HomeAssistant) -> None: + """Test thermostat discovery with auto mode, with upper and lower target temperatures.""" + hass.config.units = US_CUSTOMARY_SYSTEM + device = ( + "climate.test_thermostat", + "auto", + { + "temperature": None, + "target_temp_high": 80.0, + "target_temp_low": 60.0, + "current_temperature": 75.0, + "friendly_name": "Test Thermostat", + "supported_features": 1 | 2 | 4 | 128, + "hvac_modes": ["off", "heat", "cool", "auto", "dry", "fan_only"], + "preset_mode": None, + "preset_modes": ["eco"], + "min_temp": 50, + "max_temp": 90, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "climate#test_thermostat" + assert appliance["displayCategories"][0] == "THERMOSTAT" + assert appliance["friendlyName"] == "Test Thermostat" + + 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", "AUTO") + properties.assert_equal( + "Alexa.ThermostatController", + "upperSetpoint", + {"value": 80.0, "scale": "FAHRENHEIT"}, + ) + properties.assert_equal( + "Alexa.ThermostatController", + "lowerSetpoint", + {"value": 60.0, "scale": "FAHRENHEIT"}, + ) + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 75.0, "scale": "FAHRENHEIT"} + ) + + # Adjust temperature when in auto mode + call, msg = await assert_request_calls_service( + "Alexa.ThermostatController", + "AdjustTargetTemperature", + "climate#test_thermostat", + "climate.set_temperature", + hass, + payload={"targetSetpointDelta": {"value": -5.0, "scale": "KELVIN"}}, + ) + assert call.data["target_temp_high"] == 71.0 + assert call.data["target_temp_low"] == 51.0 + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal( + "Alexa.ThermostatController", + "upperSetpoint", + {"value": 71.0, "scale": "FAHRENHEIT"}, + ) + properties.assert_equal( + "Alexa.ThermostatController", + "lowerSetpoint", + {"value": 51.0, "scale": "FAHRENHEIT"}, + ) + + # Fails if the upper setpoint goes too high + msg = await assert_request_fails( + "Alexa.ThermostatController", + "AdjustTargetTemperature", + "climate#test_thermostat", + "climate.set_temperature", + hass, + payload={"targetSetpointDelta": {"value": 6.0, "scale": "CELSIUS"}}, + ) + assert msg["event"]["payload"]["type"] == "TEMPERATURE_VALUE_OUT_OF_RANGE" + + # Fails if the lower setpoint goes too low + msg = await assert_request_fails( + "Alexa.ThermostatController", + "AdjustTargetTemperature", + "climate#test_thermostat", + "climate.set_temperature", + hass, + payload={"targetSetpointDelta": {"value": -6.0, "scale": "CELSIUS"}}, + ) + assert msg["event"]["payload"]["type"] == "TEMPERATURE_VALUE_OUT_OF_RANGE" + + async def test_exclude_filters(hass: HomeAssistant) -> None: """Test exclusion filters.""" request = get_new_request("Alexa.Discovery", "Discover")