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:
Jan Bouwhuis 2021-09-24 08:14:45 +02:00 committed by GitHub
parent 0363c22dd8
commit e73ca9bd18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 325 additions and 145 deletions

View File

@ -48,6 +48,7 @@ from .const import (
API_THERMOSTAT_MODES, API_THERMOSTAT_MODES,
API_THERMOSTAT_PRESETS, API_THERMOSTAT_PRESETS,
DATE_FORMAT, DATE_FORMAT,
PRESET_MODE_NA,
Inputs, Inputs,
) )
from .errors import UnsupportedProperty from .errors import UnsupportedProperty
@ -391,6 +392,8 @@ class AlexaPowerController(AlexaCapability):
if self.entity.domain == climate.DOMAIN: if self.entity.domain == climate.DOMAIN:
is_on = self.entity.state != climate.HVAC_MODE_OFF 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: elif self.entity.domain == vacuum.DOMAIN:
is_on = self.entity.state == vacuum.STATE_CLEANING is_on = self.entity.state == vacuum.STATE_CLEANING
elif self.entity.domain == timer.DOMAIN: elif self.entity.domain == timer.DOMAIN:
@ -1155,9 +1158,6 @@ class AlexaPowerLevelController(AlexaCapability):
if name != "powerLevel": if name != "powerLevel":
raise UnsupportedProperty(name) raise UnsupportedProperty(name)
if self.entity.domain == fan.DOMAIN:
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
class AlexaSecurityPanelController(AlexaCapability): class AlexaSecurityPanelController(AlexaCapability):
"""Implements Alexa.SecurityPanelController. """Implements Alexa.SecurityPanelController.
@ -1354,10 +1354,17 @@ class AlexaModeController(AlexaCapability):
self._resource = AlexaModeResource( self._resource = AlexaModeResource(
[AlexaGlobalCatalog.SETTING_PRESET], False [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( self._resource.add_mode(
f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_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() return self._resource.serialize_capability_resources()
# Cover Position Resources # Cover Position Resources
@ -1491,6 +1498,13 @@ class AlexaRangeController(AlexaCapability):
if self.instance == f"{cover.DOMAIN}.tilt": if self.instance == f"{cover.DOMAIN}.tilt":
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION) 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 # Input Number Value
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
return float(self.entity.state) return float(self.entity.state)
@ -1517,28 +1531,16 @@ class AlexaRangeController(AlexaCapability):
def capability_resources(self): def capability_resources(self):
"""Return capabilityResources object.""" """Return capabilityResources object."""
# Fan Speed Resources # Fan Speed Percentage Resources
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST] percentage_step = self.entity.attributes.get(fan.ATTR_PERCENTAGE_STEP)
max_value = len(speed_list) - 1
self._resource = AlexaPresetResource( self._resource = AlexaPresetResource(
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED], labels=["Percentage", AlexaGlobalCatalog.SETTING_FAN_SPEED],
min_value=0, min_value=0,
max_value=max_value, max_value=100,
precision=1, 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() return self._resource.serialize_capability_resources()
# Cover Position Resources # Cover Position Resources
@ -1651,6 +1653,20 @@ class AlexaRangeController(AlexaCapability):
) )
return self._semantics.serialize_semantics() 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 return None

View File

@ -78,6 +78,9 @@ API_THERMOSTAT_MODES = OrderedDict(
API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"} API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"}
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} 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: class Cause:
"""Possible causes for property changes. """Possible causes for property changes.

View File

@ -60,11 +60,9 @@ from .capabilities import (
AlexaLockController, AlexaLockController,
AlexaModeController, AlexaModeController,
AlexaMotionSensor, AlexaMotionSensor,
AlexaPercentageController,
AlexaPlaybackController, AlexaPlaybackController,
AlexaPlaybackStateReporter, AlexaPlaybackStateReporter,
AlexaPowerController, AlexaPowerController,
AlexaPowerLevelController,
AlexaRangeController, AlexaRangeController,
AlexaSceneController, AlexaSceneController,
AlexaSecurityPanelController, AlexaSecurityPanelController,
@ -530,23 +528,32 @@ class FanCapabilities(AlexaEntity):
def interfaces(self): def interfaces(self):
"""Yield the supported interfaces.""" """Yield the supported interfaces."""
yield AlexaPowerController(self.entity) yield AlexaPowerController(self.entity)
force_range_controller = True
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) 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: if supported & fan.SUPPORT_OSCILLATE:
yield AlexaToggleController( yield AlexaToggleController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}" self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
) )
force_range_controller = False
if supported & fan.SUPPORT_PRESET_MODE: if supported & fan.SUPPORT_PRESET_MODE:
yield AlexaModeController( yield AlexaModeController(
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}" self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
) )
force_range_controller = False
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}"
) )
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 AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.hass) yield Alexa(self.hass)

View File

