mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Restore fixed step fan speeds for google assistant (#76871)
This commit is contained in:
parent
d2e5d91eba
commit
3eaa1c30af
@ -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"],
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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={})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user