diff --git a/custom_components/__init__.py b/custom_components/__init__.py index 7adde7d..6c5d078 100644 --- a/custom_components/__init__.py +++ b/custom_components/__init__.py @@ -9,13 +9,16 @@ from .koolnova.device import Koolnova from .const import DOMAIN, PLATFORMS +from .coordinator import KoolnovaCoordinator + _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # pylint: disable=unused-argument """ Creation des entités à partir d'une configEntry """ - hass.data.setdefault(DOMAIN, []) + #hass.data.setdefault(DOMAIN, []) + hass.data.setdefault(DOMAIN, {}) name: str = entry.data['Name'] port: str = entry.data['Device'] @@ -26,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, stopbits: int = entry.data['Stopbits'] timeout: int = entry.data['Timeout'] - _LOGGER.debug("Appel de async_setup_entry - entry: entry_id={}, data={}".format(entry.entry_id, entry.data)) + #_LOGGER.debug("Appel de async_setup_entry - entry: entry_id={}, data={}".format(entry.entry_id, entry.data)) try: device = Koolnova(name, port, addr, baudrate, parity, bytesize, stopbits, timeout) # connect to modbus client @@ -34,12 +37,13 @@ async def async_setup_entry(hass: HomeAssistant, # update attributes await device.update() # record each area in device - _LOGGER.debug("Koolnova areas: {}".format(entry.data['areas'])) + #_LOGGER.debug("Koolnova areas: {}".format(entry.data['areas'])) for area in entry.data['areas']: await device.add_manual_registered_zone(name=area['Name'], - id_zone=area['Zone_id']) - _LOGGER.debug("Koolnova device: {}".format(device)) - hass.data[DOMAIN].append(device) + id_zone=area['Area_id']) + hass.data[DOMAIN]['device'] = device + coordinator = KoolnovaCoordinator(hass, device) + hass.data[DOMAIN]['coordinator'] = coordinator except Exception as e: _LOGGER.exception("Something went wrong ... {}".format(e)) diff --git a/custom_components/climate.py b/custom_components/climate.py index e370ecf..706d78b 100644 --- a/custom_components/climate.py +++ b/custom_components/climate.py @@ -32,9 +32,11 @@ from .const import ( HVAC_TRANSLATION, ) +from .coordinator import KoolnovaCoordinator + from homeassistant.const import ( - TEMP_CELSIUS, ATTR_TEMPERATURE, + UnitOfTemperature, ) from .koolnova.device import Koolnova, Area @@ -52,7 +54,6 @@ from .koolnova.const import ( ) _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, @@ -61,38 +62,19 @@ async def async_setup_entry(hass: HomeAssistant, """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(coordinator, device, area)) - async_add_entities(entities) + coordinator = hass.data[DOMAIN]["coordinator"] + device = hass.data[DOMAIN]["device"] -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), - ) + for area in device.areas: + entities.append(AreaClimateEntity(coordinator, device, area)) + async_add_entities(entities) class AreaClimateEntity(CoordinatorEntity, ClimateEntity): """ Reperesentation of a climate entity """ # pylint: disable = too-many-instance-attributes _attr_supported_features: int = SUPPORT_FLAGS - _attr_temperature_unit: str = TEMP_CELSIUS + _attr_temperature_unit: str = UnitOfTemperature.CELSIUS _attr_hvac_modes: list[HVACMode] = SUPPORTED_HVAC_MODES _attr_fan_modes: list[str] = SUPPORTED_FAN_MODES _attr_hvac_mode: HVACMode = HVACMode.OFF @@ -103,9 +85,10 @@ class AreaClimateEntity(CoordinatorEntity, ClimateEntity): _attr_target_temperature_high: float = MAX_TEMP_ORDER _attr_target_temperature_low: float = MIN_TEMP_ORDER _attr_target_temperature_step: float = STEP_TEMP_ORDER + _enable_turn_on_off_backwards_compatibility: bool = False def __init__(self, - coordinator: ClimateCoordinator, # pylint: disable=unused-argument + coordinator: KoolnovaCoordinator, # pylint: disable=unused-argument device: Koolnova, # pylint: disable=unused-argument area: Area, # pylint: disable=unused-argument ) -> None: @@ -118,9 +101,7 @@ class AreaClimateEntity(CoordinatorEntity, ClimateEntity): self._attr_unique_id = f"{DOMAIN}-{area.name}-area-climate" self._attr_current_temperature = area.real_temp self._attr_target_temperature = area.order_temp - _LOGGER.debug("[Climate {}] {} - {}".format(self._area.id_zone, self._area.fan_mode, FAN_TRANSLATION[int(self._area.fan_mode)])) self._attr_fan_mode = FAN_TRANSLATION[int(self._area.fan_mode)] - _LOGGER.debug("[Climate {}] {} - {}".format(self._area.id_zone, self._area.clim_mode, self._translate_to_hvac_mode())) self._attr_hvac_mode = self._translate_to_hvac_mode() def _translate_to_hvac_mode(self) -> int: @@ -181,7 +162,7 @@ class AreaClimateEntity(CoordinatorEntity, ClimateEntity): @callback def _handle_coordinator_update(self) -> None: """ Handle updated data from the coordinator """ - for _cur_area in self.coordinator.data: + for _cur_area in self.coordinator.data['areas']: 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, diff --git a/custom_components/config_flow.py b/custom_components/config_flow.py index d9e82d4..d3e2e60 100644 --- a/custom_components/config_flow.py +++ b/custom_components/config_flow.py @@ -52,14 +52,13 @@ class TestVBE4ConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required("Parity", default="EVEN"): vol.In(["EVEN", "NONE"]), vol.Required("Stopbits", default=DEFAULT_STOPBITS): vol.Coerce(int), vol.Required("Timeout", default=1): vol.Coerce(int), - vol.Optional("Discover", default="MANUAL"): vol.In(["MANUAL", "AUTOMATIC"]) + vol.Optional("Debug", default=False): cv.boolean } ) if user_input: - # second call _LOGGER.debug("config_flow [user] - Step 1b -> On a reçu les valeurs: {}".format(user_input)) - # On memorise les données dans le dictionnaire + # Second call; On memorise les données dans le dictionnaire self._user_inputs.update(user_input) self._conn = Operations(port=self._user_inputs["Device"], @@ -68,12 +67,13 @@ class TestVBE4ConfigFlow(ConfigFlow, domain=DOMAIN): parity=self._user_inputs["Parity"][0], bytesize=self._user_inputs["Sizebyte"], stopbits=self._user_inputs["Stopbits"], - timeout=self._user_inputs["Timeout"]) + timeout=self._user_inputs["Timeout"], + debug=self._user_inputs["Debug"]) try: await self._conn.connect() if not self._conn.connected(): raise CannotConnectError(reason="Client Modbus not connected") - _LOGGER.debug("test communication with koolnova system") + #_LOGGER.debug("test communication with koolnova system") ret, _ = await self._conn.system_status() if not ret: self._conn.disconnect() @@ -94,14 +94,21 @@ class TestVBE4ConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=user_form, errors=errors) - async def async_step_areas(self, + async def async_step_areas(self, user_input: dict | None = None) -> FlowResult: """ Gestion de l'étape de découverte manuelle des zones """ errors = {} + default_id = 1 + default_area_name = "Area 1" + # set default_id to the last id configured + # set default_area_name with the last id configured + for area in self._user_inputs["areas"]: + default_id = area['Area_id'] + 1 + default_area_name = "Area " + str(default_id) zone_form = vol.Schema( { - vol.Required("Name", default="zone"): vol.Coerce(str), - vol.Required("Zone_id", default=1): vol.Coerce(int), + vol.Required("Name", default=default_area_name): vol.Coerce(str), + vol.Required("Area_id", default=default_id): vol.Coerce(int), vol.Optional("Other_area", default=False): cv.boolean } ) @@ -109,20 +116,20 @@ class TestVBE4ConfigFlow(ConfigFlow, domain=DOMAIN): if user_input: # second call try: - # test if zone_id is already configured + # test if area_id is already configured for area in self._user_inputs["areas"]: - if user_input['Zone_id'] == area['Zone_id']: + if user_input['Area_id'] == area['Area_id']: raise AreaAlreadySetError(reason="Area is already configured") # Last area to configure or not ? if not user_input['Other_area']: try: if not self._conn.connected(): await self._conn.connect() - if user_input['Zone_id'] > NB_ZONE_MAX: - raise ZoneIdError(reason="Zone_Id must be between 1 and 16") - _LOGGER.debug("test area registered with id: {}".format(user_input['Zone_id'])) + if user_input['Area_id'] > NB_ZONE_MAX: + raise ZoneIdError(reason="Area_id must be between 1 and 16") + #_LOGGER.debug("test area registered with id: {}".format(user_input['Area_id'])) # test if area is configured into koolnova system - ret, _ = await self._conn.zone_registered(user_input["Zone_id"]) + ret, _ = await self._conn.zone_registered(user_input["Area_id"]) if not ret: self._conn.disconnect() raise AreaNotRegistredError(reason="Area Id is not registred") @@ -137,7 +144,7 @@ class TestVBE4ConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Cannot connect to koolnova system") errors[CONF_BASE] = "cannot_connect" except AreaNotRegistredError: - _LOGGER.exception("Area (id:{}) is not registered to the koolnova system".format(user_input['Zone_id'])) + _LOGGER.exception("Area (id:{}) is not registered to the koolnova system".format(user_input['Area_id'])) errors[CONF_BASE] = "area_not_registered" except ZoneIdError: _LOGGER.exception("Area Id must be between 1 and 16") @@ -145,14 +152,14 @@ class TestVBE4ConfigFlow(ConfigFlow, domain=DOMAIN): except Exception as e: _LOGGER.exception("Config Flow generic error") else: - _LOGGER.debug("Config_flow [zone] - Une autre zone à configurer") + #_LOGGER.debug("Config_flow [zone] - Une autre zone à configurer") # Update dict self._user_inputs["areas"].append(user_input) # New area to configure return await self.async_step_areas() except AreaAlreadySetError: - _LOGGER.exception("Area (id:{}) is already configured".format(user_input['Zone_id'])) + _LOGGER.exception("Area (id:{}) is already configured".format(user_input['Area_id'])) errors[CONF_BASE] = "area_already_configured" # first call or error diff --git a/custom_components/const.py b/custom_components/const.py index 42ee56f..64c973a 100644 --- a/custom_components/const.py +++ b/custom_components/const.py @@ -11,8 +11,6 @@ from homeassistant.components.climate.const import ( FAN_LOW, FAN_MEDIUM, FAN_HIGH, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, ) from .koolnova.const import ( GlobalMode, diff --git a/custom_components/coordinator.py b/custom_components/coordinator.py new file mode 100644 index 0000000..5c5b384 --- /dev/null +++ b/custom_components/coordinator.py @@ -0,0 +1,41 @@ +""" for Coordinator integration. """ +from __future__ import annotations +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import Throttle +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + DOMAIN, +) + +from .koolnova.device import Koolnova + +_LOGGER = logging.getLogger(__name__) + +class KoolnovaCoordinator(DataUpdateCoordinator): + """ koolnova 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), + ) \ No newline at end of file diff --git a/custom_components/koolnova/device.py b/custom_components/koolnova/device.py index 1ec9e91..8bcf4a4 100644 --- a/custom_components/koolnova/device.py +++ b/custom_components/koolnova/device.py @@ -49,7 +49,7 @@ class Area: @property def id_zone(self) -> int: - ''' Get Zone Id ''' + ''' Get area id ''' return self._id @property @@ -284,7 +284,6 @@ class Koolnova: _LOGGER.debug("Retreive engines ...") for idx in range(1, const.NUM_OF_ENGINES + 1): - _LOGGER.debug("Engine id: {}".format(idx)) engine = Engine(engine_id = idx) ret, engine.throughput = await self._client.engine_throughput(engine_id = idx) ret, engine.state = await self._client.engine_state(engine_id = idx) @@ -356,7 +355,7 @@ class Koolnova: real_temp = zone_dict['real_temp'], order_temp = zone_dict['order_temp'] )) - _LOGGER.debug("Zones registered: {}".format(self._areas)) + _LOGGER.debug("Areas registered: {}".format(self._areas)) return True @property @@ -376,7 +375,7 @@ class Koolnova: return ret, None for idx, area in enumerate(self._areas): if area.id_zone == zone_id: - # update areas list value from modbus response + # update areas list values from modbus response self._areas[idx].state = infos['state'] self._areas[idx].register = infos['register'] self._areas[idx].fan_mode = infos['fan'] @@ -387,16 +386,17 @@ class Koolnova: return ret, self._areas[zone_id - 1] async def update_all_areas(self) -> list: - """ update all areas registered """ + """ update all areas registered and all engines values """ + ##### Areas _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: + # update areas list values from modbus response self._areas[_idx].state = v['state'] self._areas[_idx].register = v['register'] self._areas[_idx].fan_mode = v['fan'] @@ -404,7 +404,35 @@ class Koolnova: self._areas[_idx].real_temp = v['real_temp'] self._areas[_idx].order_temp = v['order_temp'] - return self._areas + ##### Engines + for _idx in range(1, const.NUM_OF_ENGINES + 1): + ret, self._engines[_idx - 1].throughput = await self._client.engine_throughput(engine_id = _idx) + ret, self._engines[_idx - 1].state = await self._client.engine_state(engine_id = _idx) + ret, self._engines[_idx - 1].order_temp = await self._client.engine_order_temp(engine_id = _idx) + + ##### Global mode + ret, self._global_mode = await self._client.global_mode() + if not ret: + _LOGGER.error("Error retreiving global mode") + self._global_mode = const.GlobalMode.COLD + + ##### Efficiency + ret, self._efficiency = await self._client.efficiency() + if not ret: + _LOGGER.error("Error retreiving efficiency") + self._efficiency = const.Efficiency.LOWER_EFF + + ##### Sys state + ret, self._sys_state = await self._client.system_status() + if not ret: + _LOGGER.error("Error retreiving system status") + self._sys_state = const.SysState.SYS_STATE_OFF + + return {"areas": self._areas, + "engines": self._engines, + "glob": self._global_mode, + "eff": self._efficiency, + "sys": self._sys_state} @property def engines(self) -> list: @@ -439,6 +467,7 @@ class Koolnova: raise AssertionError('Input variable must be Enum GlobalMode') ret = await self._client.set_global_mode(val) if not ret: + _LOGGER.error("[GLOBAL] Error writing {} to modbus".format(val)) raise UpdateValueError('Error writing to modbus updated value') self._global_mode = val @@ -456,6 +485,7 @@ class Koolnova: raise AssertionError('Input variable must be Enum Efficiency') ret = await self._client.set_efficiency(val) if not ret: + _LOGGER.error("[EFF] Error writing {} to modbus".format(val)) raise UpdateValueError('Error writing to modbus updated value') self._efficiency = val @@ -472,6 +502,7 @@ class Koolnova: raise AssertionError('Input variable must be Enum SysState') ret = await self._client.set_system_status(val) if not ret: + _LOGGER.error("[SYS_STATE] Error writing {} to modbus".format(val)) raise UpdateValueError('Error writing to modbus updated value') self._sys_state = val diff --git a/custom_components/koolnova/operations.py b/custom_components/koolnova/operations.py index 0b5e93c..f7943b8 100644 --- a/custom_components/koolnova/operations.py +++ b/custom_components/koolnova/operations.py @@ -18,7 +18,7 @@ _LOGGER = log.getLogger(__name__) class Operations: ''' koolnova BMS Modbus operations class ''' - def __init__(self, port:str, timeout:int) -> None: + def __init__(self, port:str, timeout:int, debug:bool=False) -> None: ''' Class constructor ''' self._port = port self._timeout = timeout @@ -33,8 +33,8 @@ class Operations: stopbits=self._stopbits, bytesize=self._bytesize, timeout=self._timeout) - - pymodbus_apply_logging_config("DEBUG") + if debug: + pymodbus_apply_logging_config("DEBUG") def __init__(self, port:str="", @@ -43,7 +43,8 @@ class Operations: parity:str=const.DEFAULT_PARITY, stopbits:int=const.DEFAULT_STOPBITS, bytesize:int=const.DEFAULT_BYTESIZE, - timeout:int=1) -> None: + timeout:int=1, + debug:bool=False) -> None: ''' Class constructor ''' self._port = port self._addr = addr @@ -58,8 +59,8 @@ class Operations: stopbits=self._stopbits, bytesize=self._bytesize, timeout=self._timeout) - - pymodbus_apply_logging_config("DEBUG") + if debug: + pymodbus_apply_logging_config("DEBUG") async def __read_register(self, reg:int) -> (int, bool): ''' Read one holding register (code 0x03) ''' @@ -67,7 +68,7 @@ class Operations: if not self._client.connected: raise ModbusConnexionError('Client Modbus not connected') try: - _LOGGER.debug("reading holding register: {} - Addr: {}".format(hex(reg), self._addr)) + #_LOGGER.debug("reading holding register: {} - Addr: {}".format(hex(reg), self._addr)) rr = await self._client.read_holding_registers(address=reg, count=1, slave=self._addr) if rr.isError(): _LOGGER.error("reading holding register error") @@ -113,7 +114,7 @@ class Operations: if not self._client.connected: raise ModbusConnexionError('Client Modbus not connected') try: - _LOGGER.debug("writing single register: {} - Addr: {} - Val: {}".format(hex(reg), self._addr, hex(val))) + #_LOGGER.debug("writing single register: {} - Addr: {} - Val: {}".format(hex(reg), self._addr, hex(val))) rq = await self._client.write_register(address=reg, value=val, slave=self._addr) if rq.isError(): _LOGGER.error("writing register error") @@ -174,7 +175,7 @@ class Operations: zone_id:int = 0, ) -> (bool, dict): ''' Get Zone Status from Id ''' - _LOGGER.debug("Area : {}".format(zone_id)) + #_LOGGER.debug("Area : {}".format(zone_id)) if zone_id > const.NB_ZONE_MAX or zone_id == 0: raise ZoneIdError('Zone Id must be between 1 to {}'.format(const.NB_ZONE_MAX)) zone_dict = {} @@ -338,7 +339,7 @@ class Operations: return False ret = await self.__write_register(reg = const.REG_START_ZONE + (4 * (zone_id - 1)) + const.REG_TEMP_ORDER, val = int(val * 2)) if not ret: - _LOGGER.error('Error writing zone order temperature') + _LOGGER.error('Error writing area order temperature') return ret @@ -389,13 +390,13 @@ class Operations: """ set area state """ register:const.ZoneRegister = const.ZoneRegister.REGISTER_OFF if id_zone > const.NB_ZONE_MAX or id_zone == 0: - raise ZoneIdError('Zone Id must be between 1 to 16') + raise ZoneIdError('Area Id must be between 1 to 16') # retreive values to combine the new state with register read ret, register, _ = await self.area_state_and_register(id_zone = id_zone) if not ret: _LOGGER.error("Error reading state and register mode") return ret - _LOGGER.debug("register & state: {}".format(hex((int(register) << 1) | (int(val) & 0b01)))) + #_LOGGER.debug("register & state: {}".format(hex((int(register) << 1) | (int(val) & 0b01)))) ret = await self.__write_register(reg = const.REG_START_ZONE + (4 * (id_zone - 1)) + const.REG_LOCK_ZONE, val = int(int(register) << 1) | (int(val) & 0b01)) if not ret: @@ -416,7 +417,7 @@ class Operations: if not ret: _LOGGER.error("Error reading fan and clim mode") return ret - _LOGGER.debug("Fan & Clim: {}".format(hex((int(fan) << 4) | (int(val) & 0x0F)))) + #_LOGGER.debug("Fan & Clim: {}".format(hex((int(fan) << 4) | (int(val) & 0x0F)))) ret = await self.__write_register(reg = const.REG_START_ZONE + (4 * (id_zone - 1)) + const.REG_STATE_AND_FLOW, val = int(int(fan) << 4) | (int(val) & 0x0F)) if not ret: @@ -437,7 +438,7 @@ class Operations: if not ret: _LOGGER.error("Error reading fan and clim mode") return ret - _LOGGER.debug("Fan & Clim: {}".format(hex((int(val) << 4) | (int(clim) & 0x0F)))) + #_LOGGER.debug("Fan & Clim: {}".format(hex((int(val) << 4) | (int(clim) & 0x0F)))) ret = await self.__write_register(reg = const.REG_START_ZONE + (4 * (id_zone - 1)) + const.REG_STATE_AND_FLOW, val = int(int(val) << 4) | (int(clim) & 0x0F)) if not ret: diff --git a/custom_components/select.py b/custom_components/select.py index cd8e5f5..ed0fd99 100644 --- a/custom_components/select.py +++ b/custom_components/select.py @@ -3,11 +3,15 @@ import logging -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback, Event, State from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.components.select import SelectEntity from homeassistant.util import Throttle +from homeassistant.const import UnitOfTime +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) from .const import ( DOMAIN, @@ -17,7 +21,9 @@ from .const import ( EFF_MODES, EFF_TRANSLATION, ) -from homeassistant.const import UnitOfTime + +from .coordinator import KoolnovaCoordinator + from .koolnova.device import Koolnova from .koolnova.const import ( GlobalMode, @@ -32,21 +38,23 @@ async def async_setup_entry(hass: HomeAssistant, ): """ Setup select entries """ - for device in hass.data[DOMAIN]: - _LOGGER.debug("Device: {}".format(device)) - entities = [ - GlobalModeSelect(device), - EfficiencySelect(device), - ] - async_add_entities(entities) + device = hass.data[DOMAIN]["device"] + coordinator = hass.data[DOMAIN]["coordinator"] -class GlobalModeSelect(SelectEntity): + entities = [ + GlobalModeSelect(coordinator, device), + EfficiencySelect(coordinator, device), + ] + async_add_entities(entities) + +class GlobalModeSelect(CoordinatorEntity, SelectEntity): """ Select component to set global HVAC mode """ - def __init__(self, + def __init__(self, + coordinator: KoolnovaCoordinator, # pylint: disable=unused-argument device: Koolnova, # pylint: disable=unused-argument, ) -> None: - super().__init__() + super().__init__(coordinator) self._attr_options = GLOBAL_MODES self._device = device self._attr_name = f"{device.name} Global HVAC Mode" @@ -71,20 +79,22 @@ class GlobalModeSelect(SelectEntity): await self._device.set_global_mode(GlobalMode(opt)) self.select_option(option) - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """ Retrieve latest state of global mode """ + @callback + def _handle_coordinator_update(self) -> None: + """ Handle updated data from the coordinator + Retrieve latest state of global mode """ self.select_option( - GLOBAL_MODE_TRANSLATION[int(self._device.global_mode)] + GLOBAL_MODE_TRANSLATION[int(self.coordinator.data['glob'])] ) -class EfficiencySelect(SelectEntity): +class EfficiencySelect(CoordinatorEntity, SelectEntity): """Select component to set global efficiency """ - def __init__(self, + def __init__(self, + coordinator: KoolnovaCoordinator, # pylint: disable=unused-argument device: Koolnova, # pylint: disable=unused-argument, ) -> None: - super().__init__() + super().__init__(coordinator) self._attr_options = EFF_MODES self._device = device self._attr_name = f"{device.name} Global HVAC Efficiency" @@ -114,9 +124,10 @@ class EfficiencySelect(SelectEntity): await self._device.set_efficiency(Efficiency(opt)) self.select_option(option) - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """ Retrieve latest state of global efficiency """ + @callback + def _handle_coordinator_update(self) -> None: + """ Handle updated data from the coordinator + Retrieve latest state of global efficiency """ self.select_option( - EFF_TRANSLATION[int(self._device.efficiency)] + EFF_TRANSLATION[int(self.coordinator.data['eff'])] ) \ No newline at end of file diff --git a/custom_components/sensor.py b/custom_components/sensor.py index 1f3f636..fe09d75 100644 --- a/custom_components/sensor.py +++ b/custom_components/sensor.py @@ -1,4 +1,4 @@ -""" Implementation du composant sensors """ +""" for sensors components """ import logging from datetime import datetime, timedelta @@ -11,20 +11,35 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorStateClass, ) + +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) + from homeassistant.helpers.event import ( async_track_time_interval, async_track_state_change_event, ) + +from homeassistant.const import ( + ATTR_TEMPERATURE, + UnitOfTime, + UnitOfTemperature +) + from .const import ( DOMAIN ) -from homeassistant.const import UnitOfTime + +from .coordinator import KoolnovaCoordinator + from .koolnova.device import ( Koolnova, Engine, ) _LOGGER = logging.getLogger(__name__) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, @@ -32,19 +47,18 @@ async def async_setup_entry(hass: HomeAssistant, """ Configuration des entités sensor à partir de la configuration ConfigEntry passée en argument """ - _LOGGER.debug("Calling async_setup_entry - datas: {}".format(entry.data)) - _LOGGER.debug("HASS data: {}".format(hass.data[DOMAIN])) - for device in hass.data[DOMAIN]: - _LOGGER.debug("Device: {}".format(device)) - entities = [ - DiagnosticsSensor(device, "Device", entry.data), - DiagnosticsSensor(device, "Address", entry.data), - DiagModbusSensor(device, entry.data), - ] - for engine in device.engines: - _LOGGER.debug("Engine: {}".format(engine)) - entities.append(DiagEngineSensor(device, engine)) - async_add_entities(entities) + + device = hass.data[DOMAIN]["device"] + coordinator = hass.data[DOMAIN]["coordinator"] + entities = [ + DiagnosticsSensor(device, "Device", entry.data), + DiagnosticsSensor(device, "Address", entry.data), + DiagModbusSensor(device, entry.data), + ] + for engine in device.engines: + entities.append(DiagEngineThroughputSensor(coordinator, device, engine)) + entities.append(DiagEngineTempOrderSensor(coordinator, device, engine)) + async_add_entities(entities) class DiagnosticsSensor(SensorEntity): # pylint: disable = too-many-instance-attributes @@ -104,30 +118,69 @@ class DiagModbusSensor(SensorEntity): """ Do not poll for those entities """ return False -class DiagEngineSensor(SensorEntity): +class DiagEngineThroughputSensor(CoordinatorEntity, SensorEntity): # pylint: disable = too-many-instance-attributes """ Representation of a Sensor """ _attr_entity_category: EntityCategory | None = EntityCategory.DIAGNOSTIC def __init__(self, + coordinator: KoolnovaCoordinator, # pylint: disable=unused-argument device: Koolnova, # pylint: disable=unused-argument engine: Engine, # pylint: disable=unused-argument ) -> None: """ Class constructor """ + super().__init__(coordinator) self._device = device self._engine = engine self._attr_name = f"{device.name} Engine AC{engine.engine_id} Throughput" self._attr_entity_registry_enabled_default = True self._attr_device_info = self._device.device_info self._attr_unique_id = f"{DOMAIN}-Engine-AC{engine.engine_id}-throughput-sensor" - self._attr_native_value = f"TEST" + self._attr_native_value = "{}".format(engine.throughput) @property def icon(self) -> str | None: - return "mdi:monitor" + return "mdi:thermostat-cog" + + @callback + def _handle_coordinator_update(self) -> None: + """ Handle updated data from the coordinator """ + for _cur_engine in self.coordinator.data['engines']: + if self._engine.engine_id == _cur_engine.engine_id: + self._attr_native_value = "{}".format(_cur_engine.throughput) + self.async_write_ha_state() + +class DiagEngineTempOrderSensor(CoordinatorEntity, SensorEntity): + # pylint: disable = too-many-instance-attributes + """ Representation of a Sensor """ + + _attr_entity_category: EntityCategory | None = EntityCategory.DIAGNOSTIC + _attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS + + def __init__(self, + coordinator: KoolnovaCoordinator, # pylint: disable=unused-argument + device: Koolnova, # pylint: disable=unused-argument + engine: Engine, # pylint: disable=unused-argument + ) -> None: + """ Class constructor """ + super().__init__(coordinator) + self._device = device + self._engine = engine + self._attr_name = f"{device.name} Engine AC{engine.engine_id} Temperature Order" + self._attr_entity_registry_enabled_default = True + self._attr_device_info = self._device.device_info + self._attr_unique_id = f"{DOMAIN}-Engine-AC{engine.engine_id}-temp-order-sensor" + self._attr_native_value = "{}".format(engine.order_temp) @property - def should_poll(self) -> bool: - """ Do not poll for those entities """ - return False + def icon(self) -> str | None: + return "mdi:thermometer-lines" + + @callback + def _handle_coordinator_update(self) -> None: + """ Handle updated data from the coordinator """ + for _cur_engine in self.coordinator.data['engines']: + if self._engine.engine_id == _cur_engine.engine_id: + self._attr_native_value = "{}".format(_cur_engine.order_temp) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/strings.json b/custom_components/strings.json index aa2858d..20d421e 100644 --- a/custom_components/strings.json +++ b/custom_components/strings.json @@ -15,7 +15,7 @@ "Parity": "Parity", "Stopbits": "Stopbits", "Timeout": "Timeout", - "DiscoverArea": "Area discovery" + "Debug": "Debug" } }, "areas": { @@ -23,16 +23,16 @@ "description": "Information sur la zone à configurer", "data": { "Name": "Name", - "Zone_id": "Zone_id", + "Area_id": "Identifiant de la zone", "Other_area": "Ajouter une nouvelle zone" } } }, "error": { "cannot_connect": "Cannot connected to Koolnova system", - "area_not_registered": "Area is not registered to the Koolnova system", - "area_already_configured": "Area is already configured", - "zone_id_error": "Zone Id must an integer between 1 and 16" + "area_not_registered": "This Area is not registered to the Koolnova system", + "area_already_configured": "This Area is already configured", + "zone_id_error": "Area Id must an integer between 1 and 16" } } } \ No newline at end of file diff --git a/custom_components/switch.py b/custom_components/switch.py index 1fea4b3..58fe1a9 100644 --- a/custom_components/switch.py +++ b/custom_components/switch.py @@ -3,16 +3,22 @@ from __future__ import annotations import logging -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback, Event, State from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.components.switch import SwitchEntity from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, +) from .const import ( DOMAIN, MIN_TIME_BETWEEN_UPDATES, ) + +from .coordinator import KoolnovaCoordinator + from homeassistant.const import UnitOfTime from .koolnova.device import Koolnova from .koolnova.const import ( @@ -27,21 +33,23 @@ async def async_setup_entry(hass: HomeAssistant, ): """ Setup switch entries """ - for device in hass.data[DOMAIN]: - _LOGGER.debug("Device: {}".format(device)) - entities = [ - SystemStateSwitch(device), - ] - async_add_entities(entities) + device = hass.data[DOMAIN]["device"] + coordinator = hass.data[DOMAIN]["coordinator"] -class SystemStateSwitch(SwitchEntity): + entities = [ + SystemStateSwitch(coordinator, device), + ] + async_add_entities(entities) + +class SystemStateSwitch(CoordinatorEntity, SwitchEntity): """Select component to set system state """ _attr_has_entity_name = True def __init__(self, + coordinator: KoolnovaCoordinator, # pylint: disable=unused-argument device: Koolnova, # pylint: disable=unused-argument, ) -> None: - super().__init__() + super().__init__(coordinator) self._device = device self._attr_name = f"{device.name} Global HVAC State" self._attr_device_info = device.device_info @@ -50,18 +58,20 @@ class SystemStateSwitch(SwitchEntity): async def async_turn_on(self, **kwargs): """ Turn the entity on. """ - self._is_on = True + _LOGGER.debug("Turn on system") await self._device.set_sys_state(SysState.SYS_STATE_ON) + self._is_on = True async def async_turn_off(self, **kwargs): """ Turn the entity off. """ - self._is_on = False + _LOGGER.debug("Turn off system") await self._device.set_sys_state(SysState.SYS_STATE_OFF) + self._is_on = False - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """ Retrieve latest state. """ - self._is_on = bool(int(self._device.sys_state)) + @callback + def _handle_coordinator_update(self) -> None: + """ Handle updated data from the coordinator """ + self._is_on = bool(int(self.coordinator.data['sys'])) @property def is_on(self): @@ -71,9 +81,4 @@ class SystemStateSwitch(SwitchEntity): @property def icon(self) -> str | None: """Icon of the entity.""" - return "mdi:power" - - @property - def should_poll(self) -> bool: - """ Do not poll for this entity """ - return False \ No newline at end of file + return "mdi:power" \ No newline at end of file diff --git a/custom_components/translations/fr.json b/custom_components/translations/fr.json index e8a0f4c..a0ece2d 100644 --- a/custom_components/translations/fr.json +++ b/custom_components/translations/fr.json @@ -15,7 +15,7 @@ "Parity": "Parité", "Stopbits": "Nombre de bits de stop", "Timeout": "Timeout", - "DiscoverArea": "Type de découverte de zones" + "Debug": "Debug" } }, "areas": { @@ -23,16 +23,16 @@ "description": "Information sur la zone à configurer", "data": { "Name": "Nom de la zone", - "Zone_id": "Zone_id", + "Area_id": "Identifiant de la zone", "Other_area": "Ajouter une nouvelle zone" } } }, "error": { "cannot_connect": "Cannot connected to Koolnova system", - "area_not_registered": "Area is not registered to the Koolnova system", - "area_already_configured": "Area is already configured", - "zone_id_error": "Zone Id must an integer between 1 and 16" + "area_not_registered": "This Area is not registered to the Koolnova system", + "area_already_configured": "This Area is already configured", + "zone_id_error": "Area Id must an integer between 1 and 16" } } } \ No newline at end of file