mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 22:27:07 +00:00
Add humidifier support for Alexa (#81329)
This commit is contained in:
parent
35e81cf982
commit
5d4c4a1293
@ -8,6 +8,7 @@ from homeassistant.components import (
|
|||||||
climate,
|
climate,
|
||||||
cover,
|
cover,
|
||||||
fan,
|
fan,
|
||||||
|
humidifier,
|
||||||
image_processing,
|
image_processing,
|
||||||
input_button,
|
input_button,
|
||||||
input_number,
|
input_number,
|
||||||
@ -398,6 +399,8 @@ class AlexaPowerController(AlexaCapability):
|
|||||||
is_on = self.entity.state != climate.HVACMode.OFF
|
is_on = self.entity.state != climate.HVACMode.OFF
|
||||||
elif self.entity.domain == fan.DOMAIN:
|
elif self.entity.domain == fan.DOMAIN:
|
||||||
is_on = self.entity.state == fan.STATE_ON
|
is_on = self.entity.state == fan.STATE_ON
|
||||||
|
elif self.entity.domain == humidifier.DOMAIN:
|
||||||
|
is_on = self.entity.state == humidifier.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:
|
||||||
@ -1403,6 +1406,12 @@ class AlexaModeController(AlexaCapability):
|
|||||||
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None):
|
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None):
|
||||||
return f"{fan.ATTR_PRESET_MODE}.{mode}"
|
return f"{fan.ATTR_PRESET_MODE}.{mode}"
|
||||||
|
|
||||||
|
# Humidifier mode
|
||||||
|
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
|
||||||
|
mode = self.entity.attributes.get(humidifier.ATTR_MODE, None)
|
||||||
|
if mode in self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []):
|
||||||
|
return f"{humidifier.ATTR_MODE}.{mode}"
|
||||||
|
|
||||||
# Cover Position
|
# Cover Position
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
# Return state instead of position when using ModeController.
|
# Return state instead of position when using ModeController.
|
||||||
@ -1459,6 +1468,20 @@ class AlexaModeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._resource.serialize_capability_resources()
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
|
# Humidifier modes
|
||||||
|
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
|
||||||
|
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
|
||||||
|
modes = self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, [])
|
||||||
|
for mode in modes:
|
||||||
|
self._resource.add_mode(f"{humidifier.ATTR_MODE}.{mode}", [mode])
|
||||||
|
# Humidifiers or Fans with a single mode completely break Alexa discovery, add a
|
||||||
|
# fake preset (see issue #53832).
|
||||||
|
if len(modes) == 1:
|
||||||
|
self._resource.add_mode(
|
||||||
|
f"{humidifier.ATTR_MODE}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
|
||||||
|
)
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
# Cover Position Resources
|
# Cover Position Resources
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
self._resource = AlexaModeResource(
|
self._resource = AlexaModeResource(
|
||||||
@ -1600,6 +1623,12 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
return self.entity.attributes.get(fan.ATTR_PERCENTAGE)
|
return self.entity.attributes.get(fan.ATTR_PERCENTAGE)
|
||||||
return 100 if self.entity.state == fan.STATE_ON else 0
|
return 100 if self.entity.state == fan.STATE_ON else 0
|
||||||
|
|
||||||
|
# Humidifier target humidity
|
||||||
|
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}":
|
||||||
|
# If the humidifier is turned off the target humidity attribute is not set.
|
||||||
|
# We return 0 to make clear we do not know the current value.
|
||||||
|
return self.entity.attributes.get(humidifier.ATTR_HUMIDITY, 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)
|
||||||
@ -1640,6 +1669,17 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._resource.serialize_capability_resources()
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
|
# Humidifier Target Humidity Resources
|
||||||
|
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}":
|
||||||
|
self._resource = AlexaPresetResource(
|
||||||
|
labels=["Humidity", "Percentage", "Target humidity"],
|
||||||
|
min_value=self.entity.attributes.get(humidifier.ATTR_MIN_HUMIDITY, 10),
|
||||||
|
max_value=self.entity.attributes.get(humidifier.ATTR_MAX_HUMIDITY, 90),
|
||||||
|
precision=1,
|
||||||
|
unit=AlexaGlobalCatalog.UNIT_PERCENT,
|
||||||
|
)
|
||||||
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
# Cover Position Resources
|
# Cover Position Resources
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
self._resource = AlexaPresetResource(
|
self._resource = AlexaPresetResource(
|
||||||
@ -1764,6 +1804,22 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._semantics.serialize_semantics()
|
return self._semantics.serialize_semantics()
|
||||||
|
|
||||||
|
# Target Humidity Percentage
|
||||||
|
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}":
|
||||||
|
lower_labels = [AlexaSemantics.ACTION_LOWER]
|
||||||
|
raise_labels = [AlexaSemantics.ACTION_RAISE]
|
||||||
|
self._semantics = AlexaSemantics()
|
||||||
|
min_value = self.entity.attributes.get(humidifier.ATTR_MIN_HUMIDITY, 10)
|
||||||
|
max_value = self.entity.attributes.get(humidifier.ATTR_MAX_HUMIDITY, 90)
|
||||||
|
|
||||||
|
self._semantics.add_action_to_directive(
|
||||||
|
lower_labels, "SetRangeValue", {"rangeValue": min_value}
|
||||||
|
)
|
||||||
|
self._semantics.add_action_to_directive(
|
||||||
|
raise_labels, "SetRangeValue", {"rangeValue": max_value}
|
||||||
|
)
|
||||||
|
return self._semantics.serialize_semantics()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,7 +83,8 @@ API_THERMOSTAT_MODES_CUSTOM = {
|
|||||||
}
|
}
|
||||||
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
|
# AlexaModeController does not like a single mode for the fan preset or humidifier mode,
|
||||||
|
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode
|
||||||
PRESET_MODE_NA = "-"
|
PRESET_MODE_NA = "-"
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ from homeassistant.components import (
|
|||||||
cover,
|
cover,
|
||||||
fan,
|
fan,
|
||||||
group,
|
group,
|
||||||
|
humidifier,
|
||||||
image_processing,
|
image_processing,
|
||||||
input_boolean,
|
input_boolean,
|
||||||
input_button,
|
input_button,
|
||||||
@ -100,6 +101,9 @@ class DisplayCategory:
|
|||||||
# to HDMI1. Applies to Scenes
|
# to HDMI1. Applies to Scenes
|
||||||
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
|
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
|
||||||
|
|
||||||
|
# Indicates a device that cools the air in interior spaces.
|
||||||
|
AIR_CONDITIONER = "AIR_CONDITIONER"
|
||||||
|
|
||||||
# Indicates a device that emits pleasant odors and masks unpleasant odors in interior spaces.
|
# Indicates a device that emits pleasant odors and masks unpleasant odors in interior spaces.
|
||||||
AIR_FRESHENER = "AIR_FRESHENER"
|
AIR_FRESHENER = "AIR_FRESHENER"
|
||||||
|
|
||||||
@ -583,6 +587,30 @@ class FanCapabilities(AlexaEntity):
|
|||||||
yield Alexa(self.hass)
|
yield Alexa(self.hass)
|
||||||
|
|
||||||
|
|
||||||
|
@ENTITY_ADAPTERS.register(humidifier.DOMAIN)
|
||||||
|
class HumidifierCapabilities(AlexaEntity):
|
||||||
|
"""Class to represent Humidifier capabilities."""
|
||||||
|
|
||||||
|
def default_display_categories(self):
|
||||||
|
"""Return the display categories for this entity."""
|
||||||
|
return [DisplayCategory.OTHER]
|
||||||
|
|
||||||
|
def interfaces(self):
|
||||||
|
"""Yield the supported interfaces."""
|
||||||
|
yield AlexaPowerController(self.entity)
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if supported & humidifier.HumidifierEntityFeature.MODES:
|
||||||
|
yield AlexaModeController(
|
||||||
|
self.entity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}"
|
||||||
|
)
|
||||||
|
yield AlexaRangeController(
|
||||||
|
self.entity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}"
|
||||||
|
)
|
||||||
|
|
||||||
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
|
yield Alexa(self.hass)
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(lock.DOMAIN)
|
@ENTITY_ADAPTERS.register(lock.DOMAIN)
|
||||||
class LockCapabilities(AlexaEntity):
|
class LockCapabilities(AlexaEntity):
|
||||||
"""Class to represent Lock capabilities."""
|
"""Class to represent Lock capabilities."""
|
||||||
|
@ -14,6 +14,7 @@ from homeassistant.components import (
|
|||||||
cover,
|
cover,
|
||||||
fan,
|
fan,
|
||||||
group,
|
group,
|
||||||
|
humidifier,
|
||||||
input_button,
|
input_button,
|
||||||
input_number,
|
input_number,
|
||||||
light,
|
light,
|
||||||
@ -154,6 +155,8 @@ async def async_api_turn_on(
|
|||||||
service = cover.SERVICE_OPEN_COVER
|
service = cover.SERVICE_OPEN_COVER
|
||||||
elif domain == fan.DOMAIN:
|
elif domain == fan.DOMAIN:
|
||||||
service = fan.SERVICE_TURN_ON
|
service = fan.SERVICE_TURN_ON
|
||||||
|
elif domain == humidifier.DOMAIN:
|
||||||
|
service = humidifier.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 (
|
if (
|
||||||
@ -201,6 +204,8 @@ async def async_api_turn_off(
|
|||||||
service = cover.SERVICE_CLOSE_COVER
|
service = cover.SERVICE_CLOSE_COVER
|
||||||
elif domain == fan.DOMAIN:
|
elif domain == fan.DOMAIN:
|
||||||
service = fan.SERVICE_TURN_OFF
|
service = fan.SERVICE_TURN_OFF
|
||||||
|
elif domain == humidifier.DOMAIN:
|
||||||
|
service = humidifier.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 (
|
||||||
@ -448,20 +453,31 @@ async def async_api_set_percentage(
|
|||||||
"""Process a set percentage request."""
|
"""Process a set percentage request."""
|
||||||
entity = directive.entity
|
entity = directive.entity
|
||||||
|
|
||||||
if entity.domain != fan.DOMAIN:
|
if entity.domain == fan.DOMAIN:
|
||||||
|
percentage = int(directive.payload["percentage"])
|
||||||
|
service = fan.SERVICE_SET_PERCENTAGE
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
fan.ATTR_PERCENTAGE: percentage,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
entity.domain, service, data, blocking=False, context=context
|
||||||
|
)
|
||||||
|
elif entity.domain == humidifier.DOMAIN:
|
||||||
|
percentage = int(directive.payload["percentage"])
|
||||||
|
service = humidifier.SERVICE_SET_HUMIDITY
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
humidifier.ATTR_HUMIDITY: percentage,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
entity.domain, service, data, blocking=False, context=context
|
||||||
|
)
|
||||||
|
else:
|
||||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||||
|
|
||||||
percentage = int(directive.payload["percentage"])
|
|
||||||
service = fan.SERVICE_SET_PERCENTAGE
|
|
||||||
data = {
|
|
||||||
ATTR_ENTITY_ID: entity.entity_id,
|
|
||||||
fan.ATTR_PERCENTAGE: percentage,
|
|
||||||
}
|
|
||||||
|
|
||||||
await hass.services.async_call(
|
|
||||||
entity.domain, service, data, blocking=False, context=context
|
|
||||||
)
|
|
||||||
|
|
||||||
return directive.response()
|
return directive.response()
|
||||||
|
|
||||||
|
|
||||||
@ -1130,6 +1146,18 @@ async def async_api_set_mode(
|
|||||||
msg = f"Entity '{entity.entity_id}' does not support Preset '{preset_mode}'"
|
msg = f"Entity '{entity.entity_id}' does not support Preset '{preset_mode}'"
|
||||||
raise AlexaInvalidValueError(msg)
|
raise AlexaInvalidValueError(msg)
|
||||||
|
|
||||||
|
# Humidifier mode
|
||||||
|
elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
|
||||||
|
mode = mode.split(".")[1]
|
||||||
|
if mode != PRESET_MODE_NA and mode in entity.attributes.get(
|
||||||
|
humidifier.ATTR_AVAILABLE_MODES
|
||||||
|
):
|
||||||
|
service = humidifier.SERVICE_SET_MODE
|
||||||
|
data[humidifier.ATTR_MODE] = mode
|
||||||
|
else:
|
||||||
|
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
|
||||||
|
raise AlexaInvalidValueError(msg)
|
||||||
|
|
||||||
# Cover Position
|
# Cover Position
|
||||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
position = mode.split(".")[1]
|
position = mode.split(".")[1]
|
||||||
@ -1306,6 +1334,12 @@ async def async_api_set_range(
|
|||||||
else:
|
else:
|
||||||
service = fan.SERVICE_TURN_ON
|
service = fan.SERVICE_TURN_ON
|
||||||
|
|
||||||
|
# Humidifier target humidity
|
||||||
|
elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}":
|
||||||
|
range_value = int(range_value)
|
||||||
|
service = humidifier.SERVICE_SET_HUMIDITY
|
||||||
|
data[humidifier.ATTR_HUMIDITY] = range_value
|
||||||
|
|
||||||
# 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)
|
||||||
@ -1414,6 +1448,26 @@ async def async_api_adjust_range(
|
|||||||
else:
|
else:
|
||||||
service = fan.SERVICE_TURN_OFF
|
service = fan.SERVICE_TURN_OFF
|
||||||
|
|
||||||
|
# Humidifier target humidity
|
||||||
|
elif instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_HUMIDITY}":
|
||||||
|
percentage_step = 5
|
||||||
|
range_delta = (
|
||||||
|
int(range_delta * percentage_step)
|
||||||
|
if range_delta_default
|
||||||
|
else int(range_delta)
|
||||||
|
)
|
||||||
|
service = humidifier.SERVICE_SET_HUMIDITY
|
||||||
|
if not (current := entity.attributes.get(humidifier.ATTR_HUMIDITY)):
|
||||||
|
msg = f"Unable to determine {entity.entity_id} current target humidity"
|
||||||
|
raise AlexaInvalidValueError(msg)
|
||||||
|
min_value = entity.attributes.get(humidifier.ATTR_MIN_HUMIDITY, 10)
|
||||||
|
max_value = entity.attributes.get(humidifier.ATTR_MAX_HUMIDITY, 90)
|
||||||
|
percentage = response_value = min(
|
||||||
|
max_value, max(min_value, range_delta + current)
|
||||||
|
)
|
||||||
|
if percentage:
|
||||||
|
data[humidifier.ATTR_HUMIDITY] = percentage
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -411,6 +411,72 @@ async def test_report_fan_speed_state(hass):
|
|||||||
properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
|
properties.assert_equal("Alexa.RangeController", "rangeValue", 0)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_report_humidifier_humidity_state(hass):
|
||||||
|
"""Test PercentageController, PowerLevelController reports humidifier humidity correctly."""
|
||||||
|
hass.states.async_set(
|
||||||
|
"humidifier.dry",
|
||||||
|
"on",
|
||||||
|
{
|
||||||
|
"friendly_name": "Humidifier dry",
|
||||||
|
"supported_features": 0,
|
||||||
|
"humidity": 25,
|
||||||
|
"min_humidity": 20,
|
||||||
|
"max_humidity": 90,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
"humidifier.wet",
|
||||||
|
"on",
|
||||||
|
{
|
||||||
|
"friendly_name": "Humidifier wet",
|
||||||
|
"supported_features": 0,
|
||||||
|
"humidity": 80,
|
||||||
|
"min_humidity": 20,
|
||||||
|
"max_humidity": 90,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
properties = await reported_properties(hass, "humidifier.dry")
|
||||||
|
properties.assert_equal("Alexa.RangeController", "rangeValue", 25)
|
||||||
|
|
||||||
|
properties = await reported_properties(hass, "humidifier.wet")
|
||||||
|
properties.assert_equal("Alexa.RangeController", "rangeValue", 80)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_report_humidifier_mode(hass):
|
||||||
|
"""Test ModeController reports humidifier mode correctly."""
|
||||||
|
hass.states.async_set(
|
||||||
|
"humidifier.auto",
|
||||||
|
"on",
|
||||||
|
{
|
||||||
|
"friendly_name": "Humidifier auto",
|
||||||
|
"supported_features": 1,
|
||||||
|
"humidity": 50,
|
||||||
|
"mode": "Auto",
|
||||||
|
"available_modes": ["Auto", "Low", "Medium", "High"],
|
||||||
|
"min_humidity": 20,
|
||||||
|
"max_humidity": 90,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
properties = await reported_properties(hass, "humidifier.auto")
|
||||||
|
properties.assert_equal("Alexa.ModeController", "mode", "mode.Auto")
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
"humidifier.medium",
|
||||||
|
"on",
|
||||||
|
{
|
||||||
|
"friendly_name": "Humidifier auto",
|
||||||
|
"supported_features": 1,
|
||||||
|
"humidity": 60,
|
||||||
|
"mode": "Medium",
|
||||||
|
"available_modes": ["Auto", "Low", "Medium", "High"],
|
||||||
|
"min_humidity": 20,
|
||||||
|
"max_humidity": 90,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
properties = await reported_properties(hass, "humidifier.medium")
|
||||||
|
properties.assert_equal("Alexa.ModeController", "mode", "mode.Medium")
|
||||||
|
|
||||||
|
|
||||||
async def test_report_fan_preset_mode(hass):
|
async def test_report_fan_preset_mode(hass):
|
||||||
"""Test ModeController reports fan preset_mode correctly."""
|
"""Test ModeController reports fan preset_mode correctly."""
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -950,6 +950,145 @@ async def test_single_preset_mode_fan(hass, caplog):
|
|||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2022-04-19 07:53:05")
|
||||||
|
async def test_humidifier(hass, caplog):
|
||||||
|
"""Test humidifier controller."""
|
||||||
|
device = (
|
||||||
|
"humidifier.test_1",
|
||||||
|
"on",
|
||||||
|
{
|
||||||
|
"friendly_name": "Humidifier test 1",
|
||||||
|
"humidity": 66,
|
||||||
|
"supported_features": 1,
|
||||||
|
"mode": "Auto",
|
||||||
|
"available_modes": ["Auto", "Low", "Medium", "High"],
|
||||||
|
"min_humidity": 20,
|
||||||
|
"max_humidity": 90,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await discovery_test(device, hass)
|
||||||
|
|
||||||
|
await assert_power_controller_works(
|
||||||
|
"humidifier#test_1",
|
||||||
|
"humidifier.turn_on",
|
||||||
|
"humidifier.turn_off",
|
||||||
|
hass,
|
||||||
|
"2022-04-19T07:53:05Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
"Alexa.ModeController",
|
||||||
|
"SetMode",
|
||||||
|
"humidifier#test_1",
|
||||||
|
"humidifier.set_mode",
|
||||||
|
hass,
|
||||||
|
payload={"mode": "mode.Auto"},
|
||||||
|
instance="humidifier.mode",
|
||||||
|
)
|
||||||
|
assert call.data["mode"] == "Auto"
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
await assert_request_calls_service(
|
||||||
|
"Alexa.ModeController",
|
||||||
|
"SetMode",
|
||||||
|
"humidifier#test_1",
|
||||||
|
"humidifier.set_mode",
|
||||||
|
hass,
|
||||||
|
payload={"mode": "mode.-"},
|
||||||
|
instance="humidifier.mode",
|
||||||
|
)
|
||||||
|
assert "Entity 'humidifier.test_1' does not support Mode '-'" in caplog.text
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
"Alexa.RangeController",
|
||||||
|
"SetRangeValue",
|
||||||
|
"humidifier#test_1",
|
||||||
|
"humidifier.set_humidity",
|
||||||
|
hass,
|
||||||
|
payload={"rangeValue": "67"},
|
||||||
|
instance="humidifier.humidity",
|
||||||
|
)
|
||||||
|
assert call.data["humidity"] == 67
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
"Alexa.RangeController",
|
||||||
|
"SetRangeValue",
|
||||||
|
"humidifier#test_1",
|
||||||
|
"humidifier.set_humidity",
|
||||||
|
hass,
|
||||||
|
payload={"rangeValue": "33"},
|
||||||
|
instance="humidifier.humidity",
|
||||||
|
)
|
||||||
|
assert call.data["humidity"] == 33
|
||||||
|
|
||||||
|
|
||||||
|
async def test_humidifier_without_modes(hass):
|
||||||
|
"""Test humidifier discovery without modes."""
|
||||||
|
|
||||||
|
device = (
|
||||||
|
"humidifier.test_2",
|
||||||
|
"on",
|
||||||
|
{
|
||||||
|
"friendly_name": "Humidifier test 2",
|
||||||
|
"humidity": 33,
|
||||||
|
"supported_features": 0,
|
||||||
|
"min_humidity": 20,
|
||||||
|
"max_humidity": 90,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
|
assert appliance["endpointId"] == "humidifier#test_2"
|
||||||
|
assert appliance["displayCategories"][0] == "OTHER"
|
||||||
|
assert appliance["friendlyName"] == "Humidifier test 2"
|
||||||
|
capabilities = assert_endpoint_capabilities(
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
async def test_humidifier_with_modes(hass):
|
||||||
|
"""Test humidifier discovery with modes."""
|
||||||
|
|
||||||
|
device = (
|
||||||
|
"humidifier.test_1",
|
||||||
|
"on",
|
||||||
|
{
|
||||||
|
"friendly_name": "Humidifier test 1",
|
||||||
|
"humidity": 66,
|
||||||
|
"supported_features": 1,
|
||||||
|
"mode": "Auto",
|
||||||
|
"available_modes": ["Auto", "Low", "Medium", "High"],
|
||||||
|
"min_humidity": 20,
|
||||||
|
"max_humidity": 90,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
|
assert appliance["endpointId"] == "humidifier#test_1"
|
||||||
|
assert appliance["displayCategories"][0] == "OTHER"
|
||||||
|
assert appliance["friendlyName"] == "Humidifier test 1"
|
||||||
|
capabilities = assert_endpoint_capabilities(
|
||||||
|
appliance,
|
||||||
|
"Alexa.ModeController",
|
||||||
|
"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
|
||||||
|
|
||||||
|
|
||||||
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"})
|
||||||
|
@ -209,8 +209,8 @@ async def test_report_state_unsets_authorized_on_access_token_error(
|
|||||||
config._store.set_authorized.assert_called_once_with(False)
|
config._store.set_authorized.assert_called_once_with(False)
|
||||||
|
|
||||||
|
|
||||||
async def test_report_state_instance(hass, aioclient_mock):
|
async def test_report_state_fan(hass, aioclient_mock):
|
||||||
"""Test proactive state reports with instance."""
|
"""Test proactive state reports with fan instance."""
|
||||||
aioclient_mock.post(TEST_URL, text="", status=202)
|
aioclient_mock.post(TEST_URL, text="", status=202)
|
||||||
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
@ -275,6 +275,64 @@ async def test_report_state_instance(hass, aioclient_mock):
|
|||||||
assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan"
|
assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_report_state_humidifier(hass, aioclient_mock):
|
||||||
|
"""Test proactive state reports with humidifier instance."""
|
||||||
|
aioclient_mock.post(TEST_URL, text="", status=202)
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
"humidifier.test_humidifier",
|
||||||
|
"off",
|
||||||
|
{
|
||||||
|
"friendly_name": "Test humidifier",
|
||||||
|
"supported_features": 1,
|
||||||
|
"mode": None,
|
||||||
|
"available_modes": ["auto", "smart"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await state_report.async_enable_proactive_mode(hass, get_default_config(hass))
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
"humidifier.test_humidifier",
|
||||||
|
"on",
|
||||||
|
{
|
||||||
|
"friendly_name": "Test humidifier",
|
||||||
|
"supported_features": 1,
|
||||||
|
"mode": "smart",
|
||||||
|
"available_modes": ["auto", "smart"],
|
||||||
|
"humidity": 55,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# To trigger event listener
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(aioclient_mock.mock_calls) == 1
|
||||||
|
call = aioclient_mock.mock_calls
|
||||||
|
|
||||||
|
call_json = call[0][2]
|
||||||
|
assert call_json["event"]["header"]["namespace"] == "Alexa"
|
||||||
|
assert call_json["event"]["header"]["name"] == "ChangeReport"
|
||||||
|
|
||||||
|
change_reports = call_json["event"]["payload"]["change"]["properties"]
|
||||||
|
|
||||||
|
checks = 0
|
||||||
|
for report in change_reports:
|
||||||
|
if report["name"] == "mode":
|
||||||
|
assert report["value"] == "mode.smart"
|
||||||
|
assert report["instance"] == "humidifier.mode"
|
||||||
|
assert report["namespace"] == "Alexa.ModeController"
|
||||||
|
checks += 1
|
||||||
|
if report["name"] == "rangeValue":
|
||||||
|
assert report["value"] == 55
|
||||||
|
assert report["instance"] == "humidifier.humidity"
|
||||||
|
assert report["namespace"] == "Alexa.RangeController"
|
||||||
|
checks += 1
|
||||||
|
assert checks == 2
|
||||||
|
|
||||||
|
assert call_json["event"]["endpoint"]["endpointId"] == "humidifier#test_humidifier"
|
||||||
|
|
||||||
|
|
||||||
async def test_send_add_or_update_message(hass, aioclient_mock):
|
async def test_send_add_or_update_message(hass, aioclient_mock):
|
||||||
"""Test sending an AddOrUpdateReport message."""
|
"""Test sending an AddOrUpdateReport message."""
|
||||||
aioclient_mock.post(TEST_URL, text="")
|
aioclient_mock.post(TEST_URL, text="")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user