@ -55,6 +55,7 @@ from .const import (
API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_MODES_CUSTOM,
API_THERMOSTAT_PRESETS, API_THERMOSTAT_PRESETS,
DATE_FORMAT, DATE_FORMAT,
PRESET_MODE_NA,
Cause, Cause,
Inputs, Inputs,
) )
@ -123,6 +124,8 @@ async def async_api_turn_on(hass, config, directive, context):
service = SERVICE_TURN_ON service = SERVICE_TURN_ON
if domain == cover.DOMAIN: if domain == cover.DOMAIN:
service = cover.SERVICE_OPEN_COVER service = cover.SERVICE_OPEN_COVER
elif domain == fan.DOMAIN:
service = fan.SERVICE_TURN_ON
elif domain == vacuum.DOMAIN: elif domain == vacuum.DOMAIN:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if not supported & vacuum.SUPPORT_TURN_ON and supported & vacuum.SUPPORT_START: 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 service = SERVICE_TURN_OFF
if entity.domain == cover.DOMAIN: if entity.domain == cover.DOMAIN:
service = cover.SERVICE_CLOSE_COVER service = cover.SERVICE_CLOSE_COVER
elif domain == fan.DOMAIN:
service = fan.SERVICE_TURN_OFF
elif domain == vacuum.DOMAIN: elif domain == vacuum.DOMAIN:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if ( if (
@ -826,48 +831,6 @@ async def async_api_reportstate(hass, config, directive, context):
return directive.response(name="StateReport") 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")) @HANDLERS.register(("Alexa.SecurityPanelController", "Arm"))
async def async_api_arm(hass, config, directive, context): async def async_api_arm(hass, config, directive, context):
"""Process a Security Panel Arm request.""" """Process a Security Panel Arm request."""
@ -962,7 +925,9 @@ async def async_api_set_mode(hass, config, directive, context):
# Fan preset_mode # Fan preset_mode
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}": elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
preset_mode = mode.split(".")[1] 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 service = fan.SERVICE_SET_PRESET_MODE
data[fan.ATTR_PRESET_MODE] = preset_mode data[fan.ATTR_PRESET_MODE] = preset_mode
else: else:
@ -1114,6 +1079,19 @@ async def async_api_set_range(hass, config, directive, context):
service = cover.SERVICE_SET_COVER_TILT_POSITION service = cover.SERVICE_SET_COVER_TILT_POSITION
data[cover.ATTR_TILT_POSITION] = range_value 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 # Input Number Value
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
range_value = float(range_value) range_value = float(range_value)
@ -1201,6 +1179,25 @@ async def async_api_adjust_range(hass, config, directive, context):
else: else:
data[cover.ATTR_TILT_POSITION] = tilt_position 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 # Input Number Value
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
range_delta = float(range_delta) range_delta = float(range_delta)

View File

@ -383,22 +383,39 @@ async def test_report_fan_speed_state(hass):
"percentage": 100, "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 = await reported_properties(hass, "fan.off")
properties.assert_equal("Alexa.PercentageController", "percentage", 0) properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 0)
properties = await reported_properties(hass, "fan.low_speed") properties = await reported_properties(hass, "fan.low_speed")
properties.assert_equal("Alexa.PercentageController", "percentage", 33) properties.assert_equal("Alexa.RangeController", "rangeValue", 33)
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 33)
properties = await reported_properties(hass, "fan.medium_speed") properties = await reported_properties(hass, "fan.medium_speed")
properties.assert_equal("Alexa.PercentageController", "percentage", 66) properties.assert_equal("Alexa.RangeController", "rangeValue", 66)
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 66)
properties = await reported_properties(hass, "fan.high_speed") properties = await reported_properties(hass, "fan.high_speed")
properties.assert_equal("Alexa.PercentageController", "percentage", 100) properties.assert_equal("Alexa.RangeController", "rangeValue", 100)
properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 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): 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 = await reported_properties(hass, "fan.preset_mode")
properties.assert_equal("Alexa.ModeController", "mode", "preset_mode.whoosh") 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): async def test_report_fan_oscillating(hass):
"""Test ToggleController reports fan oscillating correctly.""" """Test ToggleController reports fan oscillating correctly."""

View File

