mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add water_heater to alexa (#106011)
* Add water_heater support to alexa * Improve test coverage * Follow up comments
This commit is contained in:
parent
5a3db078d5
commit
93c800c4e8
@ -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(
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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, {})
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user