Alexa fan preset_mode support (#50466)

* fan preset_modes

* process preset mode updates from alexa correctly

* add tests

* codecov patch additional tests
This commit is contained in:
Jan Bouwhuis 2021-05-31 20:58:01 +02:00 committed by GitHub
parent 0e7c2cddf7
commit 7403ba1e81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 189 additions and 6 deletions

View file

@ -1155,8 +1155,6 @@ class AlexaPowerLevelController(AlexaCapability):
if self.entity.domain == fan.DOMAIN:
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
return None
class AlexaSecurityPanelController(AlexaCapability):
"""Implements Alexa.SecurityPanelController.
@ -1304,6 +1302,12 @@ class AlexaModeController(AlexaCapability):
if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN):
return f"{fan.ATTR_DIRECTION}.{mode}"
# Fan preset_mode
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None)
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None):
return f"{fan.ATTR_PRESET_MODE}.{mode}"
# Cover Position
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
# Return state instead of position when using ModeController.
@ -1342,6 +1346,17 @@ class AlexaModeController(AlexaCapability):
)
return self._resource.serialize_capability_resources()
# Fan preset_mode
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
self._resource = AlexaModeResource(
[AlexaGlobalCatalog.SETTING_PRESET], False
)
for preset_mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, []):
self._resource.add_mode(
f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode]
)
return self._resource.serialize_capability_resources()
# Cover Position Resources
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
self._resource = AlexaModeResource(

View file

@ -535,6 +535,7 @@ class FanCapabilities(AlexaEntity):
if supported & fan.SUPPORT_SET_SPEED:
yield AlexaPercentageController(self.entity)
yield AlexaPowerLevelController(self.entity)
# The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
yield AlexaRangeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
)
@ -542,6 +543,10 @@ class FanCapabilities(AlexaEntity):
yield AlexaToggleController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
)
if supported & fan.SUPPORT_PRESET_MODE:
yield AlexaModeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
)
if supported & fan.SUPPORT_DIRECTION:
yield AlexaModeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}"

View file

@ -958,6 +958,16 @@ async def async_api_set_mode(hass, config, directive, context):
service = fan.SERVICE_SET_DIRECTION
data[fan.ATTR_DIRECTION] = direction
# Fan preset_mode
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
preset_mode = mode.split(".")[1]
if preset_mode in entity.attributes.get(fan.ATTR_PRESET_MODES):
service = fan.SERVICE_SET_PRESET_MODE
data[fan.ATTR_PRESET_MODE] = preset_mode
else:
msg = f"Entity '{entity.entity_id}' does not support Preset '{preset_mode}'"
raise AlexaInvalidValueError(msg)
# Cover Position
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
position = mode.split(".")[1]

View file

@ -400,6 +400,48 @@ async def test_report_fan_speed_state(hass):
properties.assert_equal("Alexa.RangeController", "rangeValue", 3)
async def test_report_fan_preset_mode(hass):
"""Test ModeController reports fan preset_mode correctly."""
hass.states.async_set(
"fan.preset_mode",
"eco",
{
"friendly_name": "eco enabled fan",
"supported_features": 8,
"preset_mode": "eco",
"preset_modes": ["eco", "smart", "whoosh"],
},
)
properties = await reported_properties(hass, "fan.preset_mode")
properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.eco")
hass.states.async_set(
"fan.preset_mode",
"smart",
{
"friendly_name": "smart enabled fan",
"supported_features": 8,
"preset_mode": "smart",
"preset_modes": ["eco", "smart", "whoosh"],
},
)
properties = await reported_properties(hass, "fan.preset_mode")
properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.smart")
hass.states.async_set(
"fan.preset_mode",
"whoosh",
{
"friendly_name": "whoosh enabled fan",
"supported_features": 8,
"preset_mode": "whoosh",
"preset_modes": ["eco", "smart", "whoosh"],
},
)
properties = await reported_properties(hass, "fan.preset_mode")
properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.whoosh")
async def test_report_fan_oscillating(hass):
"""Test ToggleController reports fan oscillating correctly."""
hass.states.async_set(

View file

@ -846,6 +846,89 @@ async def test_fan_range_off(hass):
)
async def test_preset_mode_fan(hass, caplog):
"""Test fan discovery.
This one has preset modes.
"""
device = (
"fan.test_7",
"off",
{
"friendly_name": "Test fan 7",
"supported_features": 8,
"preset_modes": ["auto", "eco", "smart", "whoosh"],
"preset_mode": "auto",
},
)
appliance = await discovery_test(device, hass)
assert appliance["endpointId"] == "fan#test_7"
assert appliance["displayCategories"][0] == "FAN"
assert appliance["friendlyName"] == "Test fan 7"
capabilities = assert_endpoint_capabilities(
appliance,
"Alexa.EndpointHealth",
"Alexa.ModeController",
"Alexa.PowerController",
"Alexa",
)
range_capability = get_capability(capabilities, "Alexa.ModeController")
assert range_capability is not None
assert range_capability["instance"] == "fan.preset_mode"
properties = range_capability["properties"]
assert properties["nonControllable"] is False
assert {"name": "mode"} in properties["supported"]
capability_resources = range_capability["capabilityResources"]
assert capability_resources is not None
assert {
"@type": "asset",
"value": {"assetId": "Alexa.Setting.Preset"},
} in capability_resources["friendlyNames"]
configuration = range_capability["configuration"]
assert configuration is not None
call, _ = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"fan#test_7",
"fan.set_preset_mode",
hass,
payload={"mode": "preset_mode.eco"},
instance="fan.preset_mode",
)
assert call.data["preset_mode"] == "eco"
call, _ = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"fan#test_7",
"fan.set_preset_mode",
hass,
payload={"mode": "preset_mode.whoosh"},
instance="fan.preset_mode",
)
assert call.data["preset_mode"] == "whoosh"
with pytest.raises(AssertionError):
await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"fan#test_7",
"fan.set_preset_mode",
hass,
payload={"mode": "preset_mode.invalid"},
instance="fan.preset_mode",
)
assert "Entity 'fan.test_7' does not support Preset 'invalid'" in caplog.text
caplog.clear()
async def test_lock(hass):
"""Test lock discovery."""
device = ("lock.test", "off", {"friendly_name": "Test lock"})
@ -2484,7 +2567,7 @@ async def test_alarm_control_panel_disarmed(hass):
properties = ReportedProperties(msg["context"]["properties"])
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
call, msg = await assert_request_calls_service(
_, msg = await assert_request_calls_service(
"Alexa.SecurityPanelController",
"Arm",
"alarm_control_panel#test_1",

View file

@ -50,10 +50,13 @@ async def test_report_state_instance(hass, aioclient_mock):
"off",
{
"friendly_name": "Test fan",
"supported_features": 3,
"speed": "off",
"supported_features": 15,
"speed": None,
"speed_list": ["off", "low", "high"],
"oscillating": False,
"preset_mode": None,
"preset_modes": ["auto", "smart"],
"percentage": None,
},
)
@ -64,10 +67,13 @@ async def test_report_state_instance(hass, aioclient_mock):
"on",
{
"friendly_name": "Test fan",
"supported_features": 3,
"supported_features": 15,
"speed": "high",
"speed_list": ["off", "low", "high"],
"oscillating": True,
"preset_mode": "smart",
"preset_modes": ["auto", "smart"],
"percentage": 90,
},
)
@ -82,11 +88,33 @@ async def test_report_state_instance(hass, aioclient_mock):
assert call_json["event"]["header"]["name"] == "ChangeReport"
change_reports = call_json["event"]["payload"]["change"]["properties"]
checks = 0
for report in change_reports:
if report["name"] == "toggleState":
assert report["value"] == "ON"
assert report["instance"] == "fan.oscillating"
assert report["namespace"] == "Alexa.ToggleController"
checks += 1
if report["name"] == "mode":
assert report["value"] == "preset_mode.smart"
assert report["instance"] == "fan.preset_mode"
assert report["namespace"] == "Alexa.ModeController"
checks += 1
if report["name"] == "percentage":
assert report["value"] == 90
assert report["namespace"] == "Alexa.PercentageController"
checks += 1
if report["name"] == "powerLevel":
assert report["value"] == 90
assert report["namespace"] == "Alexa.PowerLevelController"
checks += 1
if report["name"] == "rangeValue":
assert report["value"] == 2
assert report["instance"] == "fan.speed"
assert report["namespace"] == "Alexa.RangeController"
checks += 1
assert checks == 5
assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan"