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

pull/76912/head
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"
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
import logging
from typing import Any
from homeassistant.components import (
alarm_control_panel,
@ -68,6 +69,10 @@ from homeassistant.const import (
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.helpers.network import get_url
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 (
CHALLENGE_ACK_NEEDED,
@ -82,6 +87,7 @@ from .const import (
ERR_NOT_SUPPORTED,
ERR_UNSUPPORTED_INPUT,
ERR_VALUE_OUT_OF_RANGE,
FAN_SPEEDS,
)
from .error import ChallengeNeeded, SmartHomeError
@ -157,6 +163,8 @@ COMMAND_CHARGE = f"{PREFIX_COMMANDS}Charge"
TRAITS = []
FAN_SPEED_MAX_SPEED_COUNT = 5
def register_trait(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
class FanSpeedTrait(_Trait):
"""Trait to control speed of Fan.
@ -1369,6 +1391,18 @@ class FanSpeedTrait(_Trait):
name = TRAIT_FANSPEED
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
def supported(domain, features, device_class, _):
"""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:
modes = self.state.attributes.get(climate.ATTR_FAN_MODES) or []
for mode in modes:
@ -1428,6 +1474,9 @@ class FanSpeedTrait(_Trait):
if domain == fan.DOMAIN:
percent = attrs.get(fan.ATTR_PERCENTAGE) or 0
response["currentFanSpeedPercent"] = percent
response["currentFanSpeedSetting"] = percentage_to_ordered_list_item(
self._ordered_speed, percent
)
return response
@ -1447,12 +1496,19 @@ class FanSpeedTrait(_Trait):
)
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(
fan.DOMAIN,
fan.SERVICE_SET_PERCENTAGE,
{
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,
context=data.context,

View File

@ -1,6 +1,6 @@
"""Tests for the Google Assistant traits."""
from datetime import datetime, timedelta
from unittest.mock import patch
from unittest.mock import ANY, patch
import pytest
@ -1601,10 +1601,12 @@ async def test_fan_speed(hass):
assert trt.sync_attributes() == {
"reversible": False,
"supportsFanSpeedPercent": True,
"availableFanSpeeds": ANY,
}
assert trt.query_attributes() == {
"currentFanSpeedPercent": 33,
"currentFanSpeedSetting": ANY,
}
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}
@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(
"direction_state,direction_call",
[
@ -1647,10 +1760,12 @@ async def test_fan_reverse(hass, direction_state, direction_call):
assert trt.sync_attributes() == {
"reversible": True,
"supportsFanSpeedPercent": True,
"availableFanSpeeds": ANY,
}
assert trt.query_attributes() == {
"currentFanSpeedPercent": 33,
"currentFanSpeedSetting": ANY,
}
assert trt.can_execute(trait.COMMAND_REVERSE, params={})