Alexa fix Fan support and cleanup (#56053)
* del PowerLevelController, ena fan PowerController * Use AlexaRangeContoller for speed or default * Update tests * no-else-return * Avoid cases with only one preset_mode * Only report ghost_mode to Alexa - fix bug * Add some tests for patched code * pylint * pylint and tests with one preset_mode * correct ghost preset mode check in test * add tests for RangeController * ghost preset_mode locale agnostic * isort * Update homeassistant/components/alexa/capabilities.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Update homeassistant/components/alexa/entities.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Update homeassistant/components/alexa/entities.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Update homeassistant/components/alexa/entities.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Update homeassistant/components/alexa/entities.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Update entities.py Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
0363c22dd8
commit
e73ca9bd18
7 changed files with 325 additions and 145 deletions
|
@ -48,6 +48,7 @@ from .const import (
|
|||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PRESET_MODE_NA,
|
||||
Inputs,
|
||||
)
|
||||
from .errors import UnsupportedProperty
|
||||
|
@ -391,6 +392,8 @@ class AlexaPowerController(AlexaCapability):
|
|||
|
||||
if self.entity.domain == climate.DOMAIN:
|
||||
is_on = self.entity.state != climate.HVAC_MODE_OFF
|
||||
elif self.entity.domain == fan.DOMAIN:
|
||||
is_on = self.entity.state == fan.STATE_ON
|
||||
elif self.entity.domain == vacuum.DOMAIN:
|
||||
is_on = self.entity.state == vacuum.STATE_CLEANING
|
||||
elif self.entity.domain == timer.DOMAIN:
|
||||
|
@ -1155,9 +1158,6 @@ class AlexaPowerLevelController(AlexaCapability):
|
|||
if name != "powerLevel":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.domain == fan.DOMAIN:
|
||||
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
||||
|
||||
|
||||
class AlexaSecurityPanelController(AlexaCapability):
|
||||
"""Implements Alexa.SecurityPanelController.
|
||||
|
@ -1354,10 +1354,17 @@ class AlexaModeController(AlexaCapability):
|
|||
self._resource = AlexaModeResource(
|
||||
[AlexaGlobalCatalog.SETTING_PRESET], False
|
||||
)
|
||||
for preset_mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, []):
|
||||
preset_modes = self.entity.attributes.get(fan.ATTR_PRESET_MODES, [])
|
||||
for preset_mode in preset_modes:
|
||||
self._resource.add_mode(
|
||||
f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode]
|
||||
)
|
||||
# Fans with a single preset_mode completely break Alexa discovery, add a
|
||||
# fake preset (see issue #53832).
|
||||
if len(preset_modes) == 1:
|
||||
self._resource.add_mode(
|
||||
f"{fan.ATTR_PRESET_MODE}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Cover Position Resources
|
||||
|
@ -1491,6 +1498,13 @@ class AlexaRangeController(AlexaCapability):
|
|||
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION)
|
||||
|
||||
# Fan speed percentage
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported and fan.SUPPORT_SET_SPEED:
|
||||
return self.entity.attributes.get(fan.ATTR_PERCENTAGE)
|
||||
return 100 if self.entity.state == fan.STATE_ON else 0
|
||||
|
||||
# Input Number Value
|
||||
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
return float(self.entity.state)
|
||||
|
@ -1517,28 +1531,16 @@ class AlexaRangeController(AlexaCapability):
|
|||
def capability_resources(self):
|
||||
"""Return capabilityResources object."""
|
||||
|
||||
# Fan Speed Resources
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST]
|
||||
max_value = len(speed_list) - 1
|
||||
# Fan Speed Percentage Resources
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
percentage_step = self.entity.attributes.get(fan.ATTR_PERCENTAGE_STEP)
|
||||
self._resource = AlexaPresetResource(
|
||||
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
||||
labels=["Percentage", AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
||||
min_value=0,
|
||||
max_value=max_value,
|
||||
precision=1,
|
||||
max_value=100,
|
||||
precision=percentage_step if percentage_step else 100,
|
||||
unit=AlexaGlobalCatalog.UNIT_PERCENT,
|
||||
)
|
||||
for index, speed in enumerate(speed_list):
|
||||
labels = []
|
||||
if isinstance(speed, str):
|
||||
labels.append(speed.replace("_", " "))
|
||||
if index == 1:
|
||||
labels.append(AlexaGlobalCatalog.VALUE_MINIMUM)
|
||||
if index == max_value:
|
||||
labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM)
|
||||
|
||||
if len(labels) > 0:
|
||||
self._resource.add_preset(value=index, labels=labels)
|
||||
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Cover Position Resources
|
||||
|
@ -1651,6 +1653,20 @@ class AlexaRangeController(AlexaCapability):
|
|||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
# Fan Speed Percentage
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
lower_labels = [AlexaSemantics.ACTION_LOWER]
|
||||
raise_labels = [AlexaSemantics.ACTION_RAISE]
|
||||
self._semantics = AlexaSemantics()
|
||||
|
||||
self._semantics.add_action_to_directive(
|
||||
lower_labels, "SetRangeValue", {"rangeValue": 0}
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
raise_labels, "SetRangeValue", {"rangeValue": 100}
|
||||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
@ -78,6 +78,9 @@ API_THERMOSTAT_MODES = OrderedDict(
|
|||
API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"}
|
||||
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
|
||||
|
||||
# AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode
|
||||
PRESET_MODE_NA = "-"
|
||||
|
||||
|
||||
class Cause:
|
||||
"""Possible causes for property changes.
|
||||
|
|
|
@ -60,11 +60,9 @@ from .capabilities import (
|
|||
AlexaLockController,
|
||||
AlexaModeController,
|
||||
AlexaMotionSensor,
|
||||
AlexaPercentageController,
|
||||
AlexaPlaybackController,
|
||||
AlexaPlaybackStateReporter,
|
||||
AlexaPowerController,
|
||||
AlexaPowerLevelController,
|
||||
AlexaRangeController,
|
||||
AlexaSceneController,
|
||||
AlexaSecurityPanelController,
|
||||
|
@ -530,23 +528,32 @@ class FanCapabilities(AlexaEntity):
|
|||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
force_range_controller = True
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & fan.SUPPORT_SET_SPEED:
|
||||
yield AlexaPercentageController(self.entity)
|
||||
yield AlexaPowerLevelController(self.entity)
|
||||
if supported & fan.SUPPORT_OSCILLATE:
|
||||
yield AlexaToggleController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
|
||||
)
|
||||
force_range_controller = False
|
||||
if supported & fan.SUPPORT_PRESET_MODE:
|
||||
yield AlexaModeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
|
||||
)
|
||||
force_range_controller = False
|
||||
if supported & fan.SUPPORT_DIRECTION:
|
||||
yield AlexaModeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}"
|
||||
)
|
||||
force_range_controller = False
|
||||
|
||||
# AlexaRangeController controls the Fan Speed Percentage.
|
||||
# For fans which only support on/off, no controller is added. This makes the
|
||||
# fan impossible to turn on or off through Alexa, most likely due to a bug in Alexa.
|
||||
# As a workaround, we add a range controller which can only be set to 0% or 100%.
|
||||
if force_range_controller or supported & fan.SUPPORT_SET_SPEED:
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}"
|
||||
)
|
||||
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
|
|
@ -55,6 +55,7 @@ from .const import (
|
|||
API_THERMOSTAT_MODES_CUSTOM,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PRESET_MODE_NA,
|
||||
Cause,
|
||||
Inputs,
|
||||
)
|
||||
|
@ -123,6 +124,8 @@ async def async_api_turn_on(hass, config, directive, context):
|
|||
service = SERVICE_TURN_ON
|
||||
if domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
elif domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_TURN_ON
|
||||
elif domain == vacuum.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if not supported & vacuum.SUPPORT_TURN_ON and supported & vacuum.SUPPORT_START:
|
||||
|
@ -157,6 +160,8 @@ async def async_api_turn_off(hass, config, directive, context):
|
|||
service = SERVICE_TURN_OFF
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
elif domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
elif domain == vacuum.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if (
|
||||
|
@ -826,48 +831,6 @@ async def async_api_reportstate(hass, config, directive, context):
|
|||
return directive.response(name="StateReport")
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.PowerLevelController", "SetPowerLevel"))
|
||||
async def async_api_set_power_level(hass, config, directive, context):
|
||||
"""Process a SetPowerLevel request."""
|
||||
entity = directive.entity
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
percentage = int(directive.payload["powerLevel"])
|
||||
data[fan.ATTR_PERCENTAGE] = percentage
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.PowerLevelController", "AdjustPowerLevel"))
|
||||
async def async_api_adjust_power_level(hass, config, directive, context):
|
||||
"""Process an AdjustPowerLevel request."""
|
||||
entity = directive.entity
|
||||
percentage_delta = int(directive.payload["powerLevelDelta"])
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
||||
|
||||
# set percentage
|
||||
percentage = min(100, max(0, percentage_delta + current))
|
||||
data[fan.ATTR_PERCENTAGE] = percentage
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.SecurityPanelController", "Arm"))
|
||||
async def async_api_arm(hass, config, directive, context):
|
||||
"""Process a Security Panel Arm request."""
|
||||
|
@ -962,7 +925,9 @@ async def async_api_set_mode(hass, config, directive, context):
|
|||
# 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):
|
||||
if preset_mode != PRESET_MODE_NA and preset_mode in entity.attributes.get(
|
||||
fan.ATTR_PRESET_MODES
|
||||
):
|
||||
service = fan.SERVICE_SET_PRESET_MODE
|
||||
data[fan.ATTR_PRESET_MODE] = preset_mode
|
||||
else:
|
||||
|
@ -1114,6 +1079,19 @@ async def async_api_set_range(hass, config, directive, context):
|
|||
service = cover.SERVICE_SET_COVER_TILT_POSITION
|
||||
data[cover.ATTR_TILT_POSITION] = range_value
|
||||
|
||||
# Fan Speed
|
||||
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
else:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported and fan.SUPPORT_SET_SPEED:
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
data[fan.ATTR_PERCENTAGE] = range_value
|
||||
else:
|
||||
service = fan.SERVICE_TURN_ON
|
||||
|
||||
# Input Number Value
|
||||
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
range_value = float(range_value)
|
||||
|
@ -1201,6 +1179,25 @@ async def async_api_adjust_range(hass, config, directive, context):
|
|||
else:
|
||||
data[cover.ATTR_TILT_POSITION] = tilt_position
|
||||
|
||||
# Fan speed percentage
|
||||
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
percentage_step = entity.attributes.get(fan.ATTR_PERCENTAGE_STEP) or 20
|
||||
range_delta = (
|
||||
int(range_delta * percentage_step)
|
||||
if range_delta_default
|
||||
else int(range_delta)
|
||||
)
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
current = entity.attributes.get(fan.ATTR_PERCENTAGE)
|
||||
if not current:
|
||||
msg = f"Unable to determine {entity.entity_id} current fan speed"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
percentage = response_value = min(100, max(0, range_delta + current))
|
||||
if percentage:
|
||||
data[fan.ATTR_PERCENTAGE] = percentage
|
||||
else:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
|
||||
# Input Number Value
|
||||
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
range_delta = float(range_delta)
|
||||
|
|
|
@ -383,22 +383,39 @@ async def test_report_fan_speed_state(hass):
|
|||
"percentage": 100,
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set(
|
||||
"fan.speed_less_on",
|
||||
"on",
|
||||
{
|
||||
"friendly_name": "Speedless fan on",
|
||||
"supported_features": 0,
|
||||
},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"fan.speed_less_off",
|
||||
"off",
|
||||
{
|
||||
"friendly_name": "Speedless fan off",
|
||||
"supported_features": 0,
|
||||
},
|
||||
)
|
||||
properties = await reported_properties(hass, "fan.off")
|
||||
properties.assert_equal("Alexa.PercentageController", "percentage", 0)
|
||||
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 0)
|
||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
|
||||
|
||||
properties = await reported_properties(hass, "fan.low_speed")
|
||||
properties.assert_equal("Alexa.PercentageController", "percentage", 33)
|
||||
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 33)
|
||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 33)
|
||||
|
||||
properties = await reported_properties(hass, "fan.medium_speed")
|
||||
properties.assert_equal("Alexa.PercentageController", "percentage", 66)
|
||||
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 66)
|
||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 66)
|
||||
|
||||
properties = await reported_properties(hass, "fan.high_speed")
|
||||
properties.assert_equal("Alexa.PercentageController", "percentage", 100)
|
||||
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 100)
|
||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 100)
|
||||
|
||||
properties = await reported_properties(hass, "fan.speed_less_on")
|
||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 100)
|
||||
|
||||
properties = await reported_properties(hass, "fan.speed_less_off")
|
||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
|
||||
|
||||
|
||||
async def test_report_fan_preset_mode(hass):
|
||||
|
@ -442,6 +459,18 @@ async def test_report_fan_preset_mode(hass):
|
|||
properties = await reported_properties(hass, "fan.preset_mode")
|
||||
properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.whoosh")
|
||||
|
||||
hass.states.async_set(
|
||||
"fan.preset_mode",
|
||||
"whoosh",
|
||||
{
|
||||
"friendly_name": "one preset mode fan",
|
||||
"supported_features": 8,
|
||||
"preset_mode": "auto",
|
||||
"preset_modes": ["auto"],
|
||||
},
|
||||
)
|
||||
properties = await reported_properties(hass, "fan.preset_mode")
|
||||
|
||||
|
||||
async def test_report_fan_oscillating(hass):
|
||||
"""Test ToggleController reports fan oscillating correctly."""
|
||||
|
|
|
@ -365,14 +365,42 @@ async def test_fan(hass):
|
|||
assert appliance["endpointId"] == "fan#test_1"
|
||||
assert appliance["displayCategories"][0] == "FAN"
|
||||
assert appliance["friendlyName"] == "Test fan 1"
|
||||
# Alexa.RangeController is added to make a van controllable when no other controllers are available
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"
|
||||
appliance,
|
||||
"Alexa.RangeController",
|
||||
"Alexa.PowerController",
|
||||
"Alexa.EndpointHealth",
|
||||
"Alexa",
|
||||
)
|
||||
|
||||
power_capability = get_capability(capabilities, "Alexa.PowerController")
|
||||
assert "capabilityResources" not in power_capability
|
||||
assert "configuration" not in power_capability
|
||||
|
||||
await assert_power_controller_works(
|
||||
"fan#test_1", "fan.turn_on", "fan.turn_off", hass
|
||||
)
|
||||
|
||||
await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"fan#test_1",
|
||||
"fan.turn_on",
|
||||
hass,
|
||||
payload={"rangeValue": "100"},
|
||||
instance="fan.percentage",
|
||||
)
|
||||
await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"fan#test_1",
|
||||
"fan.turn_off",
|
||||
hass,
|
||||
payload={"rangeValue": "0"},
|
||||
instance="fan.percentage",
|
||||
)
|
||||
|
||||
|
||||
async def test_variable_fan(hass):
|
||||
"""Test fan discovery.
|
||||
|
@ -396,103 +424,133 @@ async def test_variable_fan(hass):
|
|||
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.PercentageController",
|
||||
"Alexa.RangeController",
|
||||
"Alexa.PowerController",
|
||||
"Alexa.PowerLevelController",
|
||||
"Alexa.EndpointHealth",
|
||||
"Alexa",
|
||||
)
|
||||
|
||||
capability = get_capability(capabilities, "Alexa.PercentageController")
|
||||
capability = get_capability(capabilities, "Alexa.RangeController")
|
||||
assert capability is not None
|
||||
|
||||
capability = get_capability(capabilities, "Alexa.PowerController")
|
||||
assert capability is not None
|
||||
|
||||
capability = get_capability(capabilities, "Alexa.PowerLevelController")
|
||||
assert capability is not None
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.PercentageController",
|
||||
"SetPercentage",
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"fan#test_2",
|
||||
"fan.set_percentage",
|
||||
hass,
|
||||
payload={"percentage": "50"},
|
||||
payload={"rangeValue": "50"},
|
||||
instance="fan.percentage",
|
||||
)
|
||||
assert call.data["percentage"] == 50
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.PercentageController",
|
||||
"SetPercentage",
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"fan#test_2",
|
||||
"fan.set_percentage",
|
||||
hass,
|
||||
payload={"percentage": "33"},
|
||||
payload={"rangeValue": "33"},
|
||||
instance="fan.percentage",
|
||||
)
|
||||
assert call.data["percentage"] == 33
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.PercentageController",
|
||||
"SetPercentage",
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
"fan#test_2",
|
||||
"fan.set_percentage",
|
||||
hass,
|
||||
payload={"percentage": "100"},
|
||||
payload={"rangeValue": "100"},
|
||||
instance="fan.percentage",
|
||||
)
|
||||
assert call.data["percentage"] == 100
|
||||
|
||||
await assert_percentage_changes(
|
||||
await assert_range_changes(
|
||||
hass,
|
||||
[(95, "-5"), (100, "5"), (20, "-80"), (66, "-34")],
|
||||
"Alexa.PercentageController",
|
||||
"AdjustPercentage",
|
||||
[
|
||||
(95, -5, False),
|
||||
(100, 5, False),
|
||||
(20, -80, False),
|
||||
(66, -34, False),
|
||||
(80, -1, True),
|
||||
(20, -4, True),
|
||||
],
|
||||
"Alexa.RangeController",
|
||||
"AdjustRangeValue",
|
||||
"fan#test_2",
|
||||
"percentageDelta",
|
||||
"fan.set_percentage",
|
||||
"percentage",
|
||||
"fan.percentage",
|
||||
)
|
||||
await assert_range_changes(
|
||||
hass,
|
||||
[
|
||||
(0, -100, False),
|
||||
],
|
||||
"Alexa.RangeController",
|
||||
"AdjustRangeValue",
|
||||
"fan#test_2",
|
||||
"fan.turn_off",
|
||||
None,
|
||||
"fan.percentage",
|
||||
)
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.PowerLevelController",
|
||||
"SetPowerLevel",
|
||||
"fan#test_2",
|
||||
"fan.set_percentage",
|
||||
hass,
|
||||
payload={"powerLevel": "20"},
|
||||
)
|
||||
assert call.data["percentage"] == 20
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.PowerLevelController",
|
||||
"SetPowerLevel",
|
||||
"fan#test_2",
|
||||
"fan.set_percentage",
|
||||
hass,
|
||||
payload={"powerLevel": "50"},
|
||||
)
|
||||
assert call.data["percentage"] == 50
|
||||
async def test_variable_fan_no_current_speed(hass, caplog):
|
||||
"""Test fan discovery.
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.PowerLevelController",
|
||||
"SetPowerLevel",
|
||||
"fan#test_2",
|
||||
"fan.set_percentage",
|
||||
hass,
|
||||
payload={"powerLevel": "99"},
|
||||
This one has variable speed, but no current speed.
|
||||
"""
|
||||
device = (
|
||||
"fan.test_3",
|
||||
"off",
|
||||
{
|
||||
"friendly_name": "Test fan 3",
|
||||
"supported_features": 1,
|
||||
"percentage": None,
|
||||
},
|
||||
)
|
||||
assert call.data["percentage"] == 99
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
await assert_percentage_changes(
|
||||
hass,
|
||||
[(95, "-5"), (50, "-50"), (20, "-80")],
|
||||
"Alexa.PowerLevelController",
|
||||
"AdjustPowerLevel",
|
||||
"fan#test_2",
|
||||
"powerLevelDelta",
|
||||
"fan.set_percentage",
|
||||
"percentage",
|
||||
assert appliance["endpointId"] == "fan#test_3"
|
||||
assert appliance["displayCategories"][0] == "FAN"
|
||||
assert appliance["friendlyName"] == "Test fan 3"
|
||||
# Alexa.RangeController is added to make a van controllable when no other controllers are available
|
||||
capabilities = assert_endpoint_capabilities(
|
||||
appliance,
|
||||
"Alexa.RangeController",
|
||||
"Alexa.PowerController",
|
||||
"Alexa.EndpointHealth",
|
||||
"Alexa",
|
||||
)
|
||||
capability = get_capability(capabilities, "Alexa.RangeController")
|
||||
assert capability is not None
|
||||
|
||||
capability = get_capability(capabilities, "Alexa.PowerController")
|
||||
assert capability is not None
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
await assert_range_changes(
|
||||
hass,
|
||||
[
|
||||
(20, -5, False),
|
||||
],
|
||||
"Alexa.RangeController",
|
||||
"AdjustRangeValue",
|
||||
"fan#test_3",
|
||||
"fan.set_percentage",
|
||||
"percentage",
|
||||
"fan.percentage",
|
||||
)
|
||||
assert (
|
||||
"Request Alexa.RangeController/AdjustRangeValue error INVALID_VALUE: Unable to determine fan.test_3 current fan speed"
|
||||
in caplog.text
|
||||
)
|
||||
caplog.clear()
|
||||
|
||||
|
||||
async def test_oscillating_fan(hass):
|
||||
|
@ -742,6 +800,78 @@ async def test_preset_mode_fan(hass, caplog):
|
|||
caplog.clear()
|
||||
|
||||
|
||||
async def test_single_preset_mode_fan(hass, caplog):
|
||||
"""Test fan discovery.
|
||||
|
||||
This one has only preset mode.
|
||||
"""
|
||||
device = (
|
||||
"fan.test_8",
|
||||
"off",
|
||||
{
|
||||
"friendly_name": "Test fan 8",
|
||||
"supported_features": 8,
|
||||
"preset_modes": ["auto"],
|
||||
"preset_mode": "auto",
|
||||
},
|
||||
)
|
||||
appliance = await discovery_test(device, hass)
|
||||
|
||||
assert appliance["endpointId"] == "fan#test_8"
|
||||
assert appliance["displayCategories"][0] == "FAN"
|
||||
assert appliance["friendlyName"] == "Test fan 8"
|
||||
|
||||
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_8",
|
||||
"fan.set_preset_mode",
|
||||
hass,
|
||||
payload={"mode": "preset_mode.auto"},
|
||||
instance="fan.preset_mode",
|
||||
)
|
||||
assert call.data["preset_mode"] == "auto"
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
await assert_request_calls_service(
|
||||
"Alexa.ModeController",
|
||||
"SetMode",
|
||||
"fan#test_8",
|
||||
"fan.set_preset_mode",
|
||||
hass,
|
||||
payload={"mode": "preset_mode.-"},
|
||||
instance="fan.preset_mode",
|
||||
)
|
||||
assert "Entity 'fan.test_8' does not support Preset '-'" in caplog.text
|
||||
caplog.clear()
|
||||
|
||||
|
||||
async def test_lock(hass):
|
||||
"""Test lock discovery."""
|
||||
device = ("lock.test", "off", {"friendly_name": "Test lock"})
|
||||
|
@ -1615,7 +1745,8 @@ async def assert_range_changes(
|
|||
call, _ = await assert_request_calls_service(
|
||||
namespace, name, endpoint, service, hass, payload=payload, instance=instance
|
||||
)
|
||||
assert call.data[changed_parameter] == result_range
|
||||
if changed_parameter:
|
||||
assert call.data[changed_parameter] == result_range
|
||||
|
||||
|
||||
async def test_temp_sensor(hass):
|
||||
|
|
|
@ -97,15 +97,12 @@ async def test_report_state_instance(hass, aioclient_mock):
|
|||
assert report["instance"] == "fan.preset_mode"
|
||||
assert report["namespace"] == "Alexa.ModeController"
|
||||
checks += 1
|
||||
if report["name"] == "percentage":
|
||||
if report["name"] == "rangeValue":
|
||||
assert report["value"] == 90
|
||||
assert report["namespace"] == "Alexa.PercentageController"
|
||||
assert report["instance"] == "fan.percentage"
|
||||
assert report["namespace"] == "Alexa.RangeController"
|
||||
checks += 1
|
||||
if report["name"] == "powerLevel":
|
||||
assert report["value"] == 90
|
||||
assert report["namespace"] == "Alexa.PowerLevelController"
|
||||
checks += 1
|
||||
assert checks == 4
|
||||
assert checks == 3
|
||||
|
||||
assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan"
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue