From d44bfa8e8874f6b3c6727998650884f62c8621c7 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 25 Oct 2019 16:42:21 -0400 Subject: [PATCH] Improved Alexa ThermostatController thermostatMode handling (#28176) * Update ThermostatController to map directives to supported modes and add support for CUSTOM mode. * Removed erroneous config value from test. * Removed unnecessary use of a comprehension by dumbing down comment so pylint could comprehend. * Removed erroneous import variable caused by removing erroneous config value from test. * Removed unnecessary use of a comprehension. * Reverted Removal or erroneous import variable and erroneous config value from test. Apparently need for additional tests outside this component. Whoops. --- .../components/alexa/capabilities.py | 27 ++++++++++++ homeassistant/components/alexa/const.py | 3 +- homeassistant/components/alexa/handlers.py | 22 +++++++++- tests/components/alexa/test_capabilities.py | 23 ++++++++--- tests/components/alexa/test_smart_home.py | 41 +++++++++++++++++-- 5 files changed, 105 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 56a18f1d521..52dde74ff3a 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -736,6 +736,33 @@ class AlexaThermostatController(AlexaCapability): return {"value": temp, "scale": API_TEMP_UNITS[unit]} + def configuration(self): + """Return configuration object. + + Translates climate HVAC_MODES and PRESETS to supported Alexa ThermostatMode Values. + ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM. + """ + supported_modes = [] + hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) + for mode in hvac_modes: + thermostat_mode = API_THERMOSTAT_MODES.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) + + preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES) + for mode in preset_modes: + thermostat_mode = API_THERMOSTAT_PRESETS.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) + + # Return False for supportsScheduling until supported with event listener in handler. + configuration = {"supportsScheduling": False} + + if supported_modes: + configuration["supportedModes"] = supported_modes + + return configuration + class AlexaPowerLevelController(AlexaCapability): """Implements Alexa.PowerLevelController. diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 8d1f0ac95a5..2a5f9a512b3 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -56,9 +56,10 @@ API_THERMOSTAT_MODES = OrderedDict( (climate.HVAC_MODE_AUTO, "AUTO"), (climate.HVAC_MODE_OFF, "OFF"), (climate.HVAC_MODE_FAN_ONLY, "OFF"), - (climate.HVAC_MODE_DRY, "OFF"), + (climate.HVAC_MODE_DRY, "CUSTOM"), ] ) +API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} PERCENTAGE_FAN_MAP = { diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 331990dc4a4..3dadf51509a 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -38,6 +38,7 @@ from homeassistant.util.temperature import convert as convert_temperature from .const import ( API_TEMP_UNITS, + API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause, @@ -767,11 +768,28 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): raise AlexaUnsupportedThermostatModeError(msg) service = climate.SERVICE_SET_PRESET_MODE - data[climate.ATTR_PRESET_MODE] = climate.PRESET_ECO + data[climate.ATTR_PRESET_MODE] = ha_preset + + elif mode == "CUSTOM": + operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) + custom_mode = directive.payload["thermostatMode"]["customName"] + custom_mode = next( + (k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode), + None, + ) + if custom_mode not in operation_list: + msg = ( + f"The requested thermostat mode {mode}: {custom_mode} is not supported" + ) + raise AlexaUnsupportedThermostatModeError(msg) + + service = climate.SERVICE_SET_HVAC_MODE + data[climate.ATTR_HVAC_MODE] = custom_mode else: operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) - ha_mode = next((k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None) + ha_modes = {k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode} + ha_mode = next(iter(set(ha_modes).intersection(operation_list)), None) if ha_mode not in operation_list: msg = f"The requested thermostat mode {mode} is not supported" raise AlexaUnsupportedThermostatModeError(msg) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index be4a2ba4806..89815c72544 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -472,11 +472,7 @@ async def test_report_climate_state(hass): {"value": 34.0, "scale": "CELSIUS"}, ) - for off_modes in ( - climate.HVAC_MODE_OFF, - climate.HVAC_MODE_FAN_ONLY, - climate.HVAC_MODE_DRY, - ): + for off_modes in (climate.HVAC_MODE_OFF, climate.HVAC_MODE_FAN_ONLY): hass.states.async_set( "climate.downstairs", off_modes, @@ -495,6 +491,23 @@ async def test_report_climate_state(hass): {"value": 34.0, "scale": "CELSIUS"}, ) + # assert dry is reported as CUSTOM + hass.states.async_set( + "climate.downstairs", + "dry", + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + hass.states.async_set( "climate.heat", "heat", diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 177d52e83de..00c762103f3 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1267,7 +1267,7 @@ async def test_thermostat(hass): "current_temperature": 75.0, "friendly_name": "Test Thermostat", "supported_features": 1 | 2 | 4 | 128, - "hvac_modes": ["heat", "cool", "auto", "off"], + "hvac_modes": ["off", "heat", "cool", "auto", "dry"], "preset_mode": None, "preset_modes": ["eco"], "min_temp": 50, @@ -1280,7 +1280,7 @@ async def test_thermostat(hass): assert appliance["displayCategories"][0] == "THERMOSTAT" assert appliance["friendlyName"] == "Test Thermostat" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.PowerController", "Alexa.ThermostatController", @@ -1299,6 +1299,15 @@ async def test_thermostat(hass): "Alexa.TemperatureSensor", "temperature", {"value": 75.0, "scale": "FAHRENHEIT"} ) + thermostat_capability = get_capability(capabilities, "Alexa.ThermostatController") + assert thermostat_capability is not None + configuration = thermostat_capability["configuration"] + assert configuration["supportsScheduling"] is False + + supported_modes = ["OFF", "HEAT", "COOL", "AUTO", "ECO", "CUSTOM"] + for mode in supported_modes: + assert mode in configuration["supportedModes"] + call, msg = await assert_request_calls_service( "Alexa.ThermostatController", "SetTargetTemperature", @@ -1447,6 +1456,30 @@ async def test_thermostat(hass): properties = ReportedProperties(msg["context"]["properties"]) properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT") + # Assert we can call custom modes + call, msg = await assert_request_calls_service( + "Alexa.ThermostatController", + "SetThermostatMode", + "climate#test_thermostat", + "climate.set_hvac_mode", + hass, + payload={"thermostatMode": {"value": "CUSTOM", "customName": "DEHUMIDIFY"}}, + ) + assert call.data["hvac_mode"] == "dry" + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + + # assert unsupported custom mode + msg = await assert_request_fails( + "Alexa.ThermostatController", + "SetThermostatMode", + "climate#test_thermostat", + "climate.set_hvac_mode", + hass, + payload={"thermostatMode": {"value": "CUSTOM", "customName": "INVALID"}}, + ) + assert msg["event"]["payload"]["type"] == "UNSUPPORTED_THERMOSTAT_MODE" + msg = await assert_request_fails( "Alexa.ThermostatController", "SetThermostatMode", @@ -1456,7 +1489,6 @@ async def test_thermostat(hass): payload={"thermostatMode": {"value": "INVALID"}}, ) assert msg["event"]["payload"]["type"] == "UNSUPPORTED_THERMOSTAT_MODE" - hass.config.units.temperature_unit = TEMP_CELSIUS call, _ = await assert_request_calls_service( "Alexa.ThermostatController", @@ -1479,6 +1511,9 @@ async def test_thermostat(hass): ) assert call.data["preset_mode"] == "eco" + # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. + hass.config.units.temperature_unit = TEMP_CELSIUS + async def test_exclude_filters(hass): """Test exclusion filters."""