253 lines
9.3 KiB
Python
253 lines
9.3 KiB
Python
"""Support for Bryant Evolution HVAC systems."""
|
|
|
|
from datetime import timedelta
|
|
import logging
|
|
from typing import Any
|
|
|
|
from evolutionhttp import BryantEvolutionLocalClient
|
|
|
|
from homeassistant.components.climate import (
|
|
ClimateEntity,
|
|
ClimateEntityFeature,
|
|
HVACAction,
|
|
HVACMode,
|
|
)
|
|
from homeassistant.const import UnitOfTemperature
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.device_registry import DeviceInfo
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
|
|
from . import BryantEvolutionConfigEntry, names
|
|
from .const import CONF_SYSTEM_ZONE, DOMAIN
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
SCAN_INTERVAL = timedelta(seconds=60)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: BryantEvolutionConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up a config entry."""
|
|
|
|
# Add a climate entity for each system/zone.
|
|
sam_uid = names.sam_device_uid(config_entry)
|
|
entities: list[Entity] = []
|
|
for sz in config_entry.data[CONF_SYSTEM_ZONE]:
|
|
system_id = sz[0]
|
|
zone_id = sz[1]
|
|
client = config_entry.runtime_data.get(tuple(sz))
|
|
climate = BryantEvolutionClimate(
|
|
client,
|
|
system_id,
|
|
zone_id,
|
|
sam_uid,
|
|
)
|
|
entities.append(climate)
|
|
async_add_entities(entities, update_before_add=True)
|
|
|
|
|
|
class BryantEvolutionClimate(ClimateEntity):
|
|
"""ClimateEntity for Bryant Evolution HVAC systems.
|
|
|
|
Design note: this class updates using polling. However, polling
|
|
is very slow (~1500 ms / parameter). To improve the user
|
|
experience on updates, we also locally update this instance and
|
|
call async_write_ha_state as well.
|
|
"""
|
|
|
|
_attr_has_entity_name = True
|
|
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
|
|
_attr_supported_features = (
|
|
ClimateEntityFeature.TARGET_TEMPERATURE
|
|
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
|
| ClimateEntityFeature.FAN_MODE
|
|
| ClimateEntityFeature.TURN_ON
|
|
| ClimateEntityFeature.TURN_OFF
|
|
)
|
|
_attr_hvac_modes = [
|
|
HVACMode.HEAT,
|
|
HVACMode.COOL,
|
|
HVACMode.HEAT_COOL,
|
|
HVACMode.OFF,
|
|
]
|
|
_attr_fan_modes = ["auto", "low", "med", "high"]
|
|
_enable_turn_on_off_backwards_compatibility = False
|
|
|
|
def __init__(
|
|
self,
|
|
client: BryantEvolutionLocalClient,
|
|
system_id: int,
|
|
zone_id: int,
|
|
sam_uid: str,
|
|
) -> None:
|
|
"""Initialize an entity from parts."""
|
|
self._client = client
|
|
self._attr_name = None
|
|
self._attr_unique_id = names.zone_entity_uid(sam_uid, system_id, zone_id)
|
|
self._attr_device_info = DeviceInfo(
|
|
identifiers={(DOMAIN, self._attr_unique_id)},
|
|
manufacturer="Bryant",
|
|
via_device=(DOMAIN, names.system_device_uid(sam_uid, system_id)),
|
|
name=f"System {system_id} Zone {zone_id}",
|
|
)
|
|
|
|
async def async_update(self) -> None:
|
|
"""Update the entity state."""
|
|
self._attr_current_temperature = await self._client.read_current_temperature()
|
|
if (fan_mode := await self._client.read_fan_mode()) is not None:
|
|
self._attr_fan_mode = fan_mode.lower()
|
|
else:
|
|
self._attr_fan_mode = None
|
|
self._attr_target_temperature = None
|
|
self._attr_target_temperature_high = None
|
|
self._attr_target_temperature_low = None
|
|
self._attr_hvac_mode = await self._read_hvac_mode()
|
|
|
|
# Set target_temperature or target_temperature_{high, low} based on mode.
|
|
match self._attr_hvac_mode:
|
|
case HVACMode.HEAT:
|
|
self._attr_target_temperature = (
|
|
await self._client.read_heating_setpoint()
|
|
)
|
|
case HVACMode.COOL:
|
|
self._attr_target_temperature = (
|
|
await self._client.read_cooling_setpoint()
|
|
)
|
|
case HVACMode.HEAT_COOL:
|
|
self._attr_target_temperature_high = (
|
|
await self._client.read_cooling_setpoint()
|
|
)
|
|
self._attr_target_temperature_low = (
|
|
await self._client.read_heating_setpoint()
|
|
)
|
|
case HVACMode.OFF:
|
|
pass
|
|
case _:
|
|
_LOGGER.error("Unknown HVAC mode %s", self._attr_hvac_mode)
|
|
|
|
# Note: depends on current temperature and target temperature low read
|
|
# above.
|
|
self._attr_hvac_action = await self._read_hvac_action()
|
|
|
|
async def _read_hvac_mode(self) -> HVACMode:
|
|
mode_and_active = await self._client.read_hvac_mode()
|
|
if not mode_and_active:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN, translation_key="failed_to_read_hvac_mode"
|
|
)
|
|
mode = mode_and_active[0]
|
|
mode_enum = {
|
|
"HEAT": HVACMode.HEAT,
|
|
"COOL": HVACMode.COOL,
|
|
"AUTO": HVACMode.HEAT_COOL,
|
|
"OFF": HVACMode.OFF,
|
|
}.get(mode.upper())
|
|
if mode_enum is None:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="failed_to_parse_hvac_mode",
|
|
translation_placeholders={"mode": mode},
|
|
)
|
|
return mode_enum
|
|
|
|
async def _read_hvac_action(self) -> HVACAction:
|
|
"""Return the current running hvac operation."""
|
|
mode_and_active = await self._client.read_hvac_mode()
|
|
if not mode_and_active:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN, translation_key="failed_to_read_hvac_action"
|
|
)
|
|
mode, is_active = mode_and_active
|
|
if not is_active:
|
|
return HVACAction.OFF
|
|
match mode.upper():
|
|
case "HEAT":
|
|
return HVACAction.HEATING
|
|
case "COOL":
|
|
return HVACAction.COOLING
|
|
case "OFF":
|
|
return HVACAction.OFF
|
|
case "AUTO":
|
|
# In AUTO, we need to figure out what the actual action is
|
|
# based on the setpoints.
|
|
if (
|
|
self.current_temperature is not None
|
|
and self.target_temperature_low is not None
|
|
):
|
|
if self.current_temperature > self.target_temperature_low:
|
|
# If the system is on and the current temperature is
|
|
# higher than the point at which heating would activate,
|
|
# then we must be cooling.
|
|
return HVACAction.COOLING
|
|
return HVACAction.HEATING
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="failed_to_parse_hvac_mode",
|
|
translation_placeholders={
|
|
"mode_and_active": mode_and_active,
|
|
"current_temperature": str(self.current_temperature),
|
|
"target_temperature_low": str(self.target_temperature_low),
|
|
},
|
|
)
|
|
|
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
"""Set new target hvac mode."""
|
|
if hvac_mode == HVACMode.HEAT_COOL:
|
|
hvac_mode = HVACMode.AUTO
|
|
if not await self._client.set_hvac_mode(hvac_mode):
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN, translation_key="failed_to_set_hvac_mode"
|
|
)
|
|
self._attr_hvac_mode = hvac_mode
|
|
self._async_write_ha_state()
|
|
|
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
"""Set new target temperature."""
|
|
if kwargs.get("target_temp_high"):
|
|
temp = int(kwargs["target_temp_high"])
|
|
if not await self._client.set_cooling_setpoint(temp):
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN, translation_key="failed_to_set_clsp"
|
|
)
|
|
self._attr_target_temperature_high = temp
|
|
|
|
if kwargs.get("target_temp_low"):
|
|
temp = int(kwargs["target_temp_low"])
|
|
if not await self._client.set_heating_setpoint(temp):
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN, translation_key="failed_to_set_htsp"
|
|
)
|
|
self._attr_target_temperature_low = temp
|
|
|
|
if kwargs.get("temperature"):
|
|
temp = int(kwargs["temperature"])
|
|
fn = (
|
|
self._client.set_heating_setpoint
|
|
if self.hvac_mode == HVACMode.HEAT
|
|
else self._client.set_cooling_setpoint
|
|
)
|
|
if not await fn(temp):
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN, translation_key="failed_to_set_temp"
|
|
)
|
|
self._attr_target_temperature = temp
|
|
|
|
# If we get here, we must have changed something unless HA allowed an
|
|
# invalid service call (without any recognized kwarg).
|
|
self._async_write_ha_state()
|
|
|
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
|
"""Set new target fan mode."""
|
|
if not await self._client.set_fan_mode(fan_mode):
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN, translation_key="failed_to_set_fan_mode"
|
|
)
|
|
self._attr_fan_mode = fan_mode.lower()
|
|
self.async_write_ha_state()
|