mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
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
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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."""
|
||||||
|
@ -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):
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user