2021-01-31 17:51:31 +00:00
|
|
|
"""Support for Honeywell Lyric climate platform."""
|
2021-03-18 12:07:04 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-01-31 17:51:31 +00:00
|
|
|
import logging
|
2021-04-10 19:48:33 +00:00
|
|
|
from time import localtime, strftime, time
|
2021-01-31 17:51:31 +00:00
|
|
|
|
|
|
|
from aiolyric.objects.device import LyricDevice
|
|
|
|
from aiolyric.objects.location import LyricLocation
|
|
|
|
import voluptuous as vol
|
|
|
|
|
|
|
|
from homeassistant.components.climate import ClimateEntity
|
|
|
|
from homeassistant.components.climate.const import (
|
|
|
|
ATTR_TARGET_TEMP_HIGH,
|
|
|
|
ATTR_TARGET_TEMP_LOW,
|
2021-03-14 08:05:47 +00:00
|
|
|
CURRENT_HVAC_COOL,
|
|
|
|
CURRENT_HVAC_HEAT,
|
|
|
|
CURRENT_HVAC_IDLE,
|
|
|
|
CURRENT_HVAC_OFF,
|
2021-01-31 17:51:31 +00:00
|
|
|
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
|
2021-04-23 07:55:20 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
2021-01-31 17:51:31 +00:00
|
|
|
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
|
|
|
|
|
2021-03-14 08:05:47 +00:00
|
|
|
LYRIC_HVAC_ACTION_OFF = "EquipmentOff"
|
|
|
|
LYRIC_HVAC_ACTION_HEAT = "Heat"
|
|
|
|
LYRIC_HVAC_ACTION_COOL = "Cool"
|
|
|
|
|
2021-01-31 17:51:31 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
2021-03-14 08:05:47 +00:00
|
|
|
HVAC_ACTIONS = {
|
|
|
|
LYRIC_HVAC_ACTION_OFF: CURRENT_HVAC_OFF,
|
|
|
|
LYRIC_HVAC_ACTION_HEAT: CURRENT_HVAC_HEAT,
|
|
|
|
LYRIC_HVAC_ACTION_COOL: CURRENT_HVAC_COOL,
|
|
|
|
}
|
|
|
|
|
2021-01-31 17:51:31 +00:00
|
|
|
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,
|
2021-04-10 19:48:33 +00:00
|
|
|
lambda td: strftime("%H:%M:%S", localtime(time() + td.total_seconds())),
|
2021-01-31 17:51:31 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(
|
2021-04-23 07:55:20 +00:00
|
|
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
2021-01-31 17:51:31 +00:00
|
|
|
) -> 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:
|
2021-02-02 00:09:25 +00:00
|
|
|
entities.append(
|
|
|
|
LyricClimate(
|
|
|
|
coordinator, location, device, hass.config.units.temperature_unit
|
|
|
|
)
|
|
|
|
)
|
2021-01-31 17:51:31 +00:00
|
|
|
|
|
|
|
async_add_entities(entities, True)
|
|
|
|
|
2021-05-03 16:34:28 +00:00
|
|
|
platform = entity_platform.async_get_current_platform()
|
2021-01-31 17:51:31 +00:00
|
|
|
|
|
|
|
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."""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
coordinator: DataUpdateCoordinator,
|
|
|
|
location: LyricLocation,
|
|
|
|
device: LyricDevice,
|
2021-02-02 00:09:25 +00:00
|
|
|
temperature_unit: str,
|
2021-01-31 17:51:31 +00:00
|
|
|
) -> None:
|
|
|
|
"""Initialize Honeywell Lyric climate entity."""
|
2021-02-02 00:09:25 +00:00
|
|
|
self._temperature_unit = temperature_unit
|
2021-01-31 17:51:31 +00:00
|
|
|
|
|
|
|
# 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",
|
|
|
|
device.name,
|
|
|
|
None,
|
|
|
|
)
|
|
|
|
|
|
|
|
@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
|
2021-03-18 12:07:04 +00:00
|
|
|
def current_temperature(self) -> float | None:
|
2021-01-31 17:51:31 +00:00
|
|
|
"""Return the current temperature."""
|
|
|
|
return self.device.indoorTemperature
|
|
|
|
|
2021-03-14 08:05:47 +00:00
|
|
|
@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
|
|
|
|
|
2021-01-31 17:51:31 +00:00
|
|
|
@property
|
|
|
|
def hvac_mode(self) -> str:
|
|
|
|
"""Return the hvac mode."""
|
|
|
|
return HVAC_MODES[self.device.changeableValues.mode]
|
|
|
|
|
|
|
|
@property
|
2021-03-18 12:07:04 +00:00
|
|
|
def hvac_modes(self) -> list[str]:
|
2021-01-31 17:51:31 +00:00
|
|
|
"""List of available hvac modes."""
|
|
|
|
return self._hvac_modes
|
|
|
|
|
|
|
|
@property
|
2021-03-18 12:07:04 +00:00
|
|
|
def target_temperature(self) -> float | None:
|
2021-01-31 17:51:31 +00:00
|
|
|
"""Return the temperature we try to reach."""
|
2021-02-02 00:09:25 +00:00
|
|
|
device = self.device
|
2021-01-31 17:51:31 +00:00
|
|
|
if not device.hasDualSetpointStatus:
|
|
|
|
return device.changeableValues.heatSetpoint
|
2021-02-02 00:09:25 +00:00
|
|
|
return None
|
2021-01-31 17:51:31 +00:00
|
|
|
|
|
|
|
@property
|
2021-03-18 12:07:04 +00:00
|
|
|
def target_temperature_low(self) -> float | None:
|
2021-01-31 17:51:31 +00:00
|
|
|
"""Return the upper bound temperature we try to reach."""
|
2021-02-02 00:09:25 +00:00
|
|
|
device = self.device
|
2021-01-31 17:51:31 +00:00
|
|
|
if device.hasDualSetpointStatus:
|
|
|
|
return device.changeableValues.coolSetpoint
|
2021-02-02 00:09:25 +00:00
|
|
|
return None
|
2021-01-31 17:51:31 +00:00
|
|
|
|
|
|
|
@property
|
2021-03-18 12:07:04 +00:00
|
|
|
def target_temperature_high(self) -> float | None:
|
2021-01-31 17:51:31 +00:00
|
|
|
"""Return the upper bound temperature we try to reach."""
|
2021-02-02 00:09:25 +00:00
|
|
|
device = self.device
|
2021-01-31 17:51:31 +00:00
|
|
|
if device.hasDualSetpointStatus:
|
|
|
|
return device.changeableValues.heatSetpoint
|
2021-02-02 00:09:25 +00:00
|
|
|
return None
|
2021-01-31 17:51:31 +00:00
|
|
|
|
|
|
|
@property
|
2021-03-18 12:07:04 +00:00
|
|
|
def preset_mode(self) -> str | None:
|
2021-01-31 17:51:31 +00:00
|
|
|
"""Return current preset mode."""
|
|
|
|
return self.device.changeableValues.thermostatSetpointStatus
|
|
|
|
|
|
|
|
@property
|
2021-03-18 12:07:04 +00:00
|
|
|
def preset_modes(self) -> list[str] | None:
|
2021-01-31 17:51:31 +00:00
|
|
|
"""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."""
|
2021-02-02 00:09:25 +00:00
|
|
|
device = self.device
|
2021-01-31 17:51:31 +00:00
|
|
|
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."""
|
2021-02-02 00:09:25 +00:00
|
|
|
device = self.device
|
2021-01-31 17:51:31 +00:00
|
|
|
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)
|
|
|
|
|
2021-02-02 00:09:25 +00:00
|
|
|
device = self.device
|
2021-01-31 17:51:31 +00:00
|
|
|
if device.hasDualSetpointStatus:
|
2021-04-17 17:20:35 +00:00
|
|
|
if target_temp_low is None or target_temp_high is None:
|
2021-01-31 17:51:31 +00:00
|
|
|
raise HomeAssistantError(
|
|
|
|
"Could not find target_temp_low and/or target_temp_high in arguments"
|
|
|
|
)
|
2021-04-17 17:20:35 +00:00
|
|
|
_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)
|
2021-01-31 17:51:31 +00:00
|
|
|
else:
|
|
|
|
temp = kwargs.get(ATTR_TEMPERATURE)
|
2021-04-17 17:20:35 +00:00
|
|
|
_LOGGER.debug("Set temperature: %s", temp)
|
|
|
|
try:
|
|
|
|
await self._update_thermostat(self.location, device, heatSetpoint=temp)
|
|
|
|
except LYRIC_EXCEPTIONS as exception:
|
|
|
|
_LOGGER.error(exception)
|
2021-01-31 17:51:31 +00:00
|
|
|
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()
|