Add support for variable fan speed list length. (#30574)
This commit is contained in:
parent
669c89e8c0
commit
605b0ceb5f
6 changed files with 134 additions and 67 deletions
|
@ -31,7 +31,6 @@ from .const import (
|
||||||
API_THERMOSTAT_PRESETS,
|
API_THERMOSTAT_PRESETS,
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
PERCENTAGE_FAN_MAP,
|
PERCENTAGE_FAN_MAP,
|
||||||
RANGE_FAN_MAP,
|
|
||||||
Inputs,
|
Inputs,
|
||||||
)
|
)
|
||||||
from .errors import UnsupportedProperty
|
from .errors import UnsupportedProperty
|
||||||
|
@ -1273,8 +1272,12 @@ class AlexaRangeController(AlexaCapability):
|
||||||
|
|
||||||
# Fan Speed
|
# Fan Speed
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||||
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST]
|
||||||
return RANGE_FAN_MAP.get(speed, 0)
|
speed = self.entity.attributes[fan.ATTR_SPEED]
|
||||||
|
speed_index = next(
|
||||||
|
(i for i, v in enumerate(speed_list) if v == speed), None
|
||||||
|
)
|
||||||
|
return speed_index
|
||||||
|
|
||||||
# Cover Position
|
# Cover Position
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
@ -1302,24 +1305,22 @@ class AlexaRangeController(AlexaCapability):
|
||||||
|
|
||||||
# Fan Speed Resources
|
# Fan Speed Resources
|
||||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||||
|
speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST]
|
||||||
|
max_value = len(speed_list) - 1
|
||||||
self._resource = AlexaPresetResource(
|
self._resource = AlexaPresetResource(
|
||||||
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
||||||
min_value=1,
|
min_value=0,
|
||||||
max_value=3,
|
max_value=max_value,
|
||||||
precision=1,
|
precision=1,
|
||||||
)
|
)
|
||||||
self._resource.add_preset(
|
for index, speed in enumerate(speed_list):
|
||||||
value=1,
|
labels = [speed.replace("_", " ")]
|
||||||
labels=[AlexaGlobalCatalog.VALUE_LOW, AlexaGlobalCatalog.VALUE_MINIMUM],
|
if index == 1:
|
||||||
)
|
labels.append(AlexaGlobalCatalog.VALUE_MINIMUM)
|
||||||
self._resource.add_preset(value=2, labels=[AlexaGlobalCatalog.VALUE_MEDIUM])
|
if index == max_value:
|
||||||
self._resource.add_preset(
|
labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM)
|
||||||
value=3,
|
self._resource.add_preset(value=index, labels=labels)
|
||||||
labels=[
|
|
||||||
AlexaGlobalCatalog.VALUE_HIGH,
|
|
||||||
AlexaGlobalCatalog.VALUE_MAXIMUM,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
return self._resource.serialize_capability_resources()
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
# Cover Position Resources
|
# Cover Position Resources
|
||||||
|
|
|
@ -84,20 +84,6 @@ PERCENTAGE_FAN_MAP = {
|
||||||
fan.SPEED_HIGH: 100,
|
fan.SPEED_HIGH: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
RANGE_FAN_MAP = {
|
|
||||||
fan.SPEED_OFF: 0,
|
|
||||||
fan.SPEED_LOW: 1,
|
|
||||||
fan.SPEED_MEDIUM: 2,
|
|
||||||
fan.SPEED_HIGH: 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
SPEED_FAN_MAP = {
|
|
||||||
0: fan.SPEED_OFF,
|
|
||||||
1: fan.SPEED_LOW,
|
|
||||||
2: fan.SPEED_MEDIUM,
|
|
||||||
3: fan.SPEED_HIGH,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Cause:
|
class Cause:
|
||||||
"""Possible causes for property changes.
|
"""Possible causes for property changes.
|
||||||
|
|
|
@ -51,8 +51,6 @@ from .const import (
|
||||||
API_THERMOSTAT_MODES_CUSTOM,
|
API_THERMOSTAT_MODES_CUSTOM,
|
||||||
API_THERMOSTAT_PRESETS,
|
API_THERMOSTAT_PRESETS,
|
||||||
PERCENTAGE_FAN_MAP,
|
PERCENTAGE_FAN_MAP,
|
||||||
RANGE_FAN_MAP,
|
|
||||||
SPEED_FAN_MAP,
|
|
||||||
Cause,
|
Cause,
|
||||||
Inputs,
|
Inputs,
|
||||||
)
|
)
|
||||||
|
@ -1096,8 +1094,10 @@ async def async_api_set_range(hass, config, directive, context):
|
||||||
|
|
||||||
# Fan Speed
|
# Fan Speed
|
||||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||||
|
range_value = int(range_value)
|
||||||
service = fan.SERVICE_SET_SPEED
|
service = fan.SERVICE_SET_SPEED
|
||||||
speed = SPEED_FAN_MAP.get(int(range_value))
|
speed_list = entity.attributes[fan.ATTR_SPEED_LIST]
|
||||||
|
speed = next((v for i, v in enumerate(speed_list) if i == range_value), None)
|
||||||
|
|
||||||
if not speed:
|
if not speed:
|
||||||
msg = "Entity does not support value"
|
msg = "Entity does not support value"
|
||||||
|
@ -1174,9 +1174,16 @@ async def async_api_adjust_range(hass, config, directive, context):
|
||||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||||
range_delta = int(range_delta)
|
range_delta = int(range_delta)
|
||||||
service = fan.SERVICE_SET_SPEED
|
service = fan.SERVICE_SET_SPEED
|
||||||
current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0)
|
speed_list = entity.attributes[fan.ATTR_SPEED_LIST]
|
||||||
speed = SPEED_FAN_MAP.get(
|
current_speed = entity.attributes[fan.ATTR_SPEED]
|
||||||
min(3, max(0, range_delta + current_range)), fan.SPEED_OFF
|
current_speed_index = next(
|
||||||
|
(i for i, v in enumerate(speed_list) if v == current_speed), 0
|
||||||
|
)
|
||||||
|
new_speed_index = min(
|
||||||
|
len(speed_list) - 1, max(0, current_speed_index + range_delta)
|
||||||
|
)
|
||||||
|
speed = next(
|
||||||
|
(v for i, v in enumerate(speed_list) if i == new_speed_index), None
|
||||||
)
|
)
|
||||||
|
|
||||||
if speed == fan.SPEED_OFF:
|
if speed == fan.SPEED_OFF:
|
||||||
|
|
|
@ -315,12 +315,22 @@ async def test_report_fan_speed_state(hass):
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"fan.off",
|
"fan.off",
|
||||||
"off",
|
"off",
|
||||||
{"friendly_name": "Off fan", "speed": "off", "supported_features": 1},
|
{
|
||||||
|
"friendly_name": "Off fan",
|
||||||
|
"speed": "off",
|
||||||
|
"supported_features": 1,
|
||||||
|
"speed_list": ["off", "low", "medium", "high"],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"fan.low_speed",
|
"fan.low_speed",
|
||||||
"on",
|
"on",
|
||||||
{"friendly_name": "Low speed fan", "speed": "low", "supported_features": 1},
|
{
|
||||||
|
"friendly_name": "Low speed fan",
|
||||||
|
"speed": "low",
|
||||||
|
"supported_features": 1,
|
||||||
|
"speed_list": ["off", "low", "medium", "high"],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"fan.medium_speed",
|
"fan.medium_speed",
|
||||||
|
@ -329,12 +339,18 @@ async def test_report_fan_speed_state(hass):
|
||||||
"friendly_name": "Medium speed fan",
|
"friendly_name": "Medium speed fan",
|
||||||
"speed": "medium",
|
"speed": "medium",
|
||||||
"supported_features": 1,
|
"supported_features": 1,
|
||||||
|
"speed_list": ["off", "low", "medium", "high"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"fan.high_speed",
|
"fan.high_speed",
|
||||||
"on",
|
"on",
|
||||||
{"friendly_name": "High speed fan", "speed": "high", "supported_features": 1},
|
{
|
||||||
|
"friendly_name": "High speed fan",
|
||||||
|
"speed": "high",
|
||||||
|
"supported_features": 1,
|
||||||
|
"speed_list": ["off", "low", "medium", "high"],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
properties = await reported_properties(hass, "fan.off")
|
properties = await reported_properties(hass, "fan.off")
|
||||||
|
@ -361,25 +377,24 @@ async def test_report_fan_speed_state(hass):
|
||||||
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(
|
||||||
"fan.off",
|
"fan.oscillating_off",
|
||||||
"off",
|
"off",
|
||||||
{"friendly_name": "Off fan", "speed": "off", "supported_features": 3},
|
{"friendly_name": "fan oscillating off", "supported_features": 2},
|
||||||
)
|
)
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"fan.low_speed",
|
"fan.oscillating_on",
|
||||||
"on",
|
"on",
|
||||||
{
|
{
|
||||||
"friendly_name": "Low speed fan",
|
"friendly_name": "Fan oscillating on",
|
||||||
"speed": "low",
|
|
||||||
"oscillating": True,
|
"oscillating": True,
|
||||||
"supported_features": 3,
|
"supported_features": 2,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
properties = await reported_properties(hass, "fan.off")
|
properties = await reported_properties(hass, "fan.oscillating_off")
|
||||||
properties.assert_equal("Alexa.ToggleController", "toggleState", "OFF")
|
properties.assert_equal("Alexa.ToggleController", "toggleState", "OFF")
|
||||||
|
|
||||||
properties = await reported_properties(hass, "fan.low_speed")
|
properties = await reported_properties(hass, "fan.oscillating_on")
|
||||||
properties.assert_equal("Alexa.ToggleController", "toggleState", "ON")
|
properties.assert_equal("Alexa.ToggleController", "toggleState", "ON")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,7 @@ def get_capability(capabilities, capability_name, instance=None):
|
||||||
for capability in capabilities:
|
for capability in capabilities:
|
||||||
if instance and capability["instance"] == instance:
|
if instance and capability["instance"] == instance:
|
||||||
return capability
|
return capability
|
||||||
elif capability["interface"] == capability_name:
|
if capability["interface"] == capability_name:
|
||||||
return capability
|
return capability
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -497,11 +497,11 @@ async def test_variable_fan(hass):
|
||||||
|
|
||||||
|
|
||||||
async def test_oscillating_fan(hass):
|
async def test_oscillating_fan(hass):
|
||||||
"""Test oscillating fan discovery."""
|
"""Test oscillating fan with ToggleController."""
|
||||||
device = (
|
device = (
|
||||||
"fan.test_3",
|
"fan.test_3",
|
||||||
"off",
|
"off",
|
||||||
{"friendly_name": "Test fan 3", "supported_features": 3},
|
{"friendly_name": "Test fan 3", "supported_features": 2},
|
||||||
)
|
)
|
||||||
appliance = await discovery_test(device, hass)
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
|
@ -510,10 +510,7 @@ async def test_oscillating_fan(hass):
|
||||||
assert appliance["friendlyName"] == "Test fan 3"
|
assert appliance["friendlyName"] == "Test fan 3"
|
||||||
capabilities = assert_endpoint_capabilities(
|
capabilities = assert_endpoint_capabilities(
|
||||||
appliance,
|
appliance,
|
||||||
"Alexa.PercentageController",
|
|
||||||
"Alexa.PowerController",
|
"Alexa.PowerController",
|
||||||
"Alexa.PowerLevelController",
|
|
||||||
"Alexa.RangeController",
|
|
||||||
"Alexa.ToggleController",
|
"Alexa.ToggleController",
|
||||||
"Alexa.EndpointHealth",
|
"Alexa.EndpointHealth",
|
||||||
"Alexa",
|
"Alexa",
|
||||||
|
@ -558,13 +555,13 @@ async def test_oscillating_fan(hass):
|
||||||
|
|
||||||
|
|
||||||
async def test_direction_fan(hass):
|
async def test_direction_fan(hass):
|
||||||
"""Test direction fan discovery."""
|
"""Test fan direction with modeController."""
|
||||||
device = (
|
device = (
|
||||||
"fan.test_4",
|
"fan.test_4",
|
||||||
"on",
|
"on",
|
||||||
{
|
{
|
||||||
"friendly_name": "Test fan 4",
|
"friendly_name": "Test fan 4",
|
||||||
"supported_features": 5,
|
"supported_features": 4,
|
||||||
"direction": "forward",
|
"direction": "forward",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -575,10 +572,7 @@ async def test_direction_fan(hass):
|
||||||
assert appliance["friendlyName"] == "Test fan 4"
|
assert appliance["friendlyName"] == "Test fan 4"
|
||||||
capabilities = assert_endpoint_capabilities(
|
capabilities = assert_endpoint_capabilities(
|
||||||
appliance,
|
appliance,
|
||||||
"Alexa.PercentageController",
|
|
||||||
"Alexa.PowerController",
|
"Alexa.PowerController",
|
||||||
"Alexa.PowerLevelController",
|
|
||||||
"Alexa.RangeController",
|
|
||||||
"Alexa.ModeController",
|
"Alexa.ModeController",
|
||||||
"Alexa.EndpointHealth",
|
"Alexa.EndpointHealth",
|
||||||
"Alexa",
|
"Alexa",
|
||||||
|
@ -667,17 +661,14 @@ async def test_direction_fan(hass):
|
||||||
|
|
||||||
|
|
||||||
async def test_fan_range(hass):
|
async def test_fan_range(hass):
|
||||||
"""Test fan discovery with range controller.
|
"""Test fan speed with rangeController."""
|
||||||
|
|
||||||
This one has variable speed.
|
|
||||||
"""
|
|
||||||
device = (
|
device = (
|
||||||
"fan.test_5",
|
"fan.test_5",
|
||||||
"off",
|
"off",
|
||||||
{
|
{
|
||||||
"friendly_name": "Test fan 5",
|
"friendly_name": "Test fan 5",
|
||||||
"supported_features": 1,
|
"supported_features": 1,
|
||||||
"speed_list": ["low", "medium", "high"],
|
"speed_list": ["off", "low", "medium", "high", "turbo", "warp_speed"],
|
||||||
"speed": "medium",
|
"speed": "medium",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -701,6 +692,60 @@ async def test_fan_range(hass):
|
||||||
assert range_capability is not None
|
assert range_capability is not None
|
||||||
assert range_capability["instance"] == "fan.speed"
|
assert range_capability["instance"] == "fan.speed"
|
||||||
|
|
||||||
|
capability_resources = range_capability["capabilityResources"]
|
||||||
|
assert capability_resources is not None
|
||||||
|
assert {
|
||||||
|
"@type": "asset",
|
||||||
|
"value": {"assetId": "Alexa.Setting.FanSpeed"},
|
||||||
|
} in capability_resources["friendlyNames"]
|
||||||
|
|
||||||
|
configuration = range_capability["configuration"]
|
||||||
|
assert configuration is not None
|
||||||
|
|
||||||
|
supported_range = configuration["supportedRange"]
|
||||||
|
assert supported_range["minimumValue"] == 0
|
||||||
|
assert supported_range["maximumValue"] == 5
|
||||||
|
assert supported_range["precision"] == 1
|
||||||
|
|
||||||
|
presets = configuration["presets"]
|
||||||
|
assert {
|
||||||
|
"rangeValue": 0,
|
||||||
|
"presetResources": {
|
||||||
|
"friendlyNames": [
|
||||||
|
{"@type": "text", "value": {"text": "off", "locale": "en-US"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
} in presets
|
||||||
|
|
||||||
|
assert {
|
||||||
|
"rangeValue": 1,
|
||||||
|
"presetResources": {
|
||||||
|
"friendlyNames": [
|
||||||
|
{"@type": "text", "value": {"text": "low", "locale": "en-US"}},
|
||||||
|
{"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
} in presets
|
||||||
|
|
||||||
|
assert {
|
||||||
|
"rangeValue": 2,
|
||||||
|
"presetResources": {
|
||||||
|
"friendlyNames": [
|
||||||
|
{"@type": "text", "value": {"text": "medium", "locale": "en-US"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
} in presets
|
||||||
|
|
||||||
|
assert {
|
||||||
|
"rangeValue": 5,
|
||||||
|
"presetResources": {
|
||||||
|
"friendlyNames": [
|
||||||
|
{"@type": "text", "value": {"text": "warp speed", "locale": "en-US"}},
|
||||||
|
{"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
} in presets
|
||||||
|
|
||||||
call, _ = await assert_request_calls_service(
|
call, _ = await assert_request_calls_service(
|
||||||
"Alexa.RangeController",
|
"Alexa.RangeController",
|
||||||
"SetRangeValue",
|
"SetRangeValue",
|
||||||
|
@ -712,9 +757,20 @@ async def test_fan_range(hass):
|
||||||
)
|
)
|
||||||
assert call.data["speed"] == "low"
|
assert call.data["speed"] == "low"
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
"Alexa.RangeController",
|
||||||
|
"SetRangeValue",
|
||||||
|
"fan#test_5",
|
||||||
|
"fan.set_speed",
|
||||||
|
hass,
|
||||||
|
payload={"rangeValue": "5"},
|
||||||
|
instance="fan.speed",
|
||||||
|
)
|
||||||
|
assert call.data["speed"] == "warp_speed"
|
||||||
|
|
||||||
await assert_range_changes(
|
await assert_range_changes(
|
||||||
hass,
|
hass,
|
||||||
[("low", "-1"), ("high", "1"), ("medium", "0")],
|
[("low", "-1"), ("high", "1"), ("medium", "0"), ("warp_speed", "99")],
|
||||||
"Alexa.RangeController",
|
"Alexa.RangeController",
|
||||||
"AdjustRangeValue",
|
"AdjustRangeValue",
|
||||||
"fan#test_5",
|
"fan#test_5",
|
||||||
|
@ -733,7 +789,7 @@ async def test_fan_range_off(hass):
|
||||||
{
|
{
|
||||||
"friendly_name": "Test fan 6",
|
"friendly_name": "Test fan 6",
|
||||||
"supported_features": 1,
|
"supported_features": 1,
|
||||||
"speed_list": ["low", "medium", "high"],
|
"speed_list": ["off", "low", "medium", "high"],
|
||||||
"speed": "high",
|
"speed": "high",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -752,7 +808,7 @@ async def test_fan_range_off(hass):
|
||||||
|
|
||||||
await assert_range_changes(
|
await assert_range_changes(
|
||||||
hass,
|
hass,
|
||||||
[("off", "-3")],
|
[("off", "-3"), ("off", "-99")],
|
||||||
"Alexa.RangeController",
|
"Alexa.RangeController",
|
||||||
"AdjustRangeValue",
|
"AdjustRangeValue",
|
||||||
"fan#test_6",
|
"fan#test_6",
|
||||||
|
|
|
@ -49,6 +49,7 @@ async def test_report_state_instance(hass, aioclient_mock):
|
||||||
"friendly_name": "Test fan",
|
"friendly_name": "Test fan",
|
||||||
"supported_features": 3,
|
"supported_features": 3,
|
||||||
"speed": "off",
|
"speed": "off",
|
||||||
|
"speed_list": ["off", "low", "high"],
|
||||||
"oscillating": False,
|
"oscillating": False,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -62,6 +63,7 @@ async def test_report_state_instance(hass, aioclient_mock):
|
||||||
"friendly_name": "Test fan",
|
"friendly_name": "Test fan",
|
||||||
"supported_features": 3,
|
"supported_features": 3,
|
||||||
"speed": "high",
|
"speed": "high",
|
||||||
|
"speed_list": ["off", "low", "high"],
|
||||||
"oscillating": True,
|
"oscillating": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue