Add water_heater to alexa (#106011)

* Add water_heater support to alexa

* Improve test coverage

* Follow up comments
This commit is contained in:
Jan Bouwhuis 2023-12-20 16:48:02 +01:00 committed by GitHub
parent 5a3db078d5
commit 93c800c4e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 424 additions and 14 deletions

View File

@ -19,6 +19,7 @@ from homeassistant.components import (
number, number,
timer, timer,
vacuum, vacuum,
water_heater,
) )
from homeassistant.components.alarm_control_panel import ( from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature,
@ -435,7 +436,8 @@ class AlexaPowerController(AlexaCapability):
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:
is_on = self.entity.state != STATE_IDLE is_on = self.entity.state != STATE_IDLE
elif self.entity.domain == water_heater.DOMAIN:
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
else: else:
is_on = self.entity.state != STATE_OFF is_on = self.entity.state != STATE_OFF
@ -938,6 +940,9 @@ class AlexaTemperatureSensor(AlexaCapability):
if self.entity.domain == climate.DOMAIN: if self.entity.domain == climate.DOMAIN:
unit = self.hass.config.units.temperature_unit unit = self.hass.config.units.temperature_unit
temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE) temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)
elif self.entity.domain == water_heater.DOMAIN:
unit = self.hass.config.units.temperature_unit
temp = self.entity.attributes.get(water_heater.ATTR_CURRENT_TEMPERATURE)
if temp is None or temp in (STATE_UNAVAILABLE, STATE_UNKNOWN): if temp is None or temp in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return None return None
@ -1108,6 +1113,8 @@ class AlexaThermostatController(AlexaCapability):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE: if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE:
properties.append({"name": "targetSetpoint"}) properties.append({"name": "targetSetpoint"})
if supported & water_heater.WaterHeaterEntityFeature.TARGET_TEMPERATURE:
properties.append({"name": "targetSetpoint"})
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
properties.append({"name": "lowerSetpoint"}) properties.append({"name": "lowerSetpoint"})
properties.append({"name": "upperSetpoint"}) properties.append({"name": "upperSetpoint"})
@ -1127,6 +1134,8 @@ class AlexaThermostatController(AlexaCapability):
return None return None
if name == "thermostatMode": if name == "thermostatMode":
if self.entity.domain == water_heater.DOMAIN:
return None
preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE) preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE)
mode: dict[str, str] | str | None mode: dict[str, str] | str | None
@ -1176,9 +1185,13 @@ class AlexaThermostatController(AlexaCapability):
ThermostatMode Values. ThermostatMode Values.
ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM. ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM.
Water heater devices do not return thermostat modes.
""" """
if self.entity.domain == water_heater.DOMAIN:
return None
supported_modes: list[str] = [] supported_modes: list[str] = []
hvac_modes = self.entity.attributes[climate.ATTR_HVAC_MODES] hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES, [])
for mode in hvac_modes: for mode in hvac_modes:
if thermostat_mode := API_THERMOSTAT_MODES.get(mode): if thermostat_mode := API_THERMOSTAT_MODES.get(mode):
supported_modes.append(thermostat_mode) supported_modes.append(thermostat_mode)
@ -1408,6 +1421,16 @@ class AlexaModeController(AlexaCapability):
if mode in self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []): if mode in self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []):
return f"{humidifier.ATTR_MODE}.{mode}" return f"{humidifier.ATTR_MODE}.{mode}"
# Water heater operation mode
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
operation_mode = self.entity.attributes.get(
water_heater.ATTR_OPERATION_MODE, None
)
if operation_mode in self.entity.attributes.get(
water_heater.ATTR_OPERATION_LIST, []
):
return f"{water_heater.ATTR_OPERATION_MODE}.{operation_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.
@ -1478,6 +1501,26 @@ class AlexaModeController(AlexaCapability):
) )
return self._resource.serialize_capability_resources() return self._resource.serialize_capability_resources()
# Water heater operation modes
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
operation_modes = self.entity.attributes.get(
water_heater.ATTR_OPERATION_LIST, []
)
for operation_mode in operation_modes:
self._resource.add_mode(
f"{water_heater.ATTR_OPERATION_MODE}.{operation_mode}",
[operation_mode],
)
# Devices with a single mode completely break Alexa discovery,
# add a fake preset (see issue #53832).
if len(operation_modes) == 1:
self._resource.add_mode(
f"{water_heater.ATTR_OPERATION_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(

View File

@ -32,6 +32,7 @@ from homeassistant.components import (
switch, switch,
timer, timer,
vacuum, vacuum,
water_heater,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
@ -248,6 +249,9 @@ class DisplayCategory:
# Indicates a vacuum cleaner. # Indicates a vacuum cleaner.
VACUUM_CLEANER = "VACUUM_CLEANER" VACUUM_CLEANER = "VACUUM_CLEANER"
# Indicates a water heater.
WATER_HEATER = "WATER_HEATER"
# Indicates a network-connected wearable device, such as an Apple Watch, # Indicates a network-connected wearable device, such as an Apple Watch,
# Fitbit, or Samsung Gear. # Fitbit, or Samsung Gear.
WEARABLE = "WEARABLE" WEARABLE = "WEARABLE"
@ -456,23 +460,46 @@ class ButtonCapabilities(AlexaEntity):
@ENTITY_ADAPTERS.register(climate.DOMAIN) @ENTITY_ADAPTERS.register(climate.DOMAIN)
@ENTITY_ADAPTERS.register(water_heater.DOMAIN)
class ClimateCapabilities(AlexaEntity): class ClimateCapabilities(AlexaEntity):
"""Class to represent Climate capabilities.""" """Class to represent Climate capabilities."""
def default_display_categories(self) -> list[str]: def default_display_categories(self) -> list[str]:
"""Return the display categories for this entity.""" """Return the display categories for this entity."""
if self.entity.domain == water_heater.DOMAIN:
return [DisplayCategory.WATER_HEATER]
return [DisplayCategory.THERMOSTAT] return [DisplayCategory.THERMOSTAT]
def interfaces(self) -> Generator[AlexaCapability, None, None]: def interfaces(self) -> Generator[AlexaCapability, None, None]:
"""Yield the supported interfaces.""" """Yield the supported interfaces."""
# If we support two modes, one being off, we allow turning on too. # If we support two modes, one being off, we allow turning on too.
if climate.HVACMode.OFF in self.entity.attributes.get( supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
climate.ATTR_HVAC_MODES, [] if (
self.entity.domain == climate.DOMAIN
and climate.HVACMode.OFF
in self.entity.attributes.get(climate.ATTR_HVAC_MODES, [])
or self.entity.domain == water_heater.DOMAIN
and (supported_features & water_heater.WaterHeaterEntityFeature.ON_OFF)
): ):
yield AlexaPowerController(self.entity) yield AlexaPowerController(self.entity)
yield AlexaThermostatController(self.hass, self.entity) if (
yield AlexaTemperatureSensor(self.hass, self.entity) self.entity.domain == climate.DOMAIN
or self.entity.domain == water_heater.DOMAIN
and (
supported_features
& water_heater.WaterHeaterEntityFeature.OPERATION_MODE
)
):
yield AlexaThermostatController(self.hass, self.entity)
yield AlexaTemperatureSensor(self.hass, self.entity)
if self.entity.domain == water_heater.DOMAIN and (
supported_features & water_heater.WaterHeaterEntityFeature.OPERATION_MODE
):
yield AlexaModeController(
self.entity,
instance=f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}",
)
yield AlexaEndpointHealth(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity) yield Alexa(self.entity)

View File

@ -22,6 +22,7 @@ from homeassistant.components import (
number, number,
timer, timer,
vacuum, vacuum,
water_heater,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -80,6 +81,23 @@ from .state_report import AlexaDirective, AlexaResponse, async_enable_proactive_
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive" DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive"
MIN_MAX_TEMP = {
climate.DOMAIN: {
"min_temp": climate.ATTR_MIN_TEMP,
"max_temp": climate.ATTR_MAX_TEMP,
},
water_heater.DOMAIN: {
"min_temp": water_heater.ATTR_MIN_TEMP,
"max_temp": water_heater.ATTR_MAX_TEMP,
},
}
SERVICE_SET_TEMPERATURE = {
climate.DOMAIN: climate.SERVICE_SET_TEMPERATURE,
water_heater.DOMAIN: water_heater.SERVICE_SET_TEMPERATURE,
}
HANDLERS: Registry[ HANDLERS: Registry[
tuple[str, str], tuple[str, str],
Callable[ Callable[
@ -804,8 +822,10 @@ async def async_api_set_target_temp(
) -> AlexaResponse: ) -> AlexaResponse:
"""Process a set target temperature request.""" """Process a set target temperature request."""
entity = directive.entity entity = directive.entity
min_temp = entity.attributes[climate.ATTR_MIN_TEMP] domain = entity.domain
max_temp = entity.attributes[climate.ATTR_MAX_TEMP]
min_temp = entity.attributes[MIN_MAX_TEMP[domain]["min_temp"]]
max_temp = entity.attributes["max_temp"]
unit = hass.config.units.temperature_unit unit = hass.config.units.temperature_unit
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
@ -849,9 +869,11 @@ async def async_api_set_target_temp(
} }
) )
service = SERVICE_SET_TEMPERATURE[domain]
await hass.services.async_call( await hass.services.async_call(
entity.domain, entity.domain,
climate.SERVICE_SET_TEMPERATURE, service,
data, data,
blocking=False, blocking=False,
context=context, context=context,
@ -867,11 +889,12 @@ async def async_api_adjust_target_temp(
directive: AlexaDirective, directive: AlexaDirective,
context: ha.Context, context: ha.Context,
) -> AlexaResponse: ) -> AlexaResponse:
"""Process an adjust target temperature request.""" """Process an adjust target temperature request for climates and water heaters."""
data: dict[str, Any] data: dict[str, Any]
entity = directive.entity entity = directive.entity
min_temp = entity.attributes[climate.ATTR_MIN_TEMP] domain = entity.domain
max_temp = entity.attributes[climate.ATTR_MAX_TEMP] min_temp = entity.attributes[MIN_MAX_TEMP[domain]["min_temp"]]
max_temp = entity.attributes[MIN_MAX_TEMP[domain]["max_temp"]]
unit = hass.config.units.temperature_unit unit = hass.config.units.temperature_unit
temp_delta = temperature_from_object( temp_delta = temperature_from_object(
@ -932,9 +955,11 @@ async def async_api_adjust_target_temp(
} }
) )
service = SERVICE_SET_TEMPERATURE[domain]
await hass.services.async_call( await hass.services.async_call(
entity.domain, entity.domain,
climate.SERVICE_SET_TEMPERATURE, service,
data, data,
blocking=False, blocking=False,
context=context, context=context,
@ -1163,6 +1188,23 @@ async def async_api_set_mode(
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'" msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
raise AlexaInvalidValueError(msg) raise AlexaInvalidValueError(msg)
# Water heater operation mode
elif instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
operation_mode = mode.split(".")[1]
operation_modes: list[str] | None = entity.attributes.get(
water_heater.ATTR_OPERATION_LIST
)
if (
operation_mode != PRESET_MODE_NA
and operation_modes
and operation_mode in operation_modes
):
service = water_heater.SERVICE_SET_OPERATION_MODE
data[water_heater.ATTR_OPERATION_MODE] = operation_mode
else:
msg = f"Entity '{entity.entity_id}' does not support Operation mode '{operation_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]

View File

@ -8,6 +8,13 @@ from homeassistant.components.alexa import smart_home
from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE, HVACMode from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE, HVACMode
from homeassistant.components.lock import STATE_JAMMED, STATE_LOCKING, STATE_UNLOCKING from homeassistant.components.lock import STATE_JAMMED, STATE_LOCKING, STATE_UNLOCKING
from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature
from homeassistant.components.water_heater import (
ATTR_OPERATION_LIST,
ATTR_OPERATION_MODE,
STATE_ECO,
STATE_GAS,
STATE_HEAT_PUMP,
)
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_AWAY,
@ -16,6 +23,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_DISARMED,
STATE_LOCKED, STATE_LOCKED,
STATE_OFF,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
STATE_UNLOCKED, STATE_UNLOCKED,
@ -777,6 +785,96 @@ async def test_report_climate_state(hass: HomeAssistant) -> None:
assert msg["event"]["payload"]["type"] == "INTERNAL_ERROR" assert msg["event"]["payload"]["type"] == "INTERNAL_ERROR"
async def test_report_water_heater_state(hass: HomeAssistant) -> None:
"""Test ThermostatController also reports state correctly for water heaters."""
for operation_mode in (STATE_ECO, STATE_GAS, STATE_HEAT_PUMP):
hass.states.async_set(
"water_heater.boyler",
operation_mode,
{
"friendly_name": "Boyler",
"supported_features": 11,
ATTR_CURRENT_TEMPERATURE: 34,
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
ATTR_OPERATION_LIST: [STATE_ECO, STATE_GAS, STATE_HEAT_PUMP],
ATTR_OPERATION_MODE: operation_mode,
},
)
properties = await reported_properties(hass, "water_heater.boyler")
properties.assert_not_has_property(
"Alexa.ThermostatController", "thermostatMode"
)
properties.assert_equal(
"Alexa.ModeController", "mode", f"operation_mode.{operation_mode}"
)
properties.assert_equal(
"Alexa.TemperatureSensor",
"temperature",
{"value": 34.0, "scale": "CELSIUS"},
)
for off_mode in [STATE_OFF]:
hass.states.async_set(
"water_heater.boyler",
off_mode,
{
"friendly_name": "Boyler",
"supported_features": 11,
ATTR_CURRENT_TEMPERATURE: 34,
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
},
)
properties = await reported_properties(hass, "water_heater.boyler")
properties.assert_not_has_property(
"Alexa.ThermostatController", "thermostatMode"
)
properties.assert_not_has_property("Alexa.ModeController", "mode")
properties.assert_equal(
"Alexa.TemperatureSensor",
"temperature",
{"value": 34.0, "scale": "CELSIUS"},
)
for state in "unavailable", "unknown":
hass.states.async_set(
f"water_heater.{state}",
state,
{"friendly_name": f"Boyler {state}", "supported_features": 11},
)
properties = await reported_properties(hass, f"water_heater.{state}")
properties.assert_not_has_property(
"Alexa.ThermostatController", "thermostatMode"
)
properties.assert_not_has_property("Alexa.ModeController", "mode")
async def test_report_singe_mode_water_heater(hass: HomeAssistant) -> None:
"""Test ThermostatController also reports state correctly for water heaters."""
operation_mode = STATE_ECO
hass.states.async_set(
"water_heater.boyler",
operation_mode,
{
"friendly_name": "Boyler",
"supported_features": 11,
ATTR_CURRENT_TEMPERATURE: 34,
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
ATTR_OPERATION_LIST: [STATE_ECO],
ATTR_OPERATION_MODE: operation_mode,
},
)
properties = await reported_properties(hass, "water_heater.boyler")
properties.assert_not_has_property("Alexa.ThermostatController", "thermostatMode")
properties.assert_equal(
"Alexa.ModeController", "mode", f"operation_mode.{operation_mode}"
)
properties.assert_equal(
"Alexa.TemperatureSensor",
"temperature",
{"value": 34.0, "scale": "CELSIUS"},
)
async def test_temperature_sensor_sensor(hass: HomeAssistant) -> None: async def test_temperature_sensor_sensor(hass: HomeAssistant) -> None:
"""Test TemperatureSensor reports sensor temperature correctly.""" """Test TemperatureSensor reports sensor temperature correctly."""
for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"): for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"):
@ -823,6 +921,29 @@ async def test_temperature_sensor_climate(hass: HomeAssistant) -> None:
) )
async def test_temperature_sensor_water_heater(hass: HomeAssistant) -> None:
"""Test TemperatureSensor reports climate temperature correctly."""
for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"):
hass.states.async_set(
"water_heater.boyler",
STATE_ECO,
{"supported_features": 11, ATTR_CURRENT_TEMPERATURE: bad_value},
)
properties = await reported_properties(hass, "water_heater.boyler")
properties.assert_not_has_property("Alexa.TemperatureSensor", "temperature")
hass.states.async_set(
"water_heater.boyler",
STATE_ECO,
{"supported_features": 11, ATTR_CURRENT_TEMPERATURE: 34},
)
properties = await reported_properties(hass, "water_heater.boyler")
properties.assert_equal(
"Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"}
)
async def test_report_alarm_control_panel_state(hass: HomeAssistant) -> None: async def test_report_alarm_control_panel_state(hass: HomeAssistant) -> None:
"""Test SecurityPanelController implements armState property.""" """Test SecurityPanelController implements armState property."""
hass.states.async_set("alarm_control_panel.armed_away", STATE_ALARM_ARMED_AWAY, {}) hass.states.async_set("alarm_control_panel.armed_away", STATE_ALARM_ARMED_AWAY, {})

View File

@ -128,12 +128,14 @@ async def assert_request_calls_service(
async def assert_request_fails( async def assert_request_fails(
namespace, name, endpoint, service_not_called, hass, payload=None namespace, name, endpoint, service_not_called, hass, payload=None, instance=None
): ):
"""Assert an API request returns an ErrorResponse.""" """Assert an API request returns an ErrorResponse."""
request = get_new_request(namespace, name, endpoint) request = get_new_request(namespace, name, endpoint)
if payload: if payload:
request["directive"]["payload"] = payload request["directive"]["payload"] = payload
if instance:
request["directive"]["header"]["instance"] = instance
domain, service_name = service_not_called.split(".") domain, service_name = service_not_called.split(".")
call = async_mock_service(hass, domain, service_name) call = async_mock_service(hass, domain, service_name)

View File

@ -2700,6 +2700,181 @@ async def test_thermostat(hass: HomeAssistant) -> None:
assert call.data["preset_mode"] == "eco" assert call.data["preset_mode"] == "eco"
async def test_water_heater(hass: HomeAssistant) -> None:
"""Test water_heater discovery."""
hass.config.units = US_CUSTOMARY_SYSTEM
device = (
"water_heater.boyler",
"gas",
{
"temperature": 70.0,
"target_temp_high": None,
"target_temp_low": None,
"current_temperature": 75.0,
"friendly_name": "Test water heater",
"supported_features": 1 | 2 | 8,
"operation_list": ["off", "gas", "eco"],
"operation_mode": "gas",
"min_temp": 50,
"max_temp": 90,
},
)
appliance = await discovery_test(device, hass)
assert appliance["endpointId"] == "water_heater#boyler"
assert appliance["displayCategories"][0] == "WATER_HEATER"
assert appliance["friendlyName"] == "Test water heater"
capabilities = assert_endpoint_capabilities(
appliance,
"Alexa.PowerController",
"Alexa.ThermostatController",
"Alexa.ModeController",
"Alexa.TemperatureSensor",
"Alexa.EndpointHealth",
"Alexa",
)
properties = await reported_properties(hass, "water_heater#boyler")
properties.assert_equal("Alexa.ModeController", "mode", "operation_mode.gas")
properties.assert_equal(
"Alexa.ThermostatController",
"targetSetpoint",
{"value": 70.0, "scale": "FAHRENHEIT"},
)
properties.assert_equal(
"Alexa.TemperatureSensor", "temperature", {"value": 75.0, "scale": "FAHRENHEIT"}
)
modes_capability = get_capability(capabilities, "Alexa.ModeController")
assert modes_capability is not None
configuration = modes_capability["configuration"]
supported_modes = ["operation_mode.off", "operation_mode.gas", "operation_mode.eco"]
for mode in supported_modes:
assert mode in [item["value"] for item in configuration["supportedModes"]]
call, msg = await assert_request_calls_service(
"Alexa.ThermostatController",
"SetTargetTemperature",
"water_heater#boyler",
"water_heater.set_temperature",
hass,
payload={"targetSetpoint": {"value": 69.0, "scale": "FAHRENHEIT"}},
)
assert call.data["temperature"] == 69.0
properties = ReportedProperties(msg["context"]["properties"])
properties.assert_equal(
"Alexa.ThermostatController",
"targetSetpoint",
{"value": 69.0, "scale": "FAHRENHEIT"},
)
msg = await assert_request_fails(
"Alexa.ThermostatController",
"SetTargetTemperature",
"water_heater#boyler",
"water_heater.set_temperature",
hass,
payload={"targetSetpoint": {"value": 0.0, "scale": "CELSIUS"}},
)
assert msg["event"]["payload"]["type"] == "TEMPERATURE_VALUE_OUT_OF_RANGE"
call, msg = await assert_request_calls_service(
"Alexa.ThermostatController",
"SetTargetTemperature",
"water_heater#boyler",
"water_heater.set_temperature",
hass,
payload={
"targetSetpoint": {"value": 30.0, "scale": "CELSIUS"},
},
)
assert call.data["temperature"] == 86.0
properties = ReportedProperties(msg["context"]["properties"])
properties.assert_equal(
"Alexa.ThermostatController",
"targetSetpoint",
{"value": 86.0, "scale": "FAHRENHEIT"},
)
call, msg = await assert_request_calls_service(
"Alexa.ThermostatController",
"AdjustTargetTemperature",
"water_heater#boyler",
"water_heater.set_temperature",
hass,
payload={"targetSetpointDelta": {"value": -10.0, "scale": "KELVIN"}},
)
assert call.data["temperature"] == 52.0
properties = ReportedProperties(msg["context"]["properties"])
properties.assert_equal(
"Alexa.ThermostatController",
"targetSetpoint",
{"value": 52.0, "scale": "FAHRENHEIT"},
)
msg = await assert_request_fails(
"Alexa.ThermostatController",
"AdjustTargetTemperature",
"water_heater#boyler",
"water_heater.set_temperature",
hass,
payload={"targetSetpointDelta": {"value": 20.0, "scale": "CELSIUS"}},
)
assert msg["event"]["payload"]["type"] == "TEMPERATURE_VALUE_OUT_OF_RANGE"
# Setting mode, the payload can be an object with a value attribute...
call, msg = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"water_heater#boyler",
"water_heater.set_operation_mode",
hass,
payload={"mode": "operation_mode.eco"},
instance="water_heater.operation_mode",
)
assert call.data["operation_mode"] == "eco"
properties = ReportedProperties(msg["context"]["properties"])
properties.assert_equal("Alexa.ModeController", "mode", "operation_mode.eco")
call, msg = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"water_heater#boyler",
"water_heater.set_operation_mode",
hass,
payload={"mode": "operation_mode.gas"},
instance="water_heater.operation_mode",
)
assert call.data["operation_mode"] == "gas"
properties = ReportedProperties(msg["context"]["properties"])
properties.assert_equal("Alexa.ModeController", "mode", "operation_mode.gas")
# assert unsupported mode
msg = await assert_request_fails(
"Alexa.ModeController",
"SetMode",
"water_heater#boyler",
"water_heater.set_operation_mode",
hass,
payload={"mode": "operation_mode.invalid"},
instance="water_heater.operation_mode",
)
assert msg["event"]["payload"]["type"] == "INVALID_VALUE"
call, _ = await assert_request_calls_service(
"Alexa.ModeController",
"SetMode",
"water_heater#boyler",
"water_heater.set_operation_mode",
hass,
payload={"mode": "operation_mode.off"},
instance="water_heater.operation_mode",
)
assert call.data["operation_mode"] == "off"
async def test_no_current_target_temp_adjusting_temp(hass: HomeAssistant) -> None: async def test_no_current_target_temp_adjusting_temp(hass: HomeAssistant) -> None:
"""Test thermostat adjusting temp with no initial target temperature.""" """Test thermostat adjusting temp with no initial target temperature."""
hass.config.units = US_CUSTOMARY_SYSTEM hass.config.units = US_CUSTOMARY_SYSTEM