@ -365,14 +365,42 @@ async def test_fan(hass):
assert appliance["endpointId"] == "fan#test_1" assert appliance["endpointId"] == "fan#test_1"
assert appliance["displayCategories"][0] == "FAN" assert appliance["displayCategories"][0] == "FAN"
assert appliance["friendlyName"] == "Test fan 1" 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( 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") power_capability = get_capability(capabilities, "Alexa.PowerController")
assert "capabilityResources" not in power_capability assert "capabilityResources" not in power_capability
assert "configuration" 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): async def test_variable_fan(hass):
"""Test fan discovery. """Test fan discovery.
@ -396,103 +424,133 @@ async def test_variable_fan(hass):
capabilities = assert_endpoint_capabilities( capabilities = assert_endpoint_capabilities(
appliance, appliance,
"Alexa.PercentageController", "Alexa.RangeController",
"Alexa.PowerController", "Alexa.PowerController",
"Alexa.PowerLevelController",
"Alexa.EndpointHealth", "Alexa.EndpointHealth",
"Alexa", "Alexa",
) )
capability = get_capability(capabilities, "Alexa.PercentageController") capability = get_capability(capabilities, "Alexa.RangeController")
assert capability is not None assert capability is not None
capability = get_capability(capabilities, "Alexa.PowerController") capability = get_capability(capabilities, "Alexa.PowerController")
assert capability is not None assert capability is not None
capability = get_capability(capabilities, "Alexa.PowerLevelController")
assert capability is not None
call, _ = await assert_request_calls_service( call, _ = await assert_request_calls_service(
"Alexa.PercentageController", "Alexa.RangeController",
"SetPercentage", "SetRangeValue",
"fan#test_2", "fan#test_2",
"fan.set_percentage", "fan.set_percentage",
hass, hass,
payload={"percentage": "50"}, payload={"rangeValue": "50"},
instance="fan.percentage",
) )
assert call.data["percentage"] == 50 assert call.data["percentage"] == 50
call, _ = await assert_request_calls_service( call, _ = await assert_request_calls_service(
"Alexa.PercentageController", "Alexa.RangeController",
"SetPercentage", "SetRangeValue",
"fan#test_2", "fan#test_2",
"fan.set_percentage", "fan.set_percentage",
hass, hass,
payload={"percentage": "33"}, payload={"rangeValue": "33"},
instance="fan.percentage",
) )
assert call.data["percentage"] == 33 assert call.data["percentage"] == 33
call, _ = await assert_request_calls_service( call, _ = await assert_request_calls_service(
"Alexa.PercentageController", "Alexa.RangeController",
"SetPercentage", "SetRangeValue",
"fan#test_2", "fan#test_2",
"fan.set_percentage", "fan.set_percentage",
hass, hass,
payload={"percentage": "100"}, payload={"rangeValue": "100"},
instance="fan.percentage",
) )
assert call.data["percentage"] == 100 assert call.data["percentage"] == 100
await assert_percentage_changes( await assert_range_changes(
hass, hass,
[(95, "-5"), (100, "5"), (20, "-80"), (66, "-34")], [
"Alexa.PercentageController", (95, -5, False),
"AdjustPercentage", (100, 5, False),
(20, -80, False),
(66, -34, False),
(80, -1, True),
(20, -4, True),
],
"Alexa.RangeController",
"AdjustRangeValue",
"fan#test_2", "fan#test_2",
"percentageDelta",
"fan.set_percentage", "fan.set_percentage",
"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( async def test_variable_fan_no_current_speed(hass, caplog):
"Alexa.PowerLevelController", """Test fan discovery.
"SetPowerLevel",
"fan#test_2",
"fan.set_percentage",
hass,
payload={"powerLevel": "50"},
)
assert call.data["percentage"] == 50
call, _ = await assert_request_calls_service( This one has variable speed, but no current speed.
"Alexa.PowerLevelController", """
"SetPowerLevel", device = (
"fan#test_2", "fan.test_3",
"fan.set_percentage", "off",
hass, {
payload={"powerLevel": "99"}, "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( assert appliance["endpointId"] == "fan#test_3"
hass, assert appliance["displayCategories"][0] == "FAN"
[(95, "-5"), (50, "-50"), (20, "-80")], assert appliance["friendlyName"] == "Test fan 3"
"Alexa.PowerLevelController", # Alexa.RangeController is added to make a van controllable when no other controllers are available
"AdjustPowerLevel", capabilities = assert_endpoint_capabilities(
"fan#test_2", appliance,
"powerLevelDelta", "Alexa.RangeController",
"fan.set_percentage", "Alexa.PowerController",
"percentage", "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): async def test_oscillating_fan(hass):
@ -742,6 +800,78 @@ async def test_preset_mode_fan(hass, caplog):
caplog.clear() 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): 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"})
@ -1615,7 +1745,8 @@ async def assert_range_changes(
call, _ = await assert_request_calls_service( call, _ = await assert_request_calls_service(
namespace, name, endpoint, service, hass, payload=payload, instance=instance 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): async def test_temp_sensor(hass):

View File

@ -97,15 +97,12 @@ async def test_report_state_instance(hass, aioclient_mock):
assert report["instance"] == "fan.preset_mode" assert report["instance"] == "fan.preset_mode"
assert report["namespace"] == "Alexa.ModeController" assert report["namespace"] == "Alexa.ModeController"
checks += 1 checks += 1
if report["name"] == "percentage": if report["name"] == "rangeValue":
assert report["value"] == 90 assert report["value"] == 90
assert report["namespace"] == "Alexa.PercentageController" assert report["instance"] == "fan.percentage"
assert report["namespace"] == "Alexa.RangeController"
checks += 1 checks += 1
if report["name"] == "powerLevel": assert checks == 3
assert report["value"] == 90
assert report["namespace"] == "Alexa.PowerLevelController"
checks += 1
assert checks == 4
assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan" assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan"