mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
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:
parent
132ee972bd
commit
2222a121f4
@ -121,6 +121,7 @@ COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
|
|||||||
COMMAND_THERMOSTAT_SET_MODE = f"{PREFIX_COMMANDS}ThermostatSetMode"
|
COMMAND_THERMOSTAT_SET_MODE = f"{PREFIX_COMMANDS}ThermostatSetMode"
|
||||||
COMMAND_LOCKUNLOCK = f"{PREFIX_COMMANDS}LockUnlock"
|
COMMAND_LOCKUNLOCK = f"{PREFIX_COMMANDS}LockUnlock"
|
||||||
COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed"
|
COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed"
|
||||||
|
COMMAND_FANSPEEDRELATIVE = f"{PREFIX_COMMANDS}SetFanSpeedRelative"
|
||||||
COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes"
|
COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes"
|
||||||
COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput"
|
COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput"
|
||||||
COMMAND_NEXT_INPUT = f"{PREFIX_COMMANDS}NextInput"
|
COMMAND_NEXT_INPUT = f"{PREFIX_COMMANDS}NextInput"
|
||||||
@ -1276,10 +1277,9 @@ class FanSpeedTrait(_Trait):
|
|||||||
reversible = False
|
reversible = False
|
||||||
|
|
||||||
if domain == fan.DOMAIN:
|
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, [])
|
modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, [])
|
||||||
for mode in modes:
|
for mode in modes:
|
||||||
if mode not in self.speed_synonyms:
|
|
||||||
continue
|
|
||||||
speed = {
|
speed = {
|
||||||
"speed_name": mode,
|
"speed_name": mode,
|
||||||
"speed_values": [
|
"speed_values": [
|
||||||
@ -1321,6 +1321,7 @@ class FanSpeedTrait(_Trait):
|
|||||||
if speed is not None:
|
if speed is not None:
|
||||||
response["on"] = speed != fan.SPEED_OFF
|
response["on"] = speed != fan.SPEED_OFF
|
||||||
response["currentFanSpeedSetting"] = speed
|
response["currentFanSpeedSetting"] = speed
|
||||||
|
if percent is not None:
|
||||||
response["currentFanSpeedPercent"] = percent
|
response["currentFanSpeedPercent"] = percent
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -1369,6 +1370,7 @@ class ModesTrait(_Trait):
|
|||||||
commands = [COMMAND_MODES]
|
commands = [COMMAND_MODES]
|
||||||
|
|
||||||
SYNONYMS = {
|
SYNONYMS = {
|
||||||
|
"preset mode": ["preset mode", "mode", "preset"],
|
||||||
"sound mode": ["sound mode", "effects"],
|
"sound mode": ["sound mode", "effects"],
|
||||||
"option": ["option", "setting", "mode", "value"],
|
"option": ["option", "setting", "mode", "value"],
|
||||||
}
|
}
|
||||||
@ -1376,6 +1378,9 @@ class ModesTrait(_Trait):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def supported(domain, features, device_class, _):
|
def supported(domain, features, device_class, _):
|
||||||
"""Test if state is supported."""
|
"""Test if state is supported."""
|
||||||
|
if domain == fan.DOMAIN and features & fan.SUPPORT_PRESET_MODE:
|
||||||
|
return True
|
||||||
|
|
||||||
if domain == input_select.DOMAIN:
|
if domain == input_select.DOMAIN:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -1419,6 +1424,7 @@ class ModesTrait(_Trait):
|
|||||||
modes = []
|
modes = []
|
||||||
|
|
||||||
for domain, attr, name in (
|
for domain, attr, name in (
|
||||||
|
(fan.DOMAIN, fan.ATTR_PRESET_MODES, "preset mode"),
|
||||||
(media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST, "sound mode"),
|
(media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST, "sound mode"),
|
||||||
(input_select.DOMAIN, input_select.ATTR_OPTIONS, "option"),
|
(input_select.DOMAIN, input_select.ATTR_OPTIONS, "option"),
|
||||||
(humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES, "mode"),
|
(humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES, "mode"),
|
||||||
@ -1445,7 +1451,10 @@ class ModesTrait(_Trait):
|
|||||||
response = {}
|
response = {}
|
||||||
mode_settings = {}
|
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:
|
if media_player.ATTR_SOUND_MODE_LIST in attrs:
|
||||||
mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE)
|
mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE)
|
||||||
elif self.state.domain == input_select.DOMAIN:
|
elif self.state.domain == input_select.DOMAIN:
|
||||||
@ -1466,8 +1475,22 @@ class ModesTrait(_Trait):
|
|||||||
"""Execute a SetModes command."""
|
"""Execute a SetModes command."""
|
||||||
settings = params.get("updateModeSettings")
|
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:
|
if self.state.domain == input_select.DOMAIN:
|
||||||
option = params["updateModeSettings"]["option"]
|
option = settings["option"]
|
||||||
await self.hass.services.async_call(
|
await self.hass.services.async_call(
|
||||||
input_select.DOMAIN,
|
input_select.DOMAIN,
|
||||||
input_select.SERVICE_SELECT_OPTION,
|
input_select.SERVICE_SELECT_OPTION,
|
||||||
@ -1508,26 +1531,25 @@ class ModesTrait(_Trait):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.state.domain != media_player.DOMAIN:
|
if self.state.domain == media_player.DOMAIN:
|
||||||
_LOGGER.info(
|
sound_mode = settings.get("sound mode")
|
||||||
"Received an Options command for unrecognised domain %s",
|
if sound_mode:
|
||||||
self.state.domain,
|
await self.hass.services.async_call(
|
||||||
)
|
media_player.DOMAIN,
|
||||||
return
|
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")
|
_LOGGER.info(
|
||||||
|
"Received an Options command for unrecognised domain %s",
|
||||||
if sound_mode:
|
self.state.domain,
|
||||||
await self.hass.services.async_call(
|
)
|
||||||
media_player.DOMAIN,
|
return
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register_trait
|
@register_trait
|
||||||
|
@ -247,14 +247,20 @@ DEMO_DEVICES = [
|
|||||||
{
|
{
|
||||||
"id": "fan.living_room_fan",
|
"id": "fan.living_room_fan",
|
||||||
"name": {"name": "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",
|
"type": "action.devices.types.FAN",
|
||||||
"willReportState": False,
|
"willReportState": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fan.ceiling_fan",
|
"id": "fan.ceiling_fan",
|
||||||
"name": {"name": "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",
|
"type": "action.devices.types.FAN",
|
||||||
"willReportState": False,
|
"willReportState": False,
|
||||||
},
|
},
|
||||||
@ -275,7 +281,10 @@ DEMO_DEVICES = [
|
|||||||
{
|
{
|
||||||
"id": "fan.preset_only_limited_fan",
|
"id": "fan.preset_only_limited_fan",
|
||||||
"name": {"name": "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",
|
"type": "action.devices.types.FAN",
|
||||||
"willReportState": False,
|
"willReportState": False,
|
||||||
},
|
},
|
||||||
|
@ -1429,6 +1429,7 @@ async def test_fan_speed(hass):
|
|||||||
],
|
],
|
||||||
"speed": "low",
|
"speed": "low",
|
||||||
"percentage": 33,
|
"percentage": 33,
|
||||||
|
"percentage_step": 1.0,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
BASIC_CONFIG,
|
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):
|
async def test_openclose_cover(hass):
|
||||||
"""Test OpenClose trait support for cover domain."""
|
"""Test OpenClose trait support for cover domain."""
|
||||||
assert helpers.get_google_type(cover.DOMAIN, None) is not None
|
assert helpers.get_google_type(cover.DOMAIN, None) is not None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user