Add support for fan speed percentage and preset modes to google_assistant integration (#50283)

* support relative fan speeds

* fan preset modes

* improve tests

* Revert relative speed code report zero percentage
This commit is contained in:
Jan Bouwhuis 2021-06-02 22:09:22 +02:00 committed by GitHub
parent 132ee972bd
commit 2222a121f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 149 additions and 26 deletions

View file

@ -121,6 +121,7 @@ COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
COMMAND_THERMOSTAT_SET_MODE = f"{PREFIX_COMMANDS}ThermostatSetMode"
COMMAND_LOCKUNLOCK = f"{PREFIX_COMMANDS}LockUnlock"
COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed"
COMMAND_FANSPEEDRELATIVE = f"{PREFIX_COMMANDS}SetFanSpeedRelative"
COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes"
COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput"
COMMAND_NEXT_INPUT = f"{PREFIX_COMMANDS}NextInput"
@ -1276,10 +1277,9 @@ class FanSpeedTrait(_Trait):
reversible = False
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:
if mode not in self.speed_synonyms:
continue
speed = {
"speed_name": mode,
"speed_values": [
@ -1321,6 +1321,7 @@ class FanSpeedTrait(_Trait):
if speed is not None:
response["on"] = speed != fan.SPEED_OFF
response["currentFanSpeedSetting"] = speed
if percent is not None:
response["currentFanSpeedPercent"] = percent
return response
@ -1369,6 +1370,7 @@ class ModesTrait(_Trait):
commands = [COMMAND_MODES]
SYNONYMS = {
"preset mode": ["preset mode", "mode", "preset"],
"sound mode": ["sound mode", "effects"],
"option": ["option", "setting", "mode", "value"],
}
@ -1376,6 +1378,9 @@ class ModesTrait(_Trait):
@staticmethod
def supported(domain, features, device_class, _):
"""Test if state is supported."""
if domain == fan.DOMAIN and features & fan.SUPPORT_PRESET_MODE:
return True
if domain == input_select.DOMAIN:
return True
@ -1419,6 +1424,7 @@ class ModesTrait(_Trait):
modes = []
for domain, attr, name in (
(fan.DOMAIN, fan.ATTR_PRESET_MODES, "preset mode"),
(media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST, "sound mode"),
(input_select.DOMAIN, input_select.ATTR_OPTIONS, "option"),
(humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES, "mode"),
@ -1445,7 +1451,10 @@ class ModesTrait(_Trait):
response = {}
mode_settings = {}
if self.state.domain == media_player.DOMAIN:
if self.state.domain == fan.DOMAIN:
if fan.ATTR_PRESET_MODES in attrs:
mode_settings["preset mode"] = attrs.get(fan.ATTR_PRESET_MODE)
elif self.state.domain == media_player.DOMAIN:
if media_player.ATTR_SOUND_MODE_LIST in attrs:
mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE)
elif self.state.domain == input_select.DOMAIN:
@ -1466,8 +1475,22 @@ class ModesTrait(_Trait):
"""Execute a SetModes command."""
settings = params.get("updateModeSettings")
if self.state.domain == fan.DOMAIN:
preset_mode = settings["preset mode"]
await self.hass.services.async_call(
fan.DOMAIN,
fan.SERVICE_SET_PRESET_MODE,
{
ATTR_ENTITY_ID: self.state.entity_id,
fan.ATTR_PRESET_MODE: preset_mode,
},
blocking=True,
context=data.context,
)
return
if self.state.domain == input_select.DOMAIN:
option = params["updateModeSettings"]["option"]
option = settings["option"]
await self.hass.services.async_call(
input_select.DOMAIN,
input_select.SERVICE_SELECT_OPTION,
@ -1508,26 +1531,25 @@ class ModesTrait(_Trait):
)
return
if self.state.domain != media_player.DOMAIN:
_LOGGER.info(
"Received an Options command for unrecognised domain %s",
self.state.domain,
)
return
if self.state.domain == media_player.DOMAIN:
sound_mode = settings.get("sound mode")
if sound_mode:
await self.hass.services.async_call(
media_player.DOMAIN,
media_player.SERVICE_SELECT_SOUND_MODE,
{
ATTR_ENTITY_ID: self.state.entity_id,
media_player.ATTR_SOUND_MODE: sound_mode,
},
blocking=True,
context=data.context,
)
sound_mode = settings.get("sound mode")
if sound_mode:
await self.hass.services.async_call(
media_player.DOMAIN,
media_player.SERVICE_SELECT_SOUND_MODE,
{
ATTR_ENTITY_ID: self.state.entity_id,
media_player.ATTR_SOUND_MODE: sound_mode,
},
blocking=True,
context=data.context,
)
_LOGGER.info(
"Received an Options command for unrecognised domain %s",
self.state.domain,
)
return
@register_trait

View file

@ -247,14 +247,20 @@ DEMO_DEVICES = [
{
"id": "fan.living_room_fan",
"name": {"name": "Living Room Fan"},
"traits": ["action.devices.traits.FanSpeed", "action.devices.traits.OnOff"],
"traits": [
"action.devices.traits.FanSpeed",
"action.devices.traits.OnOff",
],
"type": "action.devices.types.FAN",
"willReportState": False,
},
{
"id": "fan.ceiling_fan",
"name": {"name": "Ceiling Fan"},
"traits": ["action.devices.traits.FanSpeed", "action.devices.traits.OnOff"],
"traits": [
"action.devices.traits.FanSpeed",
"action.devices.traits.OnOff",
],
"type": "action.devices.types.FAN",
"willReportState": False,
},
@ -275,7 +281,10 @@ DEMO_DEVICES = [
{
"id": "fan.preset_only_limited_fan",
"name": {"name": "Preset Only Limited Fan"},
"traits": ["action.devices.traits.OnOff"],
"traits": [
"action.devices.traits.OnOff",
"action.devices.traits.Modes",
],
"type": "action.devices.types.FAN",
"willReportState": False,
},

View file

@ -1429,6 +1429,7 @@ async def test_fan_speed(hass):
],
"speed": "low",
"percentage": 33,
"percentage_step": 1.0,
},
),
BASIC_CONFIG,
@ -1951,6 +1952,97 @@ async def test_sound_modes(hass):
}
async def test_preset_modes(hass):
"""Test Mode trait for fan preset modes."""
assert helpers.get_google_type(fan.DOMAIN, None) is not None
assert trait.ModesTrait.supported(fan.DOMAIN, fan.SUPPORT_PRESET_MODE, None, None)
trt = trait.ModesTrait(
hass,
State(
"fan.living_room",
STATE_ON,
attributes={
fan.ATTR_PRESET_MODES: ["auto", "whoosh"],
fan.ATTR_PRESET_MODE: "auto",
ATTR_SUPPORTED_FEATURES: fan.SUPPORT_PRESET_MODE,
},
),
BASIC_CONFIG,
)
attribs = trt.sync_attributes()
assert attribs == {
"availableModes": [
{
"name": "preset mode",
"name_values": [
{"name_synonym": ["preset mode", "mode", "preset"], "lang": "en"}
],
"settings": [
{
"setting_name": "auto",
"setting_values": [{"setting_synonym": ["auto"], "lang": "en"}],
},
{
"setting_name": "whoosh",
"setting_values": [
{"setting_synonym": ["whoosh"], "lang": "en"}
],
},
],
"ordered": False,
}
]
}
assert trt.query_attributes() == {
"currentModeSettings": {"preset mode": "auto"},
"on": True,
}
assert trt.can_execute(
trait.COMMAND_MODES,
params={"updateModeSettings": {"preset mode": "auto"}},
)
calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PRESET_MODE)
await trt.execute(
trait.COMMAND_MODES,
BASIC_DATA,
{"updateModeSettings": {"preset mode": "auto"}},
{},
)
assert len(calls) == 1
assert calls[0].data == {
"entity_id": "fan.living_room",
"preset_mode": "auto",
}
async def test_traits_unknown_domains(hass, caplog):
"""Test Mode trait for unsupported domain."""
trt = trait.ModesTrait(
hass,
State(
"switch.living_room",
STATE_ON,
),
BASIC_CONFIG,
)
assert trt.supported("not_supported_domain", False, None, None) is False
await trt.execute(
trait.COMMAND_MODES,
BASIC_DATA,
{"updateModeSettings": {}},
{},
)
assert "Received an Options command for unrecognised domain" in caplog.text
caplog.clear()
async def test_openclose_cover(hass):
"""Test OpenClose trait support for cover domain."""
assert helpers.get_google_type(cover.DOMAIN, None) is not None