diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index a710010bb8d..36222902296 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -142,6 +142,7 @@ COMMAND_MEDIA_SEEK_RELATIVE = f"{PREFIX_COMMANDS}mediaSeekRelative" COMMAND_MEDIA_SEEK_TO_POSITION = f"{PREFIX_COMMANDS}mediaSeekToPosition" COMMAND_MEDIA_SHUFFLE = f"{PREFIX_COMMANDS}mediaShuffle" COMMAND_MEDIA_STOP = f"{PREFIX_COMMANDS}mediaStop" +COMMAND_REVERSE = f"{PREFIX_COMMANDS}Reverse" COMMAND_SET_HUMIDITY = f"{PREFIX_COMMANDS}SetHumidity" COMMAND_SELECT_CHANNEL = f"{PREFIX_COMMANDS}selectChannel" @@ -1258,14 +1259,7 @@ class FanSpeedTrait(_Trait): """ name = TRAIT_FANSPEED - commands = [COMMAND_FANSPEED] - - speed_synonyms = { - fan.SPEED_OFF: ["stop", "off"], - fan.SPEED_LOW: ["slow", "low", "slowest", "lowest"], - fan.SPEED_MEDIUM: ["medium", "mid", "middle"], - fan.SPEED_HIGH: ["high", "max", "fast", "highest", "fastest", "maximum"], - } + commands = [COMMAND_FANSPEED, COMMAND_REVERSE] @staticmethod def supported(domain, features, device_class, _): @@ -1280,23 +1274,21 @@ class FanSpeedTrait(_Trait): """Return speed point and modes attributes for a sync request.""" domain = self.state.domain speeds = [] - reversible = False + result = {} if domain == fan.DOMAIN: - # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) - modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, []) - for mode in modes: - speed = { - "speed_name": mode, - "speed_values": [ - {"speed_synonym": self.speed_synonyms.get(mode), "lang": "en"} - ], - } - speeds.append(speed) reversible = bool( self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & fan.SUPPORT_DIRECTION ) + + result.update( + { + "reversible": reversible, + "supportsFanSpeedPercent": True, + } + ) + elif domain == climate.DOMAIN: modes = self.state.attributes.get(climate.ATTR_FAN_MODES, []) for mode in modes: @@ -1306,32 +1298,32 @@ class FanSpeedTrait(_Trait): } speeds.append(speed) - return { - "availableFanSpeeds": {"speeds": speeds, "ordered": True}, - "reversible": reversible, - "supportsFanSpeedPercent": True, - } + result.update( + { + "reversible": False, + "availableFanSpeeds": {"speeds": speeds, "ordered": True}, + } + ) + + return result def query_attributes(self): """Return speed point and modes query attributes.""" + attrs = self.state.attributes domain = self.state.domain response = {} if domain == climate.DOMAIN: - speed = attrs.get(climate.ATTR_FAN_MODE) - if speed is not None: - response["currentFanSpeedSetting"] = speed + speed = attrs.get(climate.ATTR_FAN_MODE) or "off" + response["currentFanSpeedSetting"] = speed + if domain == fan.DOMAIN: - speed = attrs.get(fan.ATTR_SPEED) percent = attrs.get(fan.ATTR_PERCENTAGE) or 0 - if speed is not None: - response["on"] = speed != fan.SPEED_OFF - response["currentFanSpeedSetting"] = speed - if percent is not None: - response["currentFanSpeedPercent"] = percent + response["currentFanSpeedPercent"] = percent + return response - async def execute(self, command, data, params, challenge): + async def execute_fanspeed(self, data, params): """Execute an SetFanSpeed command.""" domain = self.state.domain if domain == climate.DOMAIN: @@ -1345,25 +1337,43 @@ class FanSpeedTrait(_Trait): blocking=True, context=data.context, ) - if domain == fan.DOMAIN: - service_params = { - ATTR_ENTITY_ID: self.state.entity_id, - } - if "fanSpeedPercent" in params: - service = fan.SERVICE_SET_PERCENTAGE - service_params[fan.ATTR_PERCENTAGE] = params["fanSpeedPercent"] - else: - service = fan.SERVICE_SET_SPEED - service_params[fan.ATTR_SPEED] = params["fanSpeed"] + if domain == fan.DOMAIN: await self.hass.services.async_call( fan.DOMAIN, - service, - service_params, + fan.SERVICE_SET_PERCENTAGE, + { + ATTR_ENTITY_ID: self.state.entity_id, + fan.ATTR_PERCENTAGE: params["fanSpeedPercent"], + }, blocking=True, context=data.context, ) + async def execute_reverse(self, data, params): + """Execute a Reverse command.""" + domain = self.state.domain + if domain == fan.DOMAIN: + if self.state.attributes.get(fan.ATTR_DIRECTION) == fan.DIRECTION_FORWARD: + direction = fan.DIRECTION_REVERSE + else: + direction = fan.DIRECTION_FORWARD + + await self.hass.services.async_call( + fan.DOMAIN, + fan.SERVICE_SET_DIRECTION, + {ATTR_ENTITY_ID: self.state.entity_id, fan.ATTR_DIRECTION: direction}, + blocking=True, + context=data.context, + ) + + async def execute(self, command, data, params, challenge): + """Execute a smart home command.""" + if command == COMMAND_FANSPEED: + await self.execute_fanspeed(data, params) + elif command == COMMAND_REVERSE: + await self.execute_reverse(data, params) + @register_trait class ModesTrait(_Trait): diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 11677ed67d1..c57d894c36d 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1461,13 +1461,6 @@ async def test_fan_speed(hass): "fan.living_room_fan", fan.SPEED_HIGH, attributes={ - "speed_list": [ - fan.SPEED_OFF, - fan.SPEED_LOW, - fan.SPEED_MEDIUM, - fan.SPEED_HIGH, - ], - "speed": "low", "percentage": 33, "percentage_step": 1.0, }, @@ -1476,64 +1469,14 @@ async def test_fan_speed(hass): ) assert trt.sync_attributes() == { - "availableFanSpeeds": { - "ordered": True, - "speeds": [ - { - "speed_name": "off", - "speed_values": [{"speed_synonym": ["stop", "off"], "lang": "en"}], - }, - { - "speed_name": "low", - "speed_values": [ - { - "speed_synonym": ["slow", "low", "slowest", "lowest"], - "lang": "en", - } - ], - }, - { - "speed_name": "medium", - "speed_values": [ - {"speed_synonym": ["medium", "mid", "middle"], "lang": "en"} - ], - }, - { - "speed_name": "high", - "speed_values": [ - { - "speed_synonym": [ - "high", - "max", - "fast", - "highest", - "fastest", - "maximum", - ], - "lang": "en", - } - ], - }, - ], - }, "reversible": False, "supportsFanSpeedPercent": True, } assert trt.query_attributes() == { - "currentFanSpeedSetting": "low", - "on": True, "currentFanSpeedPercent": 33, } - assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeed": "medium"}) - - calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_SPEED) - await trt.execute(trait.COMMAND_FANSPEED, BASIC_DATA, {"fanSpeed": "medium"}, {}) - - assert len(calls) == 1 - assert calls[0].data == {"entity_id": "fan.living_room_fan", "speed": "medium"} - assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeedPercent": 10}) calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE) @@ -1543,6 +1486,53 @@ async def test_fan_speed(hass): assert calls[0].data == {"entity_id": "fan.living_room_fan", "percentage": 10} +@pytest.mark.parametrize( + "direction_state,direction_call", + [ + (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE), + (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD), + (None, fan.DIRECTION_FORWARD), + ], +) +async def test_fan_reverse(hass, direction_state, direction_call): + """Test FanSpeed trait speed control support for fan domain.""" + + calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_DIRECTION) + + trt = trait.FanSpeedTrait( + hass, + State( + "fan.living_room_fan", + fan.SPEED_HIGH, + attributes={ + "percentage": 33, + "percentage_step": 1.0, + "direction": direction_state, + "supported_features": fan.SUPPORT_DIRECTION, + }, + ), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == { + "reversible": True, + "supportsFanSpeedPercent": True, + } + + assert trt.query_attributes() == { + "currentFanSpeedPercent": 33, + } + + assert trt.can_execute(trait.COMMAND_REVERSE, params={}) + await trt.execute(trait.COMMAND_REVERSE, BASIC_DATA, {}, {}) + + assert len(calls) == 1 + assert calls[0].data == { + "entity_id": "fan.living_room_fan", + "direction": direction_call, + } + + async def test_climate_fan_speed(hass): """Test FanSpeed trait speed control support for climate domain.""" assert helpers.get_google_type(climate.DOMAIN, None) is not None @@ -1586,7 +1576,6 @@ async def test_climate_fan_speed(hass): ], }, "reversible": False, - "supportsFanSpeedPercent": True, } assert trt.query_attributes() == {