Add nest climate support for SDM API (#42702)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Allen Porter 2020-11-06 00:57:02 -08:00 committed by GitHub
parent 3c84c7ccf0
commit a5da98680a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1548 additions and 355 deletions

View File

@ -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

View File

@ -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)

View 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

View 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])

View File

@ -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",

View File

@ -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

View File

@ -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

View 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