diff --git a/custom_components/climate.py b/custom_components/climate.py index c9409af..cacd77a 100644 --- a/custom_components/climate.py +++ b/custom_components/climate.py @@ -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() \ No newline at end of file + @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 \ No newline at end of file diff --git a/custom_components/koolnova/device.py b/custom_components/koolnova/device.py index 15ce064..8115fc2 100644 --- a/custom_components/koolnova/device.py +++ b/custom_components/koolnova/device.py @@ -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: diff --git a/custom_components/koolnova/operations.py b/custom_components/koolnova/operations.py index c23a8b8..ce75099 100644 --- a/custom_components/koolnova/operations.py +++ b/custom_components/koolnova/operations.py @@ -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)