"""Support for Honeywell Lyric climate platform.""" from __future__ import annotations import logging from time import localtime, strftime, time from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import voluptuous as vol from homeassistant.components.climate import ClimateEntity, ClimateEntityDescription from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import LyricDeviceEntity from .const import ( DOMAIN, LYRIC_EXCEPTIONS, PRESET_HOLD_UNTIL, PRESET_NO_HOLD, PRESET_PERMANENT_HOLD, PRESET_TEMPORARY_HOLD, PRESET_VACATION_HOLD, ) _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE LYRIC_HVAC_ACTION_OFF = "EquipmentOff" LYRIC_HVAC_ACTION_HEAT = "Heat" LYRIC_HVAC_ACTION_COOL = "Cool" LYRIC_HVAC_MODE_OFF = "Off" LYRIC_HVAC_MODE_HEAT = "Heat" LYRIC_HVAC_MODE_COOL = "Cool" LYRIC_HVAC_MODE_HEAT_COOL = "Auto" LYRIC_HVAC_MODES = { HVAC_MODE_OFF: LYRIC_HVAC_MODE_OFF, HVAC_MODE_HEAT: LYRIC_HVAC_MODE_HEAT, HVAC_MODE_COOL: LYRIC_HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL: LYRIC_HVAC_MODE_HEAT_COOL, } HVAC_MODES = { LYRIC_HVAC_MODE_OFF: HVAC_MODE_OFF, LYRIC_HVAC_MODE_HEAT: HVAC_MODE_HEAT, LYRIC_HVAC_MODE_COOL: HVAC_MODE_COOL, LYRIC_HVAC_MODE_HEAT_COOL: HVAC_MODE_HEAT_COOL, } HVAC_ACTIONS = { LYRIC_HVAC_ACTION_OFF: CURRENT_HVAC_OFF, LYRIC_HVAC_ACTION_HEAT: CURRENT_HVAC_HEAT, LYRIC_HVAC_ACTION_COOL: CURRENT_HVAC_COOL, } SERVICE_HOLD_TIME = "set_hold_time" ATTR_TIME_PERIOD = "time_period" SCHEMA_HOLD_TIME = { vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( cv.time_period, cv.positive_timedelta, lambda td: strftime("%H:%M:%S", localtime(time() + td.total_seconds())), ) } async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Honeywell Lyric climate platform based on a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities = [] for location in coordinator.data.locations: for device in location.devices: entities.append( LyricClimate( coordinator, ClimateEntityDescription( key=f"{device.macID}_thermostat", name=device.name, ), location, device, hass.config.units.temperature_unit, ) ) async_add_entities(entities, True) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_HOLD_TIME, SCHEMA_HOLD_TIME, "async_set_hold_time", ) class LyricClimate(LyricDeviceEntity, ClimateEntity): """Defines a Honeywell Lyric climate entity.""" coordinator: DataUpdateCoordinator entity_description: ClimateEntityDescription def __init__( self, coordinator: DataUpdateCoordinator, description: ClimateEntityDescription, location: LyricLocation, device: LyricDevice, temperature_unit: str, ) -> None: """Initialize Honeywell Lyric climate entity.""" self._temperature_unit = temperature_unit # Setup supported hvac modes self._hvac_modes = [HVAC_MODE_OFF] # Add supported lyric thermostat features if LYRIC_HVAC_MODE_HEAT in device.allowedModes: self._hvac_modes.append(HVAC_MODE_HEAT) if LYRIC_HVAC_MODE_COOL in device.allowedModes: self._hvac_modes.append(HVAC_MODE_COOL) if ( LYRIC_HVAC_MODE_HEAT in device.allowedModes and LYRIC_HVAC_MODE_COOL in device.allowedModes ): self._hvac_modes.append(HVAC_MODE_HEAT_COOL) super().__init__( coordinator, location, device, f"{device.macID}_thermostat", ) self.entity_description = description @property def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_FLAGS @property def temperature_unit(self) -> str: """Return the unit of measurement.""" return self._temperature_unit @property def current_temperature(self) -> float | None: """Return the current temperature.""" return self.device.indoorTemperature @property def hvac_action(self) -> str: """Return the current hvac action.""" action = HVAC_ACTIONS.get(self.device.operationStatus.mode, None) if action == CURRENT_HVAC_OFF and self.hvac_mode != HVAC_MODE_OFF: action = CURRENT_HVAC_IDLE return action @property def hvac_mode(self) -> str: """Return the hvac mode.""" return HVAC_MODES[self.device.changeableValues.mode] @property def hvac_modes(self) -> list[str]: """List of available hvac modes.""" return self._hvac_modes @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" device = self.device if not device.hasDualSetpointStatus: if self.hvac_mode == HVAC_MODE_COOL: return device.changeableValues.coolSetpoint return device.changeableValues.heatSetpoint return None @property def target_temperature_low(self) -> float | None: """Return the upper bound temperature we try to reach.""" device = self.device if device.hasDualSetpointStatus: return device.changeableValues.coolSetpoint return None @property def target_temperature_high(self) -> float | None: """Return the upper bound temperature we try to reach.""" device = self.device if device.hasDualSetpointStatus: return device.changeableValues.heatSetpoint return None @property def preset_mode(self) -> str | None: """Return current preset mode.""" return self.device.changeableValues.thermostatSetpointStatus @property def preset_modes(self) -> list[str] | None: """Return preset modes.""" return [ PRESET_NO_HOLD, PRESET_HOLD_UNTIL, PRESET_PERMANENT_HOLD, PRESET_TEMPORARY_HOLD, PRESET_VACATION_HOLD, ] @property def min_temp(self) -> float: """Identify min_temp in Lyric API or defaults if not available.""" device = self.device if LYRIC_HVAC_MODE_COOL in device.allowedModes: return device.minCoolSetpoint return device.minHeatSetpoint @property def max_temp(self) -> float: """Identify max_temp in Lyric API or defaults if not available.""" device = self.device if LYRIC_HVAC_MODE_HEAT in device.allowedModes: return device.maxHeatSetpoint return device.maxCoolSetpoint async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) device = self.device if device.hasDualSetpointStatus: if target_temp_low is None or target_temp_high is None: raise HomeAssistantError( "Could not find target_temp_low and/or target_temp_high in arguments" ) _LOGGER.debug("Set temperature: %s - %s", target_temp_low, target_temp_high) try: await self._update_thermostat( self.location, device, coolSetpoint=target_temp_low, heatSetpoint=target_temp_high, ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) else: temp = kwargs.get(ATTR_TEMPERATURE) _LOGGER.debug("Set temperature: %s", temp) try: if self.hvac_mode == HVAC_MODE_COOL: await self._update_thermostat( self.location, device, coolSetpoint=temp ) else: await self._update_thermostat( self.location, device, heatSetpoint=temp ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) await self.coordinator.async_refresh() async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set hvac mode.""" _LOGGER.debug("Set hvac mode: %s", hvac_mode) try: await self._update_thermostat( self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode] ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) await self.coordinator.async_refresh() async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset (PermanentHold, HoldUntil, NoHold, VacationHold) mode.""" _LOGGER.debug("Set preset mode: %s", preset_mode) try: await self._update_thermostat( self.location, self.device, thermostatSetpointStatus=preset_mode ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) await self.coordinator.async_refresh() async def async_set_hold_time(self, time_period: str) -> None: """Set the time to hold until.""" _LOGGER.debug("set_hold_time: %s", time_period) try: await self._update_thermostat( self.location, self.device, thermostatSetpointStatus=PRESET_HOLD_UNTIL, nextPeriodTime=time_period, ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) await self.coordinator.async_refresh()