Restore fixed step fan speeds for google assistant (#76871)

This commit is contained in:
Joakim Plate 2022-08-18 04:15:48 +02:00 committed by GitHub
parent d2e5d91eba
commit 3eaa1c30af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 191 additions and 2 deletions

View File

@ -186,3 +186,21 @@ SOURCE_CLOUD = "cloud"
SOURCE_LOCAL = "local" SOURCE_LOCAL = "local"
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK} NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK}
FAN_SPEEDS = {
"5/5": ["High", "Max", "Fast", "5"],
"4/5": ["Medium High", "4"],
"3/5": ["Medium", "3"],
"2/5": ["Medium Low", "2"],
"1/5": ["Low", "Min", "Slow", "1"],
"4/4": ["High", "Max", "Fast", "4"],
"3/4": ["Medium High", "3"],
"2/4": ["Medium Low", "2"],
"1/4": ["Low", "Min", "Slow", "1"],
"3/3": ["High", "Max", "Fast", "3"],
"2/3": ["Medium", "2"],
"1/3": ["Low", "Min", "Slow", "1"],
"2/2": ["High", "Max", "Fast", "2"],
"1/2": ["Low", "Min", "Slow", "1"],
"1/1": ["Normal", "1"],
}

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any
from homeassistant.components import ( from homeassistant.components import (
alarm_control_panel, alarm_control_panel,
@ -68,6 +69,10 @@ from homeassistant.const import (
from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.helpers.network import get_url from homeassistant.helpers.network import get_url
from homeassistant.util import color as color_util, dt, temperature as temp_util from homeassistant.util import color as color_util, dt, temperature as temp_util
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
)
from .const import ( from .const import (
CHALLENGE_ACK_NEEDED, CHALLENGE_ACK_NEEDED,
@ -82,6 +87,7 @@ from .const import (
ERR_NOT_SUPPORTED, ERR_NOT_SUPPORTED,
ERR_UNSUPPORTED_INPUT, ERR_UNSUPPORTED_INPUT,
ERR_VALUE_OUT_OF_RANGE, ERR_VALUE_OUT_OF_RANGE,
FAN_SPEEDS,
) )
from .error import ChallengeNeeded, SmartHomeError from .error import ChallengeNeeded, SmartHomeError
@ -157,6 +163,8 @@ COMMAND_CHARGE = f"{PREFIX_COMMANDS}Charge"
TRAITS = [] TRAITS = []
FAN_SPEED_MAX_SPEED_COUNT = 5
def register_trait(trait): def register_trait(trait):
"""Decorate a function to register a trait.""" """Decorate a function to register a trait."""
@ -1359,6 +1367,20 @@ class ArmDisArmTrait(_Trait):
) )
def _get_fan_speed(speed_name: str) -> dict[str, Any]:
"""Return a fan speed synonyms for a speed name."""
speed_synonyms = FAN_SPEEDS.get(speed_name, [f"{speed_name}"])
return {
"speed_name": speed_name,
"speed_values": [
{
"speed_synonym": speed_synonyms,
"lang": "en",
}
],
}
@register_trait @register_trait
class FanSpeedTrait(_Trait): class FanSpeedTrait(_Trait):
"""Trait to control speed of Fan. """Trait to control speed of Fan.
@ -1369,6 +1391,18 @@ class FanSpeedTrait(_Trait):
name = TRAIT_FANSPEED name = TRAIT_FANSPEED
commands = [COMMAND_FANSPEED, COMMAND_REVERSE] commands = [COMMAND_FANSPEED, COMMAND_REVERSE]
def __init__(self, hass, state, config):
"""Initialize a trait for a state."""
super().__init__(hass, state, config)
if state.domain == fan.DOMAIN:
speed_count = min(
FAN_SPEED_MAX_SPEED_COUNT,
round(100 / self.state.attributes.get(fan.ATTR_PERCENTAGE_STEP) or 1.0),
)
self._ordered_speed = [
f"{speed}/{speed_count}" for speed in range(1, speed_count + 1)
]
@staticmethod @staticmethod
def supported(domain, features, device_class, _): def supported(domain, features, device_class, _):
"""Test if state is supported.""" """Test if state is supported."""
@ -1397,6 +1431,18 @@ class FanSpeedTrait(_Trait):
} }
) )
if self._ordered_speed:
result.update(
{
"availableFanSpeeds": {
"speeds": [
_get_fan_speed(speed) for speed in self._ordered_speed
],
"ordered": True,
},
}
)
elif domain == climate.DOMAIN: elif domain == climate.DOMAIN:
modes = self.state.attributes.get(climate.ATTR_FAN_MODES) or [] modes = self.state.attributes.get(climate.ATTR_FAN_MODES) or []
for mode in modes: for mode in modes:
@ -1428,6 +1474,9 @@ class FanSpeedTrait(_Trait):
if domain == fan.DOMAIN: if domain == fan.DOMAIN:
percent = attrs.get(fan.ATTR_PERCENTAGE) or 0 percent = attrs.get(fan.ATTR_PERCENTAGE) or 0
response["currentFanSpeedPercent"] = percent response["currentFanSpeedPercent"] = percent
response["currentFanSpeedSetting"] = percentage_to_ordered_list_item(
self._ordered_speed, percent
)
return response return response
@ -1447,12 +1496,19 @@ class FanSpeedTrait(_Trait):
) )
if domain == fan.DOMAIN: if domain == fan.DOMAIN:
if fan_speed := params.get("fanSpeed"):
fan_speed_percent = ordered_list_item_to_percentage(
self._ordered_speed, fan_speed
)
else:
fan_speed_percent = params.get("fanSpeedPercent")
await self.hass.services.async_call( await self.hass.services.async_call(
fan.DOMAIN, fan.DOMAIN,
fan.SERVICE_SET_PERCENTAGE, fan.SERVICE_SET_PERCENTAGE,
{ {
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
fan.ATTR_PERCENTAGE: params["fanSpeedPercent"], fan.ATTR_PERCENTAGE: fan_speed_percent,
}, },
blocking=not self.config.should_report_state, blocking=not self.config.should_report_state,
context=data.context, context=data.context,

View File

@ -1,6 +1,6 @@
"""Tests for the Google Assistant traits.""" """Tests for the Google Assistant traits."""
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import patch from unittest.mock import ANY, patch
import pytest import pytest
@ -1601,10 +1601,12 @@ async def test_fan_speed(hass):
assert trt.sync_attributes() == { assert trt.sync_attributes() == {
"reversible": False, "reversible": False,
"supportsFanSpeedPercent": True, "supportsFanSpeedPercent": True,
"availableFanSpeeds": ANY,
} }
assert trt.query_attributes() == { assert trt.query_attributes() == {
"currentFanSpeedPercent": 33, "currentFanSpeedPercent": 33,
"currentFanSpeedSetting": ANY,
} }
assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeedPercent": 10}) assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeedPercent": 10})
@ -1616,6 +1618,117 @@ async def test_fan_speed(hass):
assert calls[0].data == {"entity_id": "fan.living_room_fan", "percentage": 10} assert calls[0].data == {"entity_id": "fan.living_room_fan", "percentage": 10}
@pytest.mark.parametrize(
"percentage,percentage_step, speed, speeds, percentage_result",
[
(
33,
1.0,
"2/5",
[
["Low", "Min", "Slow", "1"],
["Medium Low", "2"],
["Medium", "3"],
["Medium High", "4"],
["High", "Max", "Fast", "5"],
],
40,
),
(
40,
1.0,
"2/5",
[
["Low", "Min", "Slow", "1"],
["Medium Low", "2"],
["Medium", "3"],
["Medium High", "4"],
["High", "Max", "Fast", "5"],
],
40,
),
(
33,
100 / 3,
"1/3",
[
["Low", "Min", "Slow", "1"],
["Medium", "2"],
["High", "Max", "Fast", "3"],
],
33,
),
(
20,
100 / 4,
"1/4",
[
["Low", "Min", "Slow", "1"],
["Medium Low", "2"],
["Medium High", "3"],
["High", "Max", "Fast", "4"],
],
25,
),
],
)
async def test_fan_speed_ordered(
hass,
percentage: int,
percentage_step: float,
speed: str,
speeds: list[list[str]],
percentage_result: int,
):
"""Test FanSpeed trait speed control support for fan domain."""
assert helpers.get_google_type(fan.DOMAIN, None) is not None
assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, None, None)
trt = trait.FanSpeedTrait(
hass,
State(
"fan.living_room_fan",
STATE_ON,
attributes={
"percentage": percentage,
"percentage_step": percentage_step,
},
),
BASIC_CONFIG,
)
assert trt.sync_attributes() == {
"reversible": False,
"supportsFanSpeedPercent": True,
"availableFanSpeeds": {
"ordered": True,
"speeds": [
{
"speed_name": f"{idx+1}/{len(speeds)}",
"speed_values": [{"lang": "en", "speed_synonym": x}],
}
for idx, x in enumerate(speeds)
],
},
}
assert trt.query_attributes() == {
"currentFanSpeedPercent": percentage,
"currentFanSpeedSetting": speed,
}
assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeed": speed})
calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE)
await trt.execute(trait.COMMAND_FANSPEED, BASIC_DATA, {"fanSpeed": speed}, {})
assert len(calls) == 1
assert calls[0].data == {
"entity_id": "fan.living_room_fan",
"percentage": percentage_result,
}
@pytest.mark.parametrize( @pytest.mark.parametrize(
"direction_state,direction_call", "direction_state,direction_call",
[ [
@ -1647,10 +1760,12 @@ async def test_fan_reverse(hass, direction_state, direction_call):
assert trt.sync_attributes() == { assert trt.sync_attributes() == {
"reversible": True, "reversible": True,
"supportsFanSpeedPercent": True, "supportsFanSpeedPercent": True,
"availableFanSpeeds": ANY,
} }
assert trt.query_attributes() == { assert trt.query_attributes() == {
"currentFanSpeedPercent": 33, "currentFanSpeedPercent": 33,
"currentFanSpeedSetting": ANY,
} }
assert trt.can_execute(trait.COMMAND_REVERSE, params={}) assert trt.can_execute(trait.COMMAND_REVERSE, params={})