add coordinator for climate entities
This commit is contained in:
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user