From 605b0ceb5fd50df938c19758e093c005ba9ddfe8 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 10 Jan 2020 20:26:37 -0500 Subject: [PATCH] Add support for variable fan speed list length. (#30574) --- .../components/alexa/capabilities.py | 35 +++---- homeassistant/components/alexa/const.py | 14 --- homeassistant/components/alexa/handlers.py | 19 ++-- tests/components/alexa/test_capabilities.py | 37 +++++--- tests/components/alexa/test_smart_home.py | 94 +++++++++++++++---- tests/components/alexa/test_state_report.py | 2 + 6 files changed, 134 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index c6d422f5c2b..d1b7917f263 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -31,7 +31,6 @@ from .const import ( API_THERMOSTAT_PRESETS, DATE_FORMAT, PERCENTAGE_FAN_MAP, - RANGE_FAN_MAP, Inputs, ) from .errors import UnsupportedProperty @@ -1273,8 +1272,12 @@ class AlexaRangeController(AlexaCapability): # Fan Speed if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": - speed = self.entity.attributes.get(fan.ATTR_SPEED) - return RANGE_FAN_MAP.get(speed, 0) + speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST] + speed = self.entity.attributes[fan.ATTR_SPEED] + speed_index = next( + (i for i, v in enumerate(speed_list) if v == speed), None + ) + return speed_index # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": @@ -1302,24 +1305,22 @@ class AlexaRangeController(AlexaCapability): # Fan Speed Resources if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST] + max_value = len(speed_list) - 1 self._resource = AlexaPresetResource( labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED], - min_value=1, - max_value=3, + min_value=0, + max_value=max_value, precision=1, ) - self._resource.add_preset( - value=1, - labels=[AlexaGlobalCatalog.VALUE_LOW, AlexaGlobalCatalog.VALUE_MINIMUM], - ) - self._resource.add_preset(value=2, labels=[AlexaGlobalCatalog.VALUE_MEDIUM]) - self._resource.add_preset( - value=3, - labels=[ - AlexaGlobalCatalog.VALUE_HIGH, - AlexaGlobalCatalog.VALUE_MAXIMUM, - ], - ) + for index, speed in enumerate(speed_list): + labels = [speed.replace("_", " ")] + if index == 1: + labels.append(AlexaGlobalCatalog.VALUE_MINIMUM) + if index == max_value: + labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM) + self._resource.add_preset(value=index, labels=labels) + return self._resource.serialize_capability_resources() # Cover Position Resources diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index f5f19bbf955..e45bcf824bc 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -84,20 +84,6 @@ PERCENTAGE_FAN_MAP = { fan.SPEED_HIGH: 100, } -RANGE_FAN_MAP = { - fan.SPEED_OFF: 0, - fan.SPEED_LOW: 1, - fan.SPEED_MEDIUM: 2, - fan.SPEED_HIGH: 3, -} - -SPEED_FAN_MAP = { - 0: fan.SPEED_OFF, - 1: fan.SPEED_LOW, - 2: fan.SPEED_MEDIUM, - 3: fan.SPEED_HIGH, -} - class Cause: """Possible causes for property changes. diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index b920f11821a..510efe4b610 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -51,8 +51,6 @@ from .const import ( API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_PRESETS, PERCENTAGE_FAN_MAP, - RANGE_FAN_MAP, - SPEED_FAN_MAP, Cause, Inputs, ) @@ -1096,8 +1094,10 @@ async def async_api_set_range(hass, config, directive, context): # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + range_value = int(range_value) service = fan.SERVICE_SET_SPEED - speed = SPEED_FAN_MAP.get(int(range_value)) + speed_list = entity.attributes[fan.ATTR_SPEED_LIST] + speed = next((v for i, v in enumerate(speed_list) if i == range_value), None) if not speed: msg = "Entity does not support value" @@ -1174,9 +1174,16 @@ async def async_api_adjust_range(hass, config, directive, context): if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": range_delta = int(range_delta) service = fan.SERVICE_SET_SPEED - current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0) - speed = SPEED_FAN_MAP.get( - min(3, max(0, range_delta + current_range)), fan.SPEED_OFF + speed_list = entity.attributes[fan.ATTR_SPEED_LIST] + current_speed = entity.attributes[fan.ATTR_SPEED] + current_speed_index = next( + (i for i, v in enumerate(speed_list) if v == current_speed), 0 + ) + new_speed_index = min( + len(speed_list) - 1, max(0, current_speed_index + range_delta) + ) + speed = next( + (v for i, v in enumerate(speed_list) if i == new_speed_index), None ) if speed == fan.SPEED_OFF: diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 9c086e1fc50..f8f4f5f4697 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -315,12 +315,22 @@ async def test_report_fan_speed_state(hass): hass.states.async_set( "fan.off", "off", - {"friendly_name": "Off fan", "speed": "off", "supported_features": 1}, + { + "friendly_name": "Off fan", + "speed": "off", + "supported_features": 1, + "speed_list": ["off", "low", "medium", "high"], + }, ) hass.states.async_set( "fan.low_speed", "on", - {"friendly_name": "Low speed fan", "speed": "low", "supported_features": 1}, + { + "friendly_name": "Low speed fan", + "speed": "low", + "supported_features": 1, + "speed_list": ["off", "low", "medium", "high"], + }, ) hass.states.async_set( "fan.medium_speed", @@ -329,12 +339,18 @@ async def test_report_fan_speed_state(hass): "friendly_name": "Medium speed fan", "speed": "medium", "supported_features": 1, + "speed_list": ["off", "low", "medium", "high"], }, ) hass.states.async_set( "fan.high_speed", "on", - {"friendly_name": "High speed fan", "speed": "high", "supported_features": 1}, + { + "friendly_name": "High speed fan", + "speed": "high", + "supported_features": 1, + "speed_list": ["off", "low", "medium", "high"], + }, ) properties = await reported_properties(hass, "fan.off") @@ -361,25 +377,24 @@ async def test_report_fan_speed_state(hass): async def test_report_fan_oscillating(hass): """Test ToggleController reports fan oscillating correctly.""" hass.states.async_set( - "fan.off", + "fan.oscillating_off", "off", - {"friendly_name": "Off fan", "speed": "off", "supported_features": 3}, + {"friendly_name": "fan oscillating off", "supported_features": 2}, ) hass.states.async_set( - "fan.low_speed", + "fan.oscillating_on", "on", { - "friendly_name": "Low speed fan", - "speed": "low", + "friendly_name": "Fan oscillating on", "oscillating": True, - "supported_features": 3, + "supported_features": 2, }, ) - properties = await reported_properties(hass, "fan.off") + properties = await reported_properties(hass, "fan.oscillating_off") properties.assert_equal("Alexa.ToggleController", "toggleState", "OFF") - properties = await reported_properties(hass, "fan.low_speed") + properties = await reported_properties(hass, "fan.oscillating_on") properties.assert_equal("Alexa.ToggleController", "toggleState", "ON") diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 23100bc2078..dd6faab8e96 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -132,7 +132,7 @@ def get_capability(capabilities, capability_name, instance=None): for capability in capabilities: if instance and capability["instance"] == instance: return capability - elif capability["interface"] == capability_name: + if capability["interface"] == capability_name: return capability return None @@ -497,11 +497,11 @@ async def test_variable_fan(hass): async def test_oscillating_fan(hass): - """Test oscillating fan discovery.""" + """Test oscillating fan with ToggleController.""" device = ( "fan.test_3", "off", - {"friendly_name": "Test fan 3", "supported_features": 3}, + {"friendly_name": "Test fan 3", "supported_features": 2}, ) appliance = await discovery_test(device, hass) @@ -510,10 +510,7 @@ async def test_oscillating_fan(hass): assert appliance["friendlyName"] == "Test fan 3" capabilities = assert_endpoint_capabilities( appliance, - "Alexa.PercentageController", "Alexa.PowerController", - "Alexa.PowerLevelController", - "Alexa.RangeController", "Alexa.ToggleController", "Alexa.EndpointHealth", "Alexa", @@ -558,13 +555,13 @@ async def test_oscillating_fan(hass): async def test_direction_fan(hass): - """Test direction fan discovery.""" + """Test fan direction with modeController.""" device = ( "fan.test_4", "on", { "friendly_name": "Test fan 4", - "supported_features": 5, + "supported_features": 4, "direction": "forward", }, ) @@ -575,10 +572,7 @@ async def test_direction_fan(hass): assert appliance["friendlyName"] == "Test fan 4" capabilities = assert_endpoint_capabilities( appliance, - "Alexa.PercentageController", "Alexa.PowerController", - "Alexa.PowerLevelController", - "Alexa.RangeController", "Alexa.ModeController", "Alexa.EndpointHealth", "Alexa", @@ -667,17 +661,14 @@ async def test_direction_fan(hass): async def test_fan_range(hass): - """Test fan discovery with range controller. - - This one has variable speed. - """ + """Test fan speed with rangeController.""" device = ( "fan.test_5", "off", { "friendly_name": "Test fan 5", "supported_features": 1, - "speed_list": ["low", "medium", "high"], + "speed_list": ["off", "low", "medium", "high", "turbo", "warp_speed"], "speed": "medium", }, ) @@ -701,6 +692,60 @@ async def test_fan_range(hass): assert range_capability is not None assert range_capability["instance"] == "fan.speed" + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.FanSpeed"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 5 + assert supported_range["precision"] == 1 + + presets = configuration["presets"] + assert { + "rangeValue": 0, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "off", "locale": "en-US"}} + ] + }, + } in presets + + assert { + "rangeValue": 1, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "low", "locale": "en-US"}}, + {"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}}, + ] + }, + } in presets + + assert { + "rangeValue": 2, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "medium", "locale": "en-US"}} + ] + }, + } in presets + + assert { + "rangeValue": 5, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "warp speed", "locale": "en-US"}}, + {"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}}, + ] + }, + } in presets + call, _ = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", @@ -712,9 +757,20 @@ async def test_fan_range(hass): ) assert call.data["speed"] == "low" + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "fan#test_5", + "fan.set_speed", + hass, + payload={"rangeValue": "5"}, + instance="fan.speed", + ) + assert call.data["speed"] == "warp_speed" + await assert_range_changes( hass, - [("low", "-1"), ("high", "1"), ("medium", "0")], + [("low", "-1"), ("high", "1"), ("medium", "0"), ("warp_speed", "99")], "Alexa.RangeController", "AdjustRangeValue", "fan#test_5", @@ -733,7 +789,7 @@ async def test_fan_range_off(hass): { "friendly_name": "Test fan 6", "supported_features": 1, - "speed_list": ["low", "medium", "high"], + "speed_list": ["off", "low", "medium", "high"], "speed": "high", }, ) @@ -752,7 +808,7 @@ async def test_fan_range_off(hass): await assert_range_changes( hass, - [("off", "-3")], + [("off", "-3"), ("off", "-99")], "Alexa.RangeController", "AdjustRangeValue", "fan#test_6", diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 4cd2a18a833..42a8ab48279 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -49,6 +49,7 @@ async def test_report_state_instance(hass, aioclient_mock): "friendly_name": "Test fan", "supported_features": 3, "speed": "off", + "speed_list": ["off", "low", "high"], "oscillating": False, }, ) @@ -62,6 +63,7 @@ async def test_report_state_instance(hass, aioclient_mock): "friendly_name": "Test fan", "supported_features": 3, "speed": "high", + "speed_list": ["off", "low", "high"], "oscillating": True, }, )