mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Add nest climate support for SDM API (#42702)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
3c84c7ccf0
commit
a5da98680a
@ -96,7 +96,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
PLATFORMS = ["sensor", "camera"]
|
||||
# Platforms for SDM API
|
||||
PLATFORMS = ["sensor", "camera", "climate"]
|
||||
|
||||
# Services for the legacy API
|
||||
|
||||
|
@ -1,355 +1,18 @@
|
||||
"""Support for Nest thermostats."""
|
||||
import logging
|
||||
"""Support for Nest climate that dispatches between API versions."""
|
||||
|
||||
from nest.nest import APIError
|
||||
import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
CURRENT_HVAC_COOL,
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE,
|
||||
FAN_AUTO,
|
||||
FAN_ON,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
PRESET_AWAY,
|
||||
PRESET_ECO,
|
||||
PRESET_NONE,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_SCAN_INTERVAL,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from .climate_legacy import async_setup_legacy_entry
|
||||
from .climate_sdm import async_setup_sdm_entry
|
||||
from .const import DATA_SDM
|
||||
|
||||
from . import DATA_NEST, DOMAIN as NEST_DOMAIN
|
||||
from .const import SIGNAL_NEST_UPDATE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1))}
|
||||
)
|
||||
|
||||
NEST_MODE_HEAT_COOL = "heat-cool"
|
||||
NEST_MODE_ECO = "eco"
|
||||
NEST_MODE_HEAT = "heat"
|
||||
NEST_MODE_COOL = "cool"
|
||||
NEST_MODE_OFF = "off"
|
||||
|
||||
MODE_HASS_TO_NEST = {
|
||||
HVAC_MODE_AUTO: NEST_MODE_HEAT_COOL,
|
||||
HVAC_MODE_HEAT: NEST_MODE_HEAT,
|
||||
HVAC_MODE_COOL: NEST_MODE_COOL,
|
||||
HVAC_MODE_OFF: NEST_MODE_OFF,
|
||||
}
|
||||
|
||||
MODE_NEST_TO_HASS = {v: k for k, v in MODE_HASS_TO_NEST.items()}
|
||||
|
||||
ACTION_NEST_TO_HASS = {
|
||||
"off": CURRENT_HVAC_IDLE,
|
||||
"heating": CURRENT_HVAC_HEAT,
|
||||
"cooling": CURRENT_HVAC_COOL,
|
||||
}
|
||||
|
||||
PRESET_AWAY_AND_ECO = "Away and Eco"
|
||||
|
||||
PRESET_MODES = [PRESET_NONE, PRESET_AWAY, PRESET_ECO, PRESET_AWAY_AND_ECO]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Nest thermostat.
|
||||
|
||||
No longer in use.
|
||||
"""
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up the Nest climate device based on a config entry."""
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
|
||||
thermostats = await hass.async_add_executor_job(hass.data[DATA_NEST].thermostats)
|
||||
|
||||
all_devices = [
|
||||
NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in thermostats
|
||||
]
|
||||
|
||||
async_add_entities(all_devices, True)
|
||||
|
||||
|
||||
class NestThermostat(ClimateEntity):
|
||||
"""Representation of a Nest thermostat."""
|
||||
|
||||
def __init__(self, structure, device, temp_unit):
|
||||
"""Initialize the thermostat."""
|
||||
self._unit = temp_unit
|
||||
self.structure = structure
|
||||
self.device = device
|
||||
self._fan_modes = [FAN_ON, FAN_AUTO]
|
||||
|
||||
# Set the default supported features
|
||||
self._support_flags = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
||||
|
||||
# Not all nest devices support cooling and heating remove unused
|
||||
self._operation_list = []
|
||||
|
||||
if self.device.can_heat and self.device.can_cool:
|
||||
self._operation_list.append(HVAC_MODE_AUTO)
|
||||
self._support_flags = self._support_flags | SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
|
||||
# Add supported nest thermostat features
|
||||
if self.device.can_heat:
|
||||
self._operation_list.append(HVAC_MODE_HEAT)
|
||||
|
||||
if self.device.can_cool:
|
||||
self._operation_list.append(HVAC_MODE_COOL)
|
||||
|
||||
self._operation_list.append(HVAC_MODE_OFF)
|
||||
|
||||
# feature of device
|
||||
self._has_fan = self.device.has_fan
|
||||
if self._has_fan:
|
||||
self._support_flags = self._support_flags | SUPPORT_FAN_MODE
|
||||
|
||||
# data attributes
|
||||
self._away = None
|
||||
self._location = None
|
||||
self._name = None
|
||||
self._humidity = None
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
self._temperature_scale = None
|
||||
self._mode = None
|
||||
self._action = None
|
||||
self._fan = None
|
||||
self._eco_temperature = None
|
||||
self._is_locked = None
|
||||
self._locked_temperature = None
|
||||
self._min_temperature = None
|
||||
self._max_temperature = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Do not need poll thanks using Nest streaming API."""
|
||||
return False
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register update signal handler."""
|
||||
|
||||
async def async_update_state():
|
||||
"""Update device state."""
|
||||
await self.async_update_ha_state(True)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state)
|
||||
)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID for this device."""
|
||||
return self.device.serial
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return information about the device."""
|
||||
return {
|
||||
"identifiers": {(NEST_DOMAIN, self.device.device_id)},
|
||||
"name": self.device.name_long,
|
||||
"manufacturer": "Nest Labs",
|
||||
"model": "Thermostat",
|
||||
"sw_version": self.device.software_version,
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._temperature_scale
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self._mode == NEST_MODE_ECO:
|
||||
if self.device.previous_mode in MODE_NEST_TO_HASS:
|
||||
return MODE_NEST_TO_HASS[self.device.previous_mode]
|
||||
|
||||
# previous_mode not supported so return the first compatible mode
|
||||
return self._operation_list[0]
|
||||
|
||||
return MODE_NEST_TO_HASS[self._mode]
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
"""Return the current hvac action."""
|
||||
return ACTION_NEST_TO_HASS[self._action]
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._mode not in (NEST_MODE_HEAT_COOL, NEST_MODE_ECO):
|
||||
return self._target_temperature
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self._mode == NEST_MODE_ECO:
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self._mode == NEST_MODE_ECO:
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
|
||||
temp = None
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
temp = (target_temp_low, target_temp_high)
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
try:
|
||||
if temp is not None:
|
||||
self.device.target = temp
|
||||
except APIError as api_error:
|
||||
_LOGGER.error("An error occurred while setting temperature: %s", api_error)
|
||||
# restore target temperature
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set operation mode."""
|
||||
self.device.mode = MODE_HASS_TO_NEST[hvac_mode]
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""List of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return current preset mode."""
|
||||
if self._away and self._mode == NEST_MODE_ECO:
|
||||
return PRESET_AWAY_AND_ECO
|
||||
|
||||
if self._away:
|
||||
return PRESET_AWAY
|
||||
|
||||
if self._mode == NEST_MODE_ECO:
|
||||
return PRESET_ECO
|
||||
|
||||
return PRESET_NONE
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""Return preset modes."""
|
||||
return PRESET_MODES
|
||||
|
||||
def set_preset_mode(self, preset_mode):
|
||||
"""Set preset mode."""
|
||||
if preset_mode == self.preset_mode:
|
||||
return
|
||||
|
||||
need_away = preset_mode in (PRESET_AWAY, PRESET_AWAY_AND_ECO)
|
||||
need_eco = preset_mode in (PRESET_ECO, PRESET_AWAY_AND_ECO)
|
||||
is_away = self._away
|
||||
is_eco = self._mode == NEST_MODE_ECO
|
||||
|
||||
if is_away != need_away:
|
||||
self.structure.away = need_away
|
||||
|
||||
if is_eco != need_eco:
|
||||
if need_eco:
|
||||
self.device.mode = NEST_MODE_ECO
|
||||
else:
|
||||
self.device.mode = self.device.previous_mode
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
if self._has_fan:
|
||||
# Return whether the fan is on
|
||||
return FAN_ON if self._fan else FAN_AUTO
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
"""List of available fan modes."""
|
||||
if self._has_fan:
|
||||
return self._fan_modes
|
||||
return None
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Turn fan on/off."""
|
||||
if self._has_fan:
|
||||
self.device.fan = fan_mode.lower()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Identify min_temp in Nest API or defaults if not available."""
|
||||
return self._min_temperature
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Identify max_temp in Nest API or defaults if not available."""
|
||||
return self._max_temperature
|
||||
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._humidity = self.device.humidity
|
||||
self._temperature = self.device.temperature
|
||||
self._mode = self.device.mode
|
||||
self._action = self.device.hvac_state
|
||||
self._target_temperature = self.device.target
|
||||
self._fan = self.device.fan
|
||||
self._away = self.structure.away == "away"
|
||||
self._eco_temperature = self.device.eco_temperature
|
||||
self._locked_temperature = self.device.locked_temperature
|
||||
self._min_temperature = self.device.min_temperature
|
||||
self._max_temperature = self.device.max_temperature
|
||||
self._is_locked = self.device.is_locked
|
||||
if self.device.temperature_scale == "C":
|
||||
self._temperature_scale = TEMP_CELSIUS
|
||||
else:
|
||||
self._temperature_scale = TEMP_FAHRENHEIT
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
|
||||
) -> None:
|
||||
"""Set up the climate platform."""
|
||||
if DATA_SDM not in entry.data:
|
||||
await async_setup_legacy_entry(hass, entry, async_add_entities)
|
||||
return
|
||||
await async_setup_sdm_entry(hass, entry, async_add_entities)
|
||||
|
355
homeassistant/components/nest/climate_legacy.py
Normal file
355
homeassistant/components/nest/climate_legacy.py
Normal file
@ -0,0 +1,355 @@
|
||||
"""Support for Nest thermostats."""
|
||||
import logging
|
||||
|
||||
from nest.nest import APIError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
CURRENT_HVAC_COOL,
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE,
|
||||
FAN_AUTO,
|
||||
FAN_ON,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
PRESET_AWAY,
|
||||
PRESET_ECO,
|
||||
PRESET_NONE,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_SCAN_INTERVAL,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DATA_NEST, DOMAIN as NEST_DOMAIN
|
||||
from .const import SIGNAL_NEST_UPDATE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1))}
|
||||
)
|
||||
|
||||
NEST_MODE_HEAT_COOL = "heat-cool"
|
||||
NEST_MODE_ECO = "eco"
|
||||
NEST_MODE_HEAT = "heat"
|
||||
NEST_MODE_COOL = "cool"
|
||||
NEST_MODE_OFF = "off"
|
||||
|
||||
MODE_HASS_TO_NEST = {
|
||||
HVAC_MODE_AUTO: NEST_MODE_HEAT_COOL,
|
||||
HVAC_MODE_HEAT: NEST_MODE_HEAT,
|
||||
HVAC_MODE_COOL: NEST_MODE_COOL,
|
||||
HVAC_MODE_OFF: NEST_MODE_OFF,
|
||||
}
|
||||
|
||||
MODE_NEST_TO_HASS = {v: k for k, v in MODE_HASS_TO_NEST.items()}
|
||||
|
||||
ACTION_NEST_TO_HASS = {
|
||||
"off": CURRENT_HVAC_IDLE,
|
||||
"heating": CURRENT_HVAC_HEAT,
|
||||
"cooling": CURRENT_HVAC_COOL,
|
||||
}
|
||||
|
||||
PRESET_AWAY_AND_ECO = "Away and Eco"
|
||||
|
||||
PRESET_MODES = [PRESET_NONE, PRESET_AWAY, PRESET_ECO, PRESET_AWAY_AND_ECO]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Nest thermostat.
|
||||
|
||||
No longer in use.
|
||||
"""
|
||||
|
||||
|
||||
async def async_setup_legacy_entry(hass, entry, async_add_entities):
|
||||
"""Set up the Nest climate device based on a config entry."""
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
|
||||
thermostats = await hass.async_add_executor_job(hass.data[DATA_NEST].thermostats)
|
||||
|
||||
all_devices = [
|
||||
NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in thermostats
|
||||
]
|
||||
|
||||
async_add_entities(all_devices, True)
|
||||
|
||||
|
||||
class NestThermostat(ClimateEntity):
|
||||
"""Representation of a Nest thermostat."""
|
||||
|
||||
def __init__(self, structure, device, temp_unit):
|
||||
"""Initialize the thermostat."""
|
||||
self._unit = temp_unit
|
||||
self.structure = structure
|
||||
self.device = device
|
||||
self._fan_modes = [FAN_ON, FAN_AUTO]
|
||||
|
||||
# Set the default supported features
|
||||
self._support_flags = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
||||
|
||||
# Not all nest devices support cooling and heating remove unused
|
||||
self._operation_list = []
|
||||
|
||||
if self.device.can_heat and self.device.can_cool:
|
||||
self._operation_list.append(HVAC_MODE_AUTO)
|
||||
self._support_flags |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
|
||||
# Add supported nest thermostat features
|
||||
if self.device.can_heat:
|
||||
self._operation_list.append(HVAC_MODE_HEAT)
|
||||
|
||||
if self.device.can_cool:
|
||||
self._operation_list.append(HVAC_MODE_COOL)
|
||||
|
||||
self._operation_list.append(HVAC_MODE_OFF)
|
||||
|
||||
# feature of device
|
||||
self._has_fan = self.device.has_fan
|
||||
if self._has_fan:
|
||||
self._support_flags |= SUPPORT_FAN_MODE
|
||||
|
||||
# data attributes
|
||||
self._away = None
|
||||
self._location = None
|
||||
self._name = None
|
||||
self._humidity = None
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
self._temperature_scale = None
|
||||
self._mode = None
|
||||
self._action = None
|
||||
self._fan = None
|
||||
self._eco_temperature = None
|
||||
self._is_locked = None
|
||||
self._locked_temperature = None
|
||||
self._min_temperature = None
|
||||
self._max_temperature = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Do not need poll thanks using Nest streaming API."""
|
||||
return False
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register update signal handler."""
|
||||
|
||||
async def async_update_state():
|
||||
"""Update device state."""
|
||||
await self.async_update_ha_state(True)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state)
|
||||
)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID for this device."""
|
||||
return self.device.serial
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return information about the device."""
|
||||
return {
|
||||
"identifiers": {(NEST_DOMAIN, self.device.device_id)},
|
||||
"name": self.device.name_long,
|
||||
"manufacturer": "Nest Labs",
|
||||
"model": "Thermostat",
|
||||
"sw_version": self.device.software_version,
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._temperature_scale
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self._mode == NEST_MODE_ECO:
|
||||
if self.device.previous_mode in MODE_NEST_TO_HASS:
|
||||
return MODE_NEST_TO_HASS[self.device.previous_mode]
|
||||
|
||||
# previous_mode not supported so return the first compatible mode
|
||||
return self._operation_list[0]
|
||||
|
||||
return MODE_NEST_TO_HASS[self._mode]
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
"""Return the current hvac action."""
|
||||
return ACTION_NEST_TO_HASS[self._action]
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._mode not in (NEST_MODE_HEAT_COOL, NEST_MODE_ECO):
|
||||
return self._target_temperature
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self._mode == NEST_MODE_ECO:
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self._mode == NEST_MODE_ECO:
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
|
||||
temp = None
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
temp = (target_temp_low, target_temp_high)
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
try:
|
||||
if temp is not None:
|
||||
self.device.target = temp
|
||||
except APIError as api_error:
|
||||
_LOGGER.error("An error occurred while setting temperature: %s", api_error)
|
||||
# restore target temperature
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set operation mode."""
|
||||
self.device.mode = MODE_HASS_TO_NEST[hvac_mode]
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""List of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return current preset mode."""
|
||||
if self._away and self._mode == NEST_MODE_ECO:
|
||||
return PRESET_AWAY_AND_ECO
|
||||
|
||||
if self._away:
|
||||
return PRESET_AWAY
|
||||
|
||||
if self._mode == NEST_MODE_ECO:
|
||||
return PRESET_ECO
|
||||
|
||||
return PRESET_NONE
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""Return preset modes."""
|
||||
return PRESET_MODES
|
||||
|
||||
def set_preset_mode(self, preset_mode):
|
||||
"""Set preset mode."""
|
||||
if preset_mode == self.preset_mode:
|
||||
return
|
||||
|
||||
need_away = preset_mode in (PRESET_AWAY, PRESET_AWAY_AND_ECO)
|
||||
need_eco = preset_mode in (PRESET_ECO, PRESET_AWAY_AND_ECO)
|
||||
is_away = self._away
|
||||
is_eco = self._mode == NEST_MODE_ECO
|
||||
|
||||
if is_away != need_away:
|
||||
self.structure.away = need_away
|
||||
|
||||
if is_eco != need_eco:
|
||||
if need_eco:
|
||||
self.device.mode = NEST_MODE_ECO
|
||||
else:
|
||||
self.device.mode = self.device.previous_mode
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
if self._has_fan:
|
||||
# Return whether the fan is on
|
||||
return FAN_ON if self._fan else FAN_AUTO
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
"""List of available fan modes."""
|
||||
if self._has_fan:
|
||||
return self._fan_modes
|
||||
return None
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Turn fan on/off."""
|
||||
if self._has_fan:
|
||||
self.device.fan = fan_mode.lower()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Identify min_temp in Nest API or defaults if not available."""
|
||||
return self._min_temperature
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Identify max_temp in Nest API or defaults if not available."""
|
||||
return self._max_temperature
|
||||
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._humidity = self.device.humidity
|
||||
self._temperature = self.device.temperature
|
||||
self._mode = self.device.mode
|
||||
self._action = self.device.hvac_state
|
||||
self._target_temperature = self.device.target
|
||||
self._fan = self.device.fan
|
||||
self._away = self.structure.away == "away"
|
||||
self._eco_temperature = self.device.eco_temperature
|
||||
self._locked_temperature = self.device.locked_temperature
|
||||
self._min_temperature = self.device.min_temperature
|
||||
self._max_temperature = self.device.max_temperature
|
||||
self._is_locked = self.device.is_locked
|
||||
if self.device.temperature_scale == "C":
|
||||
self._temperature_scale = TEMP_CELSIUS
|
||||
else:
|
||||
self._temperature_scale = TEMP_FAHRENHEIT
|
345
homeassistant/components/nest/climate_sdm.py
Normal file
345
homeassistant/components/nest/climate_sdm.py
Normal file
@ -0,0 +1,345 @@
|
||||
"""Support for Google Nest SDM climate devices."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from google_nest_sdm.device import Device
|
||||
from google_nest_sdm.device_traits import FanTrait, TemperatureTrait
|
||||
from google_nest_sdm.thermostat_traits import (
|
||||
ThermostatEcoTrait,
|
||||
ThermostatHvacTrait,
|
||||
ThermostatModeTrait,
|
||||
ThermostatTemperatureSetpointTrait,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
CURRENT_HVAC_COOL,
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_OFF,
|
||||
FAN_OFF,
|
||||
FAN_ON,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
PRESET_ECO,
|
||||
PRESET_NONE,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .const import DOMAIN, SIGNAL_NEST_UPDATE
|
||||
from .device_info import DeviceInfo
|
||||
|
||||
# Mapping for sdm.devices.traits.ThermostatMode mode field
|
||||
THERMOSTAT_MODE_MAP = {
|
||||
"OFF": HVAC_MODE_OFF,
|
||||
"HEAT": HVAC_MODE_HEAT,
|
||||
"COOL": HVAC_MODE_COOL,
|
||||
"HEATCOOL": HVAC_MODE_HEAT_COOL,
|
||||
"MANUAL_ECO": HVAC_MODE_AUTO,
|
||||
}
|
||||
THERMOSTAT_INV_MODE_MAP = {v: k for k, v in THERMOSTAT_MODE_MAP.items()}
|
||||
|
||||
# Mode for sdm.devices.traits.ThermostatEco
|
||||
THERMOSTAT_ECO_MODE = "MANUAL_ECO"
|
||||
|
||||
# Mapping for sdm.devices.traits.ThermostatHvac status field
|
||||
THERMOSTAT_HVAC_STATUS_MAP = {
|
||||
"OFF": CURRENT_HVAC_OFF,
|
||||
"HEATING": CURRENT_HVAC_HEAT,
|
||||
"COOLING": CURRENT_HVAC_COOL,
|
||||
}
|
||||
|
||||
# Mapping to determine the trait that supports the target temperatures
|
||||
# based on the current HVAC mode
|
||||
THERMOSTAT_SETPOINT_TRAIT_MAP = {
|
||||
HVAC_MODE_COOL: ThermostatTemperatureSetpointTrait.NAME,
|
||||
HVAC_MODE_HEAT: ThermostatTemperatureSetpointTrait.NAME,
|
||||
HVAC_MODE_HEAT_COOL: ThermostatTemperatureSetpointTrait.NAME,
|
||||
HVAC_MODE_AUTO: ThermostatEcoTrait.NAME,
|
||||
}
|
||||
THERMOSTAT_RANGE_MODES = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO]
|
||||
|
||||
PRESET_MODE_MAP = {
|
||||
"MANUAL_ECO": PRESET_ECO,
|
||||
"OFF": PRESET_NONE,
|
||||
}
|
||||
PRESET_INV_MODE_MAP = {v: k for k, v in PRESET_MODE_MAP.items()}
|
||||
|
||||
FAN_MODE_MAP = {
|
||||
"ON": FAN_ON,
|
||||
"OFF": FAN_OFF,
|
||||
}
|
||||
FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()}
|
||||
|
||||
|
||||
async def async_setup_sdm_entry(
|
||||
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
|
||||
) -> None:
|
||||
"""Set up the client entities."""
|
||||
|
||||
subscriber = hass.data[DOMAIN][entry.entry_id]
|
||||
device_manager = await subscriber.async_get_device_manager()
|
||||
|
||||
entities = []
|
||||
for device in device_manager.devices.values():
|
||||
if ThermostatHvacTrait.NAME in device.traits:
|
||||
entities.append(ThermostatEntity(device))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ThermostatEntity(ClimateEntity):
|
||||
"""A nest thermostat climate entity."""
|
||||
|
||||
def __init__(self, device: Device):
|
||||
"""Initialize ThermostatEntity."""
|
||||
self._device = device
|
||||
self._device_info = DeviceInfo(device)
|
||||
self._supported_features = 0
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Disable polling since entities have state pushed via pubsub."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self) -> Optional[str]:
|
||||
"""Return a unique ID."""
|
||||
# The API "name" field is a unique device identifier.
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._device_info.device_name
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device specific attributes."""
|
||||
return self._device_info.device_info
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity is added to register update signal handler."""
|
||||
# Event messages trigger the SIGNAL_NEST_UPDATE, which is intercepted
|
||||
# here to re-fresh the signals from _device. Unregister this callback
|
||||
# when the entity is removed.
|
||||
self._supported_features = self._get_supported_features()
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_NEST_UPDATE,
|
||||
self.async_write_ha_state,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of temperature measurement for the system."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if TemperatureTrait.NAME not in self._device.traits:
|
||||
return None
|
||||
trait = self._device.traits[TemperatureTrait.NAME]
|
||||
return trait.ambient_temperature_celsius
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature currently set to be reached."""
|
||||
trait = self._target_temperature_trait
|
||||
if not trait:
|
||||
return None
|
||||
if self.hvac_mode == HVAC_MODE_HEAT:
|
||||
return trait.heat_celsius
|
||||
if self.hvac_mode == HVAC_MODE_COOL:
|
||||
return trait.cool_celsius
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound target temperature."""
|
||||
if self.hvac_mode not in THERMOSTAT_RANGE_MODES:
|
||||
return None
|
||||
trait = self._target_temperature_trait
|
||||
if not trait:
|
||||
return None
|
||||
return trait.cool_celsius
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound target temperature."""
|
||||
if self.hvac_mode not in THERMOSTAT_RANGE_MODES:
|
||||
return None
|
||||
trait = self._target_temperature_trait
|
||||
if not trait:
|
||||
return None
|
||||
return trait.heat_celsius
|
||||
|
||||
@property
|
||||
def _target_temperature_trait(self):
|
||||
"""Return the correct trait with a target temp depending on mode."""
|
||||
if not self.hvac_mode:
|
||||
return None
|
||||
if self.hvac_mode not in THERMOSTAT_SETPOINT_TRAIT_MAP:
|
||||
return None
|
||||
trait_name = THERMOSTAT_SETPOINT_TRAIT_MAP[self.hvac_mode]
|
||||
if trait_name not in self._device.traits:
|
||||
return None
|
||||
return self._device.traits[trait_name]
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return the current operation (e.g. heat, cool, idle)."""
|
||||
if ThermostatEcoTrait.NAME in self._device.traits:
|
||||
trait = self._device.traits[ThermostatEcoTrait.NAME]
|
||||
if trait.mode == THERMOSTAT_ECO_MODE:
|
||||
return HVAC_MODE_AUTO
|
||||
if ThermostatModeTrait.NAME in self._device.traits:
|
||||
trait = self._device.traits[ThermostatModeTrait.NAME]
|
||||
if trait.mode in THERMOSTAT_MODE_MAP:
|
||||
return THERMOSTAT_MODE_MAP[trait.mode]
|
||||
return HVAC_MODE_OFF
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""List of available operation modes."""
|
||||
supported_modes = []
|
||||
for mode in self._get_device_hvac_modes:
|
||||
if mode in THERMOSTAT_MODE_MAP:
|
||||
supported_modes.append(THERMOSTAT_MODE_MAP[mode])
|
||||
return supported_modes
|
||||
|
||||
@property
|
||||
def _get_device_hvac_modes(self):
|
||||
"""Return the set of SDM API hvac modes supported by the device."""
|
||||
modes = []
|
||||
if ThermostatModeTrait.NAME in self._device.traits:
|
||||
trait = self._device.traits[ThermostatModeTrait.NAME]
|
||||
modes.extend(trait.available_modes)
|
||||
if ThermostatEcoTrait.NAME in self._device.traits:
|
||||
trait = self._device.traits[ThermostatEcoTrait.NAME]
|
||||
modes.extend(trait.available_modes)
|
||||
return set(modes)
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
"""Return the current HVAC action (heating, cooling)."""
|
||||
if ThermostatHvacTrait.NAME not in self._device.traits:
|
||||
return None
|
||||
trait = self._device.traits[ThermostatHvacTrait.NAME]
|
||||
if trait.status in THERMOSTAT_HVAC_STATUS_MAP:
|
||||
return THERMOSTAT_HVAC_STATUS_MAP[trait.status]
|
||||
return None
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return the current active preset."""
|
||||
if ThermostatEcoTrait.NAME in self._device.traits:
|
||||
trait = self._device.traits[ThermostatEcoTrait.NAME]
|
||||
return PRESET_MODE_MAP.get(trait.mode, PRESET_NONE)
|
||||
return PRESET_NONE
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""Return the available presets."""
|
||||
modes = []
|
||||
if ThermostatEcoTrait.NAME in self._device.traits:
|
||||
trait = self._device.traits[ThermostatEcoTrait.NAME]
|
||||
for mode in trait.available_modes:
|
||||
if mode in PRESET_MODE_MAP:
|
||||
modes.append(PRESET_MODE_MAP[mode])
|
||||
return modes
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
"""Return the current fan mode."""
|
||||
if FanTrait.NAME in self._device.traits:
|
||||
trait = self._device.traits[FanTrait.NAME]
|
||||
return FAN_MODE_MAP.get(trait.timer_mode, FAN_OFF)
|
||||
return FAN_OFF
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
"""Return the list of available fan modes."""
|
||||
if FanTrait.NAME in self._device.traits:
|
||||
return list(FAN_INV_MODE_MAP.keys())
|
||||
return []
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Bitmap of supported features."""
|
||||
return self._supported_features
|
||||
|
||||
def _get_supported_features(self):
|
||||
"""Compute the bitmap of supported features from the current state."""
|
||||
features = 0
|
||||
if HVAC_MODE_HEAT_COOL in self.hvac_modes or HVAC_MODE_AUTO in self.hvac_modes:
|
||||
features |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
if HVAC_MODE_HEAT in self.hvac_modes or HVAC_MODE_COOL in self.hvac_modes:
|
||||
features |= SUPPORT_TARGET_TEMPERATURE
|
||||
if ThermostatEcoTrait.NAME in self._device.traits:
|
||||
features |= SUPPORT_PRESET_MODE
|
||||
if FanTrait.NAME in self._device.traits:
|
||||
# Fan trait may be present without actually support fan mode
|
||||
fan_trait = self._device.traits[FanTrait.NAME]
|
||||
if fan_trait.timer_mode is not None:
|
||||
features |= SUPPORT_FAN_MODE
|
||||
return features
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
if hvac_mode not in self.hvac_modes:
|
||||
return
|
||||
if hvac_mode not in THERMOSTAT_INV_MODE_MAP:
|
||||
return
|
||||
api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode]
|
||||
if ThermostatModeTrait.NAME not in self._device.traits:
|
||||
return
|
||||
trait = self._device.traits[ThermostatModeTrait.NAME]
|
||||
await trait.set_mode(api_mode)
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
if ThermostatTemperatureSetpointTrait.NAME not in self._device.traits:
|
||||
return
|
||||
trait = self._device.traits[ThermostatTemperatureSetpointTrait.NAME]
|
||||
if self.preset_mode == PRESET_ECO or self.hvac_mode == HVAC_MODE_HEAT_COOL:
|
||||
if low_temp and high_temp:
|
||||
return await trait.set_range(low_temp, high_temp)
|
||||
elif self.hvac_mode == HVAC_MODE_COOL and temp:
|
||||
return await trait.set_cool(temp)
|
||||
elif self.hvac_mode == HVAC_MODE_HEAT and temp:
|
||||
return await trait.set_heat(temp)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode):
|
||||
"""Set new target preset mode."""
|
||||
if preset_mode not in self.preset_modes:
|
||||
return
|
||||
if ThermostatEcoTrait.NAME not in self._device.traits:
|
||||
return
|
||||
trait = self._device.traits[ThermostatEcoTrait.NAME]
|
||||
await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode])
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
if fan_mode not in self.fan_modes:
|
||||
return
|
||||
if FanTrait.NAME not in self._device.traits:
|
||||
return
|
||||
trait = self._device.traits[FanTrait.NAME]
|
||||
await trait.set_timer(FAN_INV_MODE_MAP[fan_mode])
|
@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nest",
|
||||
"requirements": [
|
||||
"python-nest==4.1.0",
|
||||
"google-nest-sdm==0.1.9"
|
||||
"google-nest-sdm==0.1.12"
|
||||
],
|
||||
"codeowners": [
|
||||
"@awarecan",
|
||||
|
@ -684,7 +684,7 @@ google-cloud-pubsub==2.1.0
|
||||
google-cloud-texttospeech==0.4.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==0.1.9
|
||||
google-nest-sdm==0.1.12
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
|
@ -352,7 +352,7 @@ google-api-python-client==1.6.4
|
||||
google-cloud-pubsub==2.1.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==0.1.9
|
||||
google-nest-sdm==0.1.12
|
||||
|
||||
# homeassistant.components.gree
|
||||
greeclimate==0.9.5
|
||||
|
829
tests/components/nest/climate_sdm_test.py
Normal file
829
tests/components/nest/climate_sdm_test.py
Normal file
@ -0,0 +1,829 @@
|
||||
"""
|
||||
Test for Nest climate platform for the Smart Device Management API.
|
||||
|
||||
These tests fake out the subscriber/devicemanager, and are not using a real
|
||||
pubsub subscriber.
|
||||
"""
|
||||
|
||||
from google_nest_sdm.device import Device
|
||||
from google_nest_sdm.event import EventMessage
|
||||
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_FAN_MODES,
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_PRESET_MODES,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
CURRENT_HVAC_COOL,
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_OFF,
|
||||
FAN_OFF,
|
||||
FAN_ON,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
PRESET_ECO,
|
||||
PRESET_NONE,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
|
||||
from .common import async_setup_sdm_platform
|
||||
|
||||
from tests.components.climate import common
|
||||
|
||||
PLATFORM = "climate"
|
||||
|
||||
|
||||
async def setup_climate(hass, raw_traits=None, auth=None):
|
||||
"""Load Nest climate devices."""
|
||||
devices = None
|
||||
if raw_traits:
|
||||
traits = raw_traits
|
||||
traits["sdm.devices.traits.Info"] = {"customName": "My Thermostat"}
|
||||
devices = {
|
||||
"some-device-id": Device.MakeDevice(
|
||||
{
|
||||
"name": "some-device-id",
|
||||
"type": "sdm.devices.types.Thermostat",
|
||||
"traits": traits,
|
||||
},
|
||||
auth=auth,
|
||||
),
|
||||
}
|
||||
return await async_setup_sdm_platform(hass, PLATFORM, devices)
|
||||
|
||||
|
||||
async def test_no_devices(hass):
|
||||
"""Test no devices returned by the api."""
|
||||
await setup_climate(hass)
|
||||
assert len(hass.states.async_all()) == 0
|
||||
|
||||
|
||||
async def test_climate_devices(hass):
|
||||
"""Test no eligible climate devices returned by the api."""
|
||||
await setup_climate(hass, {"sdm.devices.traits.CameraImage": {}})
|
||||
assert len(hass.states.async_all()) == 0
|
||||
|
||||
|
||||
async def test_thermostat_off(hass):
|
||||
"""Test a thermostat that is not running."""
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "OFF",
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 16.2,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_OFF
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2
|
||||
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
}
|
||||
assert thermostat.attributes[ATTR_TEMPERATURE] is None
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None
|
||||
assert ATTR_PRESET_MODE not in thermostat.attributes
|
||||
assert ATTR_PRESET_MODES not in thermostat.attributes
|
||||
assert ATTR_FAN_MODE not in thermostat.attributes
|
||||
assert ATTR_FAN_MODES not in thermostat.attributes
|
||||
|
||||
|
||||
async def test_thermostat_heat(hass):
|
||||
"""Test a thermostat that is heating."""
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {
|
||||
"status": "HEATING",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "HEAT",
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 16.2,
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": {
|
||||
"heatCelsius": 22.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_HEAT
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT
|
||||
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2
|
||||
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
}
|
||||
assert thermostat.attributes[ATTR_TEMPERATURE] == 22.0
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None
|
||||
assert ATTR_PRESET_MODE not in thermostat.attributes
|
||||
assert ATTR_PRESET_MODES not in thermostat.attributes
|
||||
|
||||
|
||||
async def test_thermostat_cool(hass):
|
||||
"""Test a thermostat that is cooling."""
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {
|
||||
"status": "COOLING",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "COOL",
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 29.9,
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": {
|
||||
"coolCelsius": 28.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_COOL
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL
|
||||
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 29.9
|
||||
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
}
|
||||
assert thermostat.attributes[ATTR_TEMPERATURE] == 28.0
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None
|
||||
assert ATTR_PRESET_MODE not in thermostat.attributes
|
||||
assert ATTR_PRESET_MODES not in thermostat.attributes
|
||||
|
||||
|
||||
async def test_thermostat_heatcool(hass):
|
||||
"""Test a thermostat that is cooling in heatcool mode."""
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {
|
||||
"status": "COOLING",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "HEATCOOL",
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 29.9,
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": {
|
||||
"heatCelsius": 22.0,
|
||||
"coolCelsius": 28.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_HEAT_COOL
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL
|
||||
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 29.9
|
||||
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
}
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 22.0
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 28.0
|
||||
assert thermostat.attributes[ATTR_TEMPERATURE] is None
|
||||
assert ATTR_PRESET_MODE not in thermostat.attributes
|
||||
assert ATTR_PRESET_MODES not in thermostat.attributes
|
||||
|
||||
|
||||
async def test_thermostat_eco_off(hass):
|
||||
"""Test a thermostat cooling with eco off."""
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {
|
||||
"status": "COOLING",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "HEATCOOL",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatEco": {
|
||||
"availableModes": ["MANUAL_ECO", "OFF"],
|
||||
"mode": "OFF",
|
||||
"heatCelsius": 20.0,
|
||||
"coolCelsius": 22.0,
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 29.9,
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": {
|
||||
"heatCelsius": 22.0,
|
||||
"coolCelsius": 28.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_HEAT_COOL
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL
|
||||
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 29.9
|
||||
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_OFF,
|
||||
}
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 22.0
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 28.0
|
||||
assert thermostat.attributes[ATTR_TEMPERATURE] is None
|
||||
assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE
|
||||
assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE]
|
||||
|
||||
|
||||
async def test_thermostat_eco_on(hass):
|
||||
"""Test a thermostat in eco mode."""
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {
|
||||
"status": "COOLING",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "HEATCOOL",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatEco": {
|
||||
"availableModes": ["MANUAL_ECO", "OFF"],
|
||||
"mode": "MANUAL_ECO",
|
||||
"heatCelsius": 21.0,
|
||||
"coolCelsius": 29.0,
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 29.9,
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": {
|
||||
"heatCelsius": 22.0,
|
||||
"coolCelsius": 28.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_AUTO
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL
|
||||
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 29.9
|
||||
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_OFF,
|
||||
}
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 21.0
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 29.0
|
||||
assert thermostat.attributes[ATTR_TEMPERATURE] is None
|
||||
assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_ECO
|
||||
assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE]
|
||||
|
||||
|
||||
class FakeAuth:
|
||||
"""A fake implementation of the auth class that records requests."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize FakeAuth."""
|
||||
self.method = None
|
||||
self.url = None
|
||||
self.json = None
|
||||
|
||||
async def request(self, method, url, json):
|
||||
"""Capure the request arguments for tests to assert on."""
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.json = json
|
||||
|
||||
|
||||
async def test_thermostat_set_hvac_mode(hass):
|
||||
"""Test a thermostat changing hvac modes."""
|
||||
auth = FakeAuth()
|
||||
subscriber = await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "OFF",
|
||||
},
|
||||
},
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_OFF
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
|
||||
await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert auth.method == "post"
|
||||
assert auth.url == "some-device-id:executeCommand"
|
||||
assert auth.json == {
|
||||
"command": "sdm.devices.commands.ThermostatMode.SetMode",
|
||||
"params": {"mode": "HEAT"},
|
||||
}
|
||||
|
||||
# Local state does not reflect the update
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_OFF
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
|
||||
# Simulate pubsub message when mode changes
|
||||
event = EventMessage(
|
||||
{
|
||||
"eventId": "some-event-id",
|
||||
"timestamp": "2019-01-01T00:00:01Z",
|
||||
"resourceUpdate": {
|
||||
"name": "some-device-id",
|
||||
"traits": {
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "HEAT",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
auth=None,
|
||||
)
|
||||
subscriber.receive_event(event)
|
||||
await hass.async_block_till_done() # Process dispatch/update signal
|
||||
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_HEAT
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
|
||||
# Simulate pubsub message when the thermostat starts heating
|
||||
event = EventMessage(
|
||||
{
|
||||
"eventId": "some-event-id",
|
||||
"timestamp": "2019-01-01T00:00:01Z",
|
||||
"resourceUpdate": {
|
||||
"name": "some-device-id",
|
||||
"traits": {
|
||||
"sdm.devices.traits.ThermostatHvac": {
|
||||
"status": "HEATING",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
auth=None,
|
||||
)
|
||||
subscriber.receive_event(event)
|
||||
await hass.async_block_till_done() # Process dispatch/update signal
|
||||
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_HEAT
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT
|
||||
|
||||
|
||||
async def test_thermostat_set_eco_preset(hass):
|
||||
"""Test a thermostat put into eco mode."""
|
||||
auth = FakeAuth()
|
||||
subscriber = await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
|
||||
"sdm.devices.traits.ThermostatEco": {
|
||||
"availableModes": ["MANUAL_ECO", "OFF"],
|
||||
"mode": "OFF",
|
||||
"heatCelsius": 15.0,
|
||||
"coolCelsius": 28.0,
|
||||
},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "OFF",
|
||||
},
|
||||
},
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_OFF
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE
|
||||
|
||||
# Turn on eco mode
|
||||
await common.async_set_preset_mode(hass, PRESET_ECO)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert auth.method == "post"
|
||||
assert auth.url == "some-device-id:executeCommand"
|
||||
assert auth.json == {
|
||||
"command": "sdm.devices.commands.ThermostatEco.SetMode",
|
||||
"params": {"mode": "MANUAL_ECO"},
|
||||
}
|
||||
|
||||
# Local state does not reflect the update
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_OFF
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE
|
||||
|
||||
# Simulate pubsub message when mode changes
|
||||
event = EventMessage(
|
||||
{
|
||||
"eventId": "some-event-id",
|
||||
"timestamp": "2019-01-01T00:00:01Z",
|
||||
"resourceUpdate": {
|
||||
"name": "some-device-id",
|
||||
"traits": {
|
||||
"sdm.devices.traits.ThermostatEco": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "MANUAL_ECO",
|
||||
"heatCelsius": 15.0,
|
||||
"coolCelsius": 28.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
auth=auth,
|
||||
)
|
||||
subscriber.receive_event(event)
|
||||
await hass.async_block_till_done() # Process dispatch/update signal
|
||||
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_AUTO
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_ECO
|
||||
|
||||
# Turn off eco mode
|
||||
await common.async_set_preset_mode(hass, PRESET_NONE)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert auth.method == "post"
|
||||
assert auth.url == "some-device-id:executeCommand"
|
||||
assert auth.json == {
|
||||
"command": "sdm.devices.commands.ThermostatEco.SetMode",
|
||||
"params": {"mode": "OFF"},
|
||||
}
|
||||
|
||||
|
||||
async def test_thermostat_set_cool(hass):
|
||||
"""Test a thermostat in cool mode with a temperature change."""
|
||||
auth = FakeAuth()
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "COOL",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": {
|
||||
"coolCelsius": 25.0,
|
||||
},
|
||||
},
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_COOL
|
||||
|
||||
await common.async_set_temperature(hass, temperature=24.0)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert auth.method == "post"
|
||||
assert auth.url == "some-device-id:executeCommand"
|
||||
assert auth.json == {
|
||||
"command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetCool",
|
||||
"params": {"coolCelsius": 24.0},
|
||||
}
|
||||
|
||||
|
||||
async def test_thermostat_set_heat(hass):
|
||||
"""Test a thermostat heating mode with a temperature change."""
|
||||
auth = FakeAuth()
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "HEAT",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": {
|
||||
"heatCelsius": 19.0,
|
||||
},
|
||||
},
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_HEAT
|
||||
|
||||
await common.async_set_temperature(hass, temperature=20.0)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert auth.method == "post"
|
||||
assert auth.url == "some-device-id:executeCommand"
|
||||
assert auth.json == {
|
||||
"command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetHeat",
|
||||
"params": {"heatCelsius": 20.0},
|
||||
}
|
||||
|
||||
|
||||
async def test_thermostat_set_heat_cool(hass):
|
||||
"""Test a thermostat in heatcool mode with a temperature change."""
|
||||
auth = FakeAuth()
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "HEATCOOL",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": {
|
||||
"heatCelsius": 19.0,
|
||||
"coolCelsius": 25.0,
|
||||
},
|
||||
},
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_HEAT_COOL
|
||||
|
||||
await common.async_set_temperature(
|
||||
hass, target_temp_low=20.0, target_temp_high=24.0
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert auth.method == "post"
|
||||
assert auth.url == "some-device-id:executeCommand"
|
||||
assert auth.json == {
|
||||
"command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetRange",
|
||||
"params": {"heatCelsius": 20.0, "coolCelsius": 24.0},
|
||||
}
|
||||
|
||||
|
||||
async def test_thermostat_fan_off(hass):
|
||||
"""Test a thermostat with the fan not running."""
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.Fan": {
|
||||
"timerMode": "OFF",
|
||||
"timerTimeout": "2019-05-10T03:22:54Z",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "OFF",
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 16.2,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_OFF
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2
|
||||
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
}
|
||||
assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF
|
||||
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
|
||||
|
||||
|
||||
async def test_thermostat_fan_on(hass):
|
||||
"""Test a thermostat with the fan running."""
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.Fan": {
|
||||
"timerMode": "ON",
|
||||
"timerTimeout": "2019-05-10T03:22:54Z",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatHvac": {
|
||||
"status": "OFF",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "OFF",
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 16.2,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_OFF
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2
|
||||
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
}
|
||||
assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON
|
||||
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
|
||||
|
||||
|
||||
async def test_thermostat_set_fan(hass):
|
||||
"""Test a thermostat enabling the fan."""
|
||||
auth = FakeAuth()
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.Fan": {
|
||||
"timerMode": "ON",
|
||||
"timerTimeout": "2019-05-10T03:22:54Z",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatHvac": {
|
||||
"status": "OFF",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "OFF",
|
||||
},
|
||||
},
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_OFF
|
||||
assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON
|
||||
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
|
||||
|
||||
# Turn off fan mode
|
||||
await common.async_set_fan_mode(hass, FAN_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert auth.method == "post"
|
||||
assert auth.url == "some-device-id:executeCommand"
|
||||
assert auth.json == {
|
||||
"command": "sdm.devices.commands.Fan.SetTimer",
|
||||
"params": {"timerMode": "OFF"},
|
||||
}
|
||||
|
||||
|
||||
async def test_thermostat_fan_empty(hass):
|
||||
"""Test a fan trait with an empty response."""
|
||||
await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.Fan": {},
|
||||
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "OFF",
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 16.2,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_OFF
|
||||
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
|
||||
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2
|
||||
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
}
|
||||
assert ATTR_FAN_MODE not in thermostat.attributes
|
||||
assert ATTR_FAN_MODES not in thermostat.attributes
|
||||
|
||||
|
||||
async def test_thermostat_target_temp(hass):
|
||||
"""Test a thermostat changing hvac modes and affected on target temps."""
|
||||
auth = FakeAuth()
|
||||
subscriber = await setup_climate(
|
||||
hass,
|
||||
{
|
||||
"sdm.devices.traits.ThermostatHvac": {
|
||||
"status": "HEATING",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "HEAT",
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 20.1,
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": {
|
||||
"heatCelsius": 23.0,
|
||||
},
|
||||
},
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_HEAT
|
||||
assert thermostat.attributes[ATTR_TEMPERATURE] == 23.0
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None
|
||||
|
||||
# Simulate pubsub message changing modes
|
||||
event = EventMessage(
|
||||
{
|
||||
"eventId": "some-event-id",
|
||||
"timestamp": "2019-01-01T00:00:01Z",
|
||||
"resourceUpdate": {
|
||||
"name": "some-device-id",
|
||||
"traits": {
|
||||
"sdm.devices.traits.ThermostatMode": {
|
||||
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
|
||||
"mode": "HEATCOOL",
|
||||
},
|
||||
"sdm.devices.traits.ThermostatTemperatureSetpoint": {
|
||||
"heatCelsius": 22.0,
|
||||
"coolCelsius": 28.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
auth=None,
|
||||
)
|
||||
subscriber.receive_event(event)
|
||||
await hass.async_block_till_done() # Process dispatch/update signal
|
||||
|
||||
thermostat = hass.states.get("climate.my_thermostat")
|
||||
assert thermostat is not None
|
||||
assert thermostat.state == HVAC_MODE_HEAT_COOL
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 22.0
|
||||
assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 28.0
|
||||
assert thermostat.attributes[ATTR_TEMPERATURE] is None
|
Loading…
x
Reference in New Issue
Block a user