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:
parent
0e7c2cddf7
commit
7403ba1e81
6 changed files with 189 additions and 6 deletions
|
@ -1155,8 +1155,6 @@ class AlexaPowerLevelController(AlexaCapability):
|
||||||
if self.entity.domain == fan.DOMAIN:
|
if self.entity.domain == fan.DOMAIN:
|
||||||
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class AlexaSecurityPanelController(AlexaCapability):
|
class AlexaSecurityPanelController(AlexaCapability):
|
||||||
"""Implements Alexa.SecurityPanelController.
|
"""Implements Alexa.SecurityPanelController.
|
||||||
|
@ -1304,6 +1302,12 @@ class AlexaModeController(AlexaCapability):
|
||||||
if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN):
|
if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN):
|
||||||
return f"{fan.ATTR_DIRECTION}.{mode}"
|
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
|
# Cover Position
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
# Return state instead of position when using ModeController.
|
# Return state instead of position when using ModeController.
|
||||||
|
@ -1342,6 +1346,17 @@ class AlexaModeController(AlexaCapability):
|
||||||
)
|
)
|
||||||
return self._resource.serialize_capability_resources()
|
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
|
# Cover Position Resources
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
self._resource = AlexaModeResource(
|
self._resource = AlexaModeResource(
|
||||||
|
|
|
@ -535,6 +535,7 @@ class FanCapabilities(AlexaEntity):
|
||||||
if supported & fan.SUPPORT_SET_SPEED:
|
if supported & fan.SUPPORT_SET_SPEED:
|
||||||
yield AlexaPercentageController(self.entity)
|
yield AlexaPercentageController(self.entity)
|
||||||
yield AlexaPowerLevelController(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(
|
yield AlexaRangeController(
|
||||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
|
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
|
||||||
)
|
)
|
||||||
|
@ -542,6 +543,10 @@ class FanCapabilities(AlexaEntity):
|
||||||
yield AlexaToggleController(
|
yield AlexaToggleController(
|
||||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
|
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:
|
if supported & fan.SUPPORT_DIRECTION:
|
||||||
yield AlexaModeController(
|
yield AlexaModeController(
|
||||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}"
|
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}"
|
||||||
|
|
|
@ -958,6 +958,16 @@ async def async_api_set_mode(hass, config, directive, context):
|
||||||
service = fan.SERVICE_SET_DIRECTION
|
service = fan.SERVICE_SET_DIRECTION
|
||||||
data[fan.ATTR_DIRECTION] = 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
|
# Cover Position
|
||||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
position = mode.split(".")[1]
|
position = mode.split(".")[1]
|
||||||
|
|
|
@ -400,6 +400,48 @@ async def test_report_fan_speed_state(hass):
|
||||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 3)
|
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):
|
async def test_report_fan_oscillating(hass):
|
||||||
"""Test ToggleController reports fan oscillating correctly."""
|
"""Test ToggleController reports fan oscillating correctly."""
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
|
|
@ -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):
|
async def test_lock(hass):
|
||||||
"""Test lock discovery."""
|
"""Test lock discovery."""
|
||||||
device = ("lock.test", "off", {"friendly_name": "Test lock"})
|
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 = ReportedProperties(msg["context"]["properties"])
|
||||||
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
|
properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY")
|
||||||
|
|
||||||
call, msg = await assert_request_calls_service(
|
_, msg = await assert_request_calls_service(
|
||||||
"Alexa.SecurityPanelController",
|
"Alexa.SecurityPanelController",
|
||||||
"Arm",
|
"Arm",
|
||||||
"alarm_control_panel#test_1",
|
"alarm_control_panel#test_1",
|
||||||
|
|
|
@ -50,10 +50,13 @@ async def test_report_state_instance(hass, aioclient_mock):
|
||||||
"off",
|
"off",
|
||||||
{
|
{
|
||||||
"friendly_name": "Test fan",
|
"friendly_name": "Test fan",
|
||||||
"supported_features": 3,
|
"supported_features": 15,
|
||||||
"speed": "off",
|
"speed": None,
|
||||||
"speed_list": ["off", "low", "high"],
|
"speed_list": ["off", "low", "high"],
|
||||||
"oscillating": False,
|
"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",
|
"on",
|
||||||
{
|
{
|
||||||
"friendly_name": "Test fan",
|
"friendly_name": "Test fan",
|
||||||
"supported_features": 3,
|
"supported_features": 15,
|
||||||
"speed": "high",
|
"speed": "high",
|
||||||
"speed_list": ["off", "low", "high"],
|
"speed_list": ["off", "low", "high"],
|
||||||
"oscillating": True,
|
"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"
|
assert call_json["event"]["header"]["name"] == "ChangeReport"
|
||||||
|
|
||||||
change_reports = call_json["event"]["payload"]["change"]["properties"]
|
change_reports = call_json["event"]["payload"]["change"]["properties"]
|
||||||
|
|
||||||
|
checks = 0
|
||||||
for report in change_reports:
|
for report in change_reports:
|
||||||
if report["name"] == "toggleState":
|
if report["name"] == "toggleState":
|
||||||
assert report["value"] == "ON"
|
assert report["value"] == "ON"
|
||||||
assert report["instance"] == "fan.oscillating"
|
assert report["instance"] == "fan.oscillating"
|
||||||
assert report["namespace"] == "Alexa.ToggleController"
|
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"
|
assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue