add coordinator for climate entities

This commit is contained in:
2023-12-08 10:09:44 +01:00
parent 899d5c7b54
commit 71ef843da8
3 changed files with 165 additions and 58 deletions

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import Throttle
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -11,6 +11,11 @@ from homeassistant.components.climate import (
ClimateEntity,
ConfigEntry,
)
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from homeassistant.components.climate.const import (
HVACMode,
@@ -51,17 +56,38 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
async def async_setup_entry(hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback):
async_add_entities: AddEntitiesCallback
) -> None:
"""Setup switch entries"""
entities = []
for device in hass.data[DOMAIN]:
coordinator = ClimateCoordinator(hass, device)
for area in device.areas:
_LOGGER.debug("Device: {} - Area: {}".format(device, area))
entities.append(AreaClimateEntity(device, area))
entities.append(AreaClimateEntity(coordinator, device, area))
async_add_entities(entities)
class AreaClimateEntity(ClimateEntity):
class ClimateCoordinator(DataUpdateCoordinator):
""" Climate coordinator """
def __init__(self,
hass: HomeAssistant,
device: Koolnova,
) -> None:
""" Class constructor """
super().__init__(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name=DOMAIN,
update_method=device.update_all_areas,
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=30),
)
class AreaClimateEntity(CoordinatorEntity, ClimateEntity):
""" Reperesentation of a climate entity """
# pylint: disable = too-many-instance-attributes
@@ -78,11 +104,13 @@ class AreaClimateEntity(ClimateEntity):
_attr_target_temperature_low: float = MIN_TEMP_ORDER
_attr_target_temperature_step: float = STEP_TEMP_ORDER
def __init__(self,
def __init__(self,
coordinator: ClimateCoordinator, # pylint: disable=unused-argument
device: Koolnova, # pylint: disable=unused-argument
area: Area, # pylint: disable=unused-argument
) -> None:
""" Class constructor """
super().__init__(coordinator)
self._device = device
self._area = area
self._attr_name = f"{device.name} {area.name} area"
@@ -125,7 +153,9 @@ class AreaClimateEntity(ClimateEntity):
break
ret = await self._device.set_area_fan_mode(zone_id = self._area.id_zone,
mode = ZoneFanMode(opt))
await self._update_state()
if not ret:
_LOGGER.exception("Error setting new fan value for area id {}".format(self._area.id_zone))
await self.coordinator.async_request_refresh()
async def async_set_hvac_mode(self, hvac_mode:HVACMode) -> None:
""" set new target hvac mode """
@@ -137,39 +167,67 @@ class AreaClimateEntity(ClimateEntity):
break
ret = await self._device.set_area_clim_mode(zone_id = self._area.id_zone,
mode = ZoneClimMode(opt))
await self._update_state()
async def async_turn_on(self) -> None:
""" turn the entity on """
_LOGGER.debug("[Climate {}] turn on the entity".format(self._area.id_zone))
#await self._update_state()
async def async_turn_off(self) -> None:
""" turn the entity off """
_LOGGER.debug("[Climate {}] turn off the entity".format(self._area.id_zone))
#await self._update_state()
async def _update_state(self) -> None:
""" Private update attributes """
_LOGGER.debug("[Climate {}] _update_state".format(self._area.id_zone))
# retreive current temperature from specific area
ret, up_area = await self._device.update_area(self._area.id_zone)
if not ret:
_LOGGER.error("[Climate {}] Cannot update area values")
return
self._area = up_area
_LOGGER.debug("[Climate {}] temp:{} - target:{} - state: {} - hvac:{} - fan:{}".format(self._area.id_zone,
self._area.real_temp,
self._area.order_temp,
self._area.state,
self._area.clim_mode,
self._area.fan_mode))
self._attr_current_temperature = self._area.real_temp
self._attr_target_temperature = self._area.order_temp
self._attr_hvac_mode = self._translate_to_hvac_mode()
self._attr_fan_mode = FAN_TRANSLATION[int(self._area.fan_mode)]
_LOGGER.exception("Error setting new hvac value for area id {}".format(self._area.id_zone))
await self.coordinator.async_request_refresh()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
""" Retreive latest values """
await self._update_state()
@callback
def _handle_coordinator_update(self) -> None:
""" Handle updated data from the coordinator """
for _cur_area in self.coordinator.data:
if _cur_area.id_zone == self._area.id_zone:
_LOGGER.debug("[Climate {}] temp:{} - target:{} - state: {} - hvac:{} - fan:{}".format(_cur_area.id_zone,
_cur_area.real_temp,
_cur_area.order_temp,
_cur_area.state,
_cur_area.clim_mode,
_cur_area.fan_mode))
self._area = _cur_area
self._attr_current_temperature = _cur_area.real_temp
self._attr_target_temperature = _cur_area.order_temp
if _cur_area.state == ZoneState.STATE_OFF:
self._attr_hvac_mode = HVACMode.OFF
else:
self._attr_hvac_mode = HVAC_TRANSLATION[int(_cur_area.clim_mode)]
self._attr_fan_mode = FAN_TRANSLATION[int(_cur_area.fan_mode)]
self.async_write_ha_state()
# async def async_turn_on(self) -> None:
# """ turn the entity on """
# _LOGGER.debug("[Climate {}] turn on the entity".format(self._area.id_zone))
# #await self._update_state()
# async def async_turn_off(self) -> None:
# """ turn the entity off """
# _LOGGER.debug("[Climate {}] turn off the entity".format(self._area.id_zone))
# #await self._update_state()
# async def _update_state(self) -> None:
# """ Private update attributes """
# _LOGGER.debug("[Climate {}] _update_state".format(self._area.id_zone))
# # retreive current temperature from specific area
# ret, up_area = await self._device.update_area(self._area.id_zone)
# if not ret:
# _LOGGER.error("[Climate {}] Cannot update area values")
# return
# self._area = up_area
# _LOGGER.debug("[Climate {}] temp:{} - target:{} - state: {} - hvac:{} - fan:{}".format(self._area.id_zone,
# self._area.real_temp,
# self._area.order_temp,
# self._area.state,
# self._area.clim_mode,
# self._area.fan_mode))
# self._attr_current_temperature = self._area.real_temp
# self._attr_target_temperature = self._area.order_temp
# self._attr_hvac_mode = self._translate_to_hvac_mode()
# self._attr_fan_mode = FAN_TRANSLATION[int(self._area.fan_mode)]
# @Throttle(MIN_TIME_BETWEEN_UPDATES)
# async def async_update(self):
# """ Retreive latest values """
# await self._update_state()
# @property
# def should_poll(self) -> bool:
# """ Do not poll for those entities """
# return False

View File

@@ -128,7 +128,7 @@ class Area:
def __repr__(self) -> str:
''' repr method '''
return repr('Zone(Name: {}, Id:{}, State:{}, Register:{}, Fan:{}, Clim:{}, Real Temp:{}, Order Temp:{})'.format(
return repr('Area(Name: {}, Id:{}, State:{}, Register:{}, Fan:{}, Clim:{}, Real Temp:{}, Order Temp:{})'.format(
self._name,
self._id,
self._state,
@@ -169,7 +169,7 @@ class Koolnova:
id_search:int = 0,
) -> (bool, int):
""" test if area id is defined """
_areas_found = [idx for idx,x in enumerate(self._areas) if x.id_zone == id_search]
_areas_found = [idx for idx, x in enumerate(self._areas) if x.id_zone == id_search]
_idx = 0
if not _areas_found:
_LOGGER.error("Area id ({}) not defined".format(id_search))
@@ -178,8 +178,8 @@ class Koolnova:
_LOGGER.error("Multiple Area with same id ({})".format(id_search))
return False, _idx
else:
_LOGGER.debug("idx found: {}".format(_idx))
_idx = _areas_found[0]
_LOGGER.debug("idx found: {}".format(_idx))
return True, _idx
async def update(self) -> bool:
@@ -292,7 +292,7 @@ class Koolnova:
return self._areas[zone_id - 1]
async def update_area(self, zone_id:int = 0) -> bool:
""" update area """
""" update specific area from zone_id """
ret, infos = await self._client.zone_registered(zone_id = zone_id)
if not ret:
_LOGGER.error("Error retreiving area ({}) values".format(zone_id))
@@ -309,6 +309,26 @@ class Koolnova:
break
return ret, self._areas[zone_id - 1]
async def update_all_areas(self) -> list:
""" update all areas registered """
_ret, _vals = await self._client.areas_registered()
if not _ret:
_LOGGER.error("Error retreiving areas values")
return None
else:
_LOGGER.debug("areas: {}".format(_vals))
for k,v in _vals.items():
for _idx, _area in enumerate(self._areas):
if k == _area.id_zone:
self._areas[_idx].state = v['state']
self._areas[_idx].register = v['register']
self._areas[_idx].fan_mode = v['fan']
self._areas[_idx].clim_mode = v['clim']
self._areas[_idx].real_temp = v['real_temp']
self._areas[_idx].order_temp = v['order_temp']
return self._areas
def get_units(self) -> list:
''' get units '''
return self._units
@@ -381,15 +401,16 @@ class Koolnova:
zone_id:int,
) -> float:
""" get current temp of specific Area """
_ret, _idx = self._area_defined(id_search = zone_id)
if not _ret:
_LOGGER.error("Area not defined ...")
return False
ret, temp = await self._client.area_temp(id_zone = zone_id)
if not ret:
_LOGGER.error("Error reading temp for area with ID: {}".format(zone_id))
return False
for idx, area in enumerate(self._areas):
if area.id_zone == zone_id:
# update areas list value from modbus response
self._areas[idx].real_temp = temp
self._areas[_idx].real_temp = temp
return temp
async def set_area_target_temp(self,
@@ -397,28 +418,32 @@ class Koolnova:
temp:float,
) -> bool:
""" set target temp of specific area """
_ret, _idx = self._area_defined(id_search = zone_id)
if not _ret:
_LOGGER.error("Area not defined ...")
return False
ret = await self._client.set_area_target_temp(zone_id = zone_id, val = temp)
if not ret:
_LOGGER.error("Error writing target temp for area with ID: {}".format(zone_id))
return False
for idx, area in enumerate(self._areas):
if area.id_zone == zone_id:
# update areas list value from modbus response
self._areas[idx].order_temp = temp
self._areas[_idx].order_temp = temp
return True
async def get_area_target_temp(self,
zone_id:int,
) -> float:
""" get target temp of specific area """
_ret, _idx = self._area_defined(id_search = zone_id)
if not _ret:
_LOGGER.error("Area not defined ...")
return False
ret, temp = await self._client.area_target_temp(id_zone = zone_id)
if not ret:
_LOGGER.error("Error reading target temp for area with ID: {}".format(zone_id))
return 0.0
for idx, area in enumerate(self._areas):
if area.id_zone == zone_id:
# update areas list value from modbus response
self._areas[idx].order_temp = temp
self._areas[_idx].order_temp = temp
return temp
async def set_area_clim_mode(self,
@@ -428,6 +453,7 @@ class Koolnova:
""" set climate mode for specific area """
_ret, _idx = self._area_defined(id_search = zone_id)
if not _ret:
_LOGGER.error("Area not defined ...")
return False
if mode == const.ZoneClimMode.OFF:
@@ -462,6 +488,7 @@ class Koolnova:
# test if area id is defined
_ret, _idx = self._area_defined(id_search = zone_id)
if not _ret:
_LOGGER.error("Area not defined ...")
return False
if self._areas[_idx].state == const.ZoneState.STATE_OFF:

View File

@@ -181,7 +181,7 @@ class Operations:
regs, ret = await self.__read_registers(start_reg = const.REG_START_ZONE + (4 * (zone_id - 1)),
count = const.NUM_REG_PER_ZONE)
if not ret:
raise ReadRegistersError("Read holding regsiter error")
raise ReadRegistersError("Error reading holding register")
if const.ZoneRegister(regs[0] >> 1) == const.ZoneRegister.REGISTER_OFF:
_LOGGER.warning("Zone with id: {} is not registered".format(zone_id))
return False, {}
@@ -194,6 +194,28 @@ class Operations:
zone_dict['real_temp'] = regs[3]/2
return True, zone_dict
async def areas_registered(self) -> (bool, dict):
""" Get all areas values """
_areas_dict:dict = {}
regs, ret = await self.__read_registers(start_reg = const.REG_START_ZONE,
count = const.NUM_REG_PER_ZONE * const.NB_ZONE_MAX)
if not ret:
raise ReadRegistersError("Error reading holding register")
for area_idx in range(const.NB_ZONE_MAX):
_idx:int = 4 * area_idx
_area_dict:dict = {}
if const.ZoneRegister(regs[_idx + const.REG_LOCK_ZONE] >> 1) == const.ZoneRegister.REGISTER_OFF:
continue
_area_dict['state'] = const.ZoneState(regs[_idx + const.REG_LOCK_ZONE] & 0b01)
_area_dict['register'] = const.ZoneRegister(regs[_idx + const.REG_LOCK_ZONE] >> 1)
_area_dict['fan'] = const.ZoneFanMode((regs[_idx + const.REG_STATE_AND_FLOW] & 0xF0) >> 4)
_area_dict['clim'] = const.ZoneClimMode(regs[_idx + const.REG_STATE_AND_FLOW] & 0x0F)
_area_dict['order_temp'] = regs[_idx + const.REG_TEMP_ORDER]/2
_area_dict['real_temp'] = regs[_idx + const.REG_TEMP_REAL]/2
_areas_dict[area_idx + 1] = _area_dict
return True, _areas_dict
async def system_status(self) -> (bool, const.SysState):
''' Read system status register '''
reg, ret = await self.__read_register(const.REG_SYS_STATE)