Restore fixed step fan speeds for google assistant (#76871)
parent
d2e5d91eba
commit
3eaa1c30af
|
@ -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"],
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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={})
|
||||
|
|
Loading…
Reference in New Issue