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
+97 -39
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -11,6 +11,11 @@ from homeassistant.components.climate import (
ClimateEntity, ClimateEntity,
ConfigEntry, ConfigEntry,
) )
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
HVACMode, HVACMode,
@@ -51,17 +56,38 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
async def async_setup_entry(hass: HomeAssistant, async def async_setup_entry(hass: HomeAssistant,
entry: ConfigEntry, entry: ConfigEntry,
async_add_entities: AddEntitiesCallback): async_add_entities: AddEntitiesCallback
) -> None:
"""Setup switch entries""" """Setup switch entries"""
entities = [] entities = []
for device in hass.data[DOMAIN]: for device in hass.data[DOMAIN]:
coordinator = ClimateCoordinator(hass, device)
for area in device.areas: for area in device.areas:
_LOGGER.debug("Device: {} - Area: {}".format(device, area)) _LOGGER.debug("Device: {} - Area: {}".format(device, area))
entities.append(AreaClimateEntity(device, area)) entities.append(AreaClimateEntity(coordinator, device, area))
async_add_entities(entities) 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 """ """ Reperesentation of a climate entity """
# pylint: disable = too-many-instance-attributes # pylint: disable = too-many-instance-attributes
@@ -79,10 +105,12 @@ class AreaClimateEntity(ClimateEntity):
_attr_target_temperature_step: float = STEP_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 device: Koolnova, # pylint: disable=unused-argument
area: Area, # pylint: disable=unused-argument area: Area, # pylint: disable=unused-argument
) -> None: ) -> None:
""" Class constructor """ """ Class constructor """
super().__init__(coordinator)
self._device = device self._device = device
self._area = area self._area = area
self._attr_name = f"{device.name} {area.name} area" self._attr_name = f"{device.name} {area.name} area"
@@ -125,7 +153,9 @@ class AreaClimateEntity(ClimateEntity):
break break
ret = await self._device.set_area_fan_mode(zone_id = self._area.id_zone, ret = await self._device.set_area_fan_mode(zone_id = self._area.id_zone,
mode = ZoneFanMode(opt)) 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: async def async_set_hvac_mode(self, hvac_mode:HVACMode) -> None:
""" set new target hvac mode """ """ set new target hvac mode """
@@ -137,39 +167,67 @@ class AreaClimateEntity(ClimateEntity):
break break
ret = await self._device.set_area_clim_mode(zone_id = self._area.id_zone, ret = await self._device.set_area_clim_mode(zone_id = self._area.id_zone,
mode = ZoneClimMode(opt)) 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: if not ret:
_LOGGER.error("[Climate {}] Cannot update area values") _LOGGER.exception("Error setting new hvac value for area id {}".format(self._area.id_zone))
return await self.coordinator.async_request_refresh()
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) @callback
async def async_update(self): def _handle_coordinator_update(self) -> None:
""" Retreive latest values """ """ Handle updated data from the coordinator """
await self._update_state() 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
+43 -16
View File
@@ -128,7 +128,7 @@ class Area:
def __repr__(self) -> str: def __repr__(self) -> str:
''' repr method ''' ''' 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._name,
self._id, self._id,
self._state, self._state,
@@ -178,8 +178,8 @@ class Koolnova:
_LOGGER.error("Multiple Area with same id ({})".format(id_search)) _LOGGER.error("Multiple Area with same id ({})".format(id_search))
return False, _idx return False, _idx
else: else:
_LOGGER.debug("idx found: {}".format(_idx))
_idx = _areas_found[0] _idx = _areas_found[0]
_LOGGER.debug("idx found: {}".format(_idx))
return True, _idx return True, _idx
async def update(self) -> bool: async def update(self) -> bool:
@@ -292,7 +292,7 @@ class Koolnova:
return self._areas[zone_id - 1] return self._areas[zone_id - 1]
async def update_area(self, zone_id:int = 0) -> bool: 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) ret, infos = await self._client.zone_registered(zone_id = zone_id)
if not ret: if not ret:
_LOGGER.error("Error retreiving area ({}) values".format(zone_id)) _LOGGER.error("Error retreiving area ({}) values".format(zone_id))
@@ -309,6 +309,26 @@ class Koolnova:
break break
return ret, self._areas[zone_id - 1] 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: def get_units(self) -> list:
''' get units ''' ''' get units '''
return self._units return self._units
@@ -381,15 +401,16 @@ class Koolnova:
zone_id:int, zone_id:int,
) -> float: ) -> float:
""" get current temp of specific Area """ """ 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) ret, temp = await self._client.area_temp(id_zone = zone_id)
if not ret: if not ret:
_LOGGER.error("Error reading temp for area with ID: {}".format(zone_id)) _LOGGER.error("Error reading temp for area with ID: {}".format(zone_id))
return False return False
for idx, area in enumerate(self._areas): self._areas[_idx].real_temp = temp
if area.id_zone == zone_id:
# update areas list value from modbus response
self._areas[idx].real_temp = temp
return temp return temp
async def set_area_target_temp(self, async def set_area_target_temp(self,
@@ -397,28 +418,32 @@ class Koolnova:
temp:float, temp:float,
) -> bool: ) -> bool:
""" set target temp of specific area """ """ 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) ret = await self._client.set_area_target_temp(zone_id = zone_id, val = temp)
if not ret: if not ret:
_LOGGER.error("Error writing target temp for area with ID: {}".format(zone_id)) _LOGGER.error("Error writing target temp for area with ID: {}".format(zone_id))
return False return False
for idx, area in enumerate(self._areas): self._areas[_idx].order_temp = temp
if area.id_zone == zone_id:
# update areas list value from modbus response
self._areas[idx].order_temp = temp
return True return True
async def get_area_target_temp(self, async def get_area_target_temp(self,
zone_id:int, zone_id:int,
) -> float: ) -> float:
""" get target temp of specific area """ """ 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) ret, temp = await self._client.area_target_temp(id_zone = zone_id)
if not ret: if not ret:
_LOGGER.error("Error reading target temp for area with ID: {}".format(zone_id)) _LOGGER.error("Error reading target temp for area with ID: {}".format(zone_id))
return 0.0 return 0.0
for idx, area in enumerate(self._areas): self._areas[_idx].order_temp = temp
if area.id_zone == zone_id:
# update areas list value from modbus response
self._areas[idx].order_temp = temp
return temp return temp
async def set_area_clim_mode(self, async def set_area_clim_mode(self,
@@ -428,6 +453,7 @@ class Koolnova:
""" set climate mode for specific area """ """ set climate mode for specific area """
_ret, _idx = self._area_defined(id_search = zone_id) _ret, _idx = self._area_defined(id_search = zone_id)
if not _ret: if not _ret:
_LOGGER.error("Area not defined ...")
return False return False
if mode == const.ZoneClimMode.OFF: if mode == const.ZoneClimMode.OFF:
@@ -462,6 +488,7 @@ class Koolnova:
# test if area id is defined # test if area id is defined
_ret, _idx = self._area_defined(id_search = zone_id) _ret, _idx = self._area_defined(id_search = zone_id)
if not _ret: if not _ret:
_LOGGER.error("Area not defined ...")
return False return False
if self._areas[_idx].state == const.ZoneState.STATE_OFF: if self._areas[_idx].state == const.ZoneState.STATE_OFF:
+23 -1
View File
@@ -181,7 +181,7 @@ class Operations:
regs, ret = await self.__read_registers(start_reg = const.REG_START_ZONE + (4 * (zone_id - 1)), regs, ret = await self.__read_registers(start_reg = const.REG_START_ZONE + (4 * (zone_id - 1)),
count = const.NUM_REG_PER_ZONE) count = const.NUM_REG_PER_ZONE)
if not ret: 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: if const.ZoneRegister(regs[0] >> 1) == const.ZoneRegister.REGISTER_OFF:
_LOGGER.warning("Zone with id: {} is not registered".format(zone_id)) _LOGGER.warning("Zone with id: {} is not registered".format(zone_id))
return False, {} return False, {}
@@ -194,6 +194,28 @@ class Operations:
zone_dict['real_temp'] = regs[3]/2 zone_dict['real_temp'] = regs[3]/2
return True, zone_dict 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): async def system_status(self) -> (bool, const.SysState):
''' Read system status register ''' ''' Read system status register '''
reg, ret = await self.__read_register(const.REG_SYS_STATE) reg, ret = await self.__read_register(const.REG_SYS_STATE)