From e939ae6a5ae28aba02573d31c47a553e6d149ecb Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 27 Jan 2022 18:20:11 +0100 Subject: [PATCH] Create client after init failure / Reduce CCU calls (#239) * Try create client after init failure * Reduce CCU calls * Cleanup device.py * Update Example und test --- changelog.txt | 4 + example.py | 13 +- hahomematic/central_unit.py | 81 +++++++---- hahomematic/client.py | 18 +-- hahomematic/const.py | 4 +- hahomematic/device.py | 261 ++++++++++++++++++++++++------------ hahomematic/entity.py | 86 +++++------- hahomematic/support.py | 2 +- setup.py | 2 +- tests/test_central.py | 4 +- 10 files changed, 291 insertions(+), 184 deletions(-) diff --git a/changelog.txt b/changelog.txt index c6c5f3b8..f6d91559 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +Version 0.28.0 (2022-01-27) +- Try create client after init failure +- Reduce CCU calls + Version 0.27.2 (2022-01-25) - Optimize data_load diff --git a/example.py b/example.py index 41988bf4..0400e1b7 100644 --- a/example.py +++ b/example.py @@ -33,13 +33,10 @@ def systemcallback(self, src, *args): self.got_devices = True print("Number of new device descriptions: %i" % len(args[0])) return - elif src == const.HH_EVENT_DEVICES_CREATED: - if len(self.central.hm_devices) > 1: - self.got_devices = True - print("New devices:") - print(len(args[0])) - print("New entities:") - print(len(args[1])) + elif src == const.HH_EVENT_DEVICES_CREATED and args and args[0] and len(args[0]) > 0: + self.got_devices = True + print("New devices:") + print(len(args[0])) return for arg in args: print("argument: %s" % arg) @@ -102,7 +99,7 @@ async def example_run(self): await self.central.create_clients(client_configs) # Once the central_1 is running we subscribe to receive messages. await self.central.init_clients() - + self.central.start_connection_checker() while not self.got_devices and self.SLEEPCOUNTER < 20: print("Waiting for devices") self.SLEEPCOUNTER += 1 diff --git a/hahomematic/central_unit.py b/hahomematic/central_unit.py index b4ae102c..a1f0f358 100644 --- a/hahomematic/central_unit.py +++ b/hahomematic/central_unit.py @@ -110,6 +110,7 @@ def __init__(self, central_config: CentralConfig): hm_data.INSTANCES[self.instance_name] = self self._connection_checker = ConnectionChecker(self) self.hub: HmHub | HmDummyHub | None = None + self._saved_client_configs: set[hm_client.ClientConfig] | None = None @property def domain(self) -> str: @@ -225,14 +226,14 @@ async def load_caches(self) -> None: _LOGGER.warning("load_caches: Failed to load caches.") await self.clear_all() - def _create_devices(self) -> None: + async def _create_devices(self) -> None: """Create the devices.""" if not self._clients: raise Exception( "_create_devices: No clients initialized. Not starting central_unit." ) try: - create_devices(self) + await create_devices(self) except Exception as err: _LOGGER.error( "_create_devices: Exception (%s) Failed to create entities", err.args @@ -325,7 +326,7 @@ async def add_new_devices( await self.paramset_descriptions.save() await client.fetch_names() await self.names.save() - create_devices(self) + await create_devices(self) async def stop(self) -> None: """ @@ -334,14 +335,8 @@ async def stop(self) -> None: """ _LOGGER.info("stop: Stop connection checker.") self._stop_connection_checker() - for name, client in self._clients.items(): - if await client.proxy_de_init(): - _LOGGER.info("stop: Proxy de-initialized: %s", name) - client.stop() - _LOGGER.info("stop: Clearing existing clients. Please recreate them!") - self._clients.clear() - self._clients_by_init_url.clear() + await self._de_init_client() # un-register this instance from XMLRPCServer self._xml_rpc_server.un_register_central(central=self) @@ -351,11 +346,37 @@ async def stop(self) -> None: _LOGGER.info("stop: Removing instance") del hm_data.INSTANCES[self.instance_name] - async def create_clients(self, client_configs: set[hm_client.ClientConfig]) -> None: + async def _de_init_client(self) -> None: + """De-init clients""" + for name, client in self._clients.items(): + if await client.proxy_de_init(): + _LOGGER.info("stop: Proxy de-initialized: %s", name) + client.stop() + + _LOGGER.info("stop: Clearing existing clients.") + self._clients.clear() + self._clients_by_init_url.clear() + + async def create_clients( + self, client_configs: set[hm_client.ClientConfig] | None = None + ) -> bool: """Create clients for the central unit. Start connection checker afterwards""" + if client_configs is None: + client_configs = self._saved_client_configs + _LOGGER.warning( + "create_clients: Trying to create interfaces to central %s. Using saved client configs)", + self.instance_name, + ) - for client_config in client_configs: - try: + if client_configs is None: + _LOGGER.warning( + "create_clients: Failed to create interfaces to central %s. Missing client configs)", + self.instance_name, + ) + return False + + try: + for client_config in client_configs: if client := await client_config.get_client(): _LOGGER.debug( "create_clients: Adding client %s to central.", @@ -366,22 +387,25 @@ async def create_clients(self, client_configs: set[hm_client.ClientConfig]) -> N if client.init_url not in self._clients_by_init_url: self._clients_by_init_url[client.init_url] = [] self._clients_by_init_url[client.init_url].append(client) - await self.rooms.load() - self._create_devices() - except BaseHomematicException as ex: - _LOGGER.debug( - "create_clients: Failed to create interface %s to central. (%s)", - client_config.name, - ex.args, - ) + await self.rooms.load() + await self._create_devices() + return True + except BaseHomematicException as ex: + await self._de_init_client() + _LOGGER.warning( + "create_clients: Failed to create interfaces for central %s. (%s)", + self.clients, + ex.args, + ) + # Save client config for later use + self._saved_client_configs = client_configs + return False async def init_clients(self) -> None: """Init clients of control unit, and start connection checker.""" for client in self._clients.values(): await client.proxy_init() - self._start_connection_checker() - def create_task(self, target: Awaitable) -> None: """Add task to the executor pool.""" try: @@ -421,7 +445,7 @@ async def async_add_executor_job( ) raise HaHomematicException from cer - def _start_connection_checker(self) -> None: + def start_connection_checker(self) -> None: """Start the connection checker.""" if self.model is not BACKEND_PYDEVCCU: self._connection_checker.start() @@ -555,8 +579,6 @@ async def get_paramset( interface_id: str, channel_address: str, paramset_key: str, - value: Any, - rx_mode: str | None = None, ) -> Any: """Set paramsets manually.""" @@ -679,6 +701,13 @@ async def _check_connection(self) -> None: ) await asyncio.sleep(connection_checker_interval) await self._central.reconnect() + elif len(self._central.clients) == 0: + _LOGGER.error( + "check_connection: No clients exist. Trying to create clients for server %s", + self._central.instance_name, + ) + await self._central.create_clients() + await self._central.init_clients() await asyncio.sleep(connection_checker_interval) except NoConnection as nex: _LOGGER.error("check_connection: no connection: %s", nex.args) diff --git a/hahomematic/client.py b/hahomematic/client.py index c454f716..30b4fc7a 100644 --- a/hahomematic/client.py +++ b/hahomematic/client.py @@ -324,7 +324,7 @@ async def fetch_paramset_description( """ Fetch a specific paramset and add it to the known ones. """ - _LOGGER.debug("fetch_paramset: %s for %s", paramset, channel_address) + _LOGGER.debug("fetch_paramset_description: %s for %s", paramset, channel_address) try: parameter_data = await self._proxy.getParamsetDescription( @@ -338,7 +338,7 @@ async def fetch_paramset_description( ) except BaseHomematicException as hhe: _LOGGER.warning( - "fetch_paramset: %s (%s) Unable to get paramset %s for channel_address %s", + "fetch_paramset_description: %s (%s) Unable to get paramset %s for channel_address %s", hhe.name, hhe.args, paramset, @@ -356,7 +356,7 @@ async def fetch_paramset_descriptions( device_description=device_description, relevant_paramsets=RELEVANT_PARAMSETS ) for address, paramsets in data.items(): - _LOGGER.debug("fetch_paramsets for %s", address) + _LOGGER.debug("fetch_paramset_descriptions for %s", address) for paramset, paramset_description in paramsets.items(): self._central.paramset_descriptions.add( interface_id=self.interface_id, @@ -376,7 +376,7 @@ async def get_paramset_descriptions( paramsets: dict[str, dict[str, Any]] = {} address = device_description[ATTR_HM_ADDRESS] paramsets[address] = {} - _LOGGER.debug("get_paramsets for %s", address) + _LOGGER.debug("get_paramset_descriptions for %s", address) for paramset in device_description.get(ATTR_HM_PARAMSETS, []): if relevant_paramsets and paramset not in relevant_paramsets: continue @@ -394,10 +394,10 @@ async def get_paramset_descriptions( ) return paramsets - async def get_all_paramsets( + async def get_all_paramset_descriptions( self, device_descriptions: list[dict[str, Any]] ) -> dict[str, dict[str, Any]]: - """Get all paramsets for provided device descriptions.""" + """Get all paramset descriptions for provided device descriptions.""" all_paramsets: dict[str, dict[str, Any]] = {} for device_description in device_descriptions: all_paramsets.update( @@ -409,11 +409,11 @@ async def get_all_paramsets( async def update_paramset_descriptions(self, device_address: str) -> None: """ - Update paramsets for provided device_address. + Update paramsets descriptionsfor provided device_address. """ if not self._central.raw_devices.get_interface(interface_id=self.interface_id): _LOGGER.warning( - "update_paramsets: Interface ID missing in central_unit.raw_devices.devices_raw_dict. Not updating paramsets for %s.", + "update_paramset_descriptions: Interface ID missing in central_unit.raw_devices.devices_raw_dict. Not updating paramsets for %s.", device_address, ) return @@ -421,7 +421,7 @@ async def update_paramset_descriptions(self, device_address: str) -> None: interface_id=self.interface_id, device_address=device_address ): _LOGGER.warning( - "update_paramsets: Channel missing in central_unit.raw_devices.devices_raw_dict[_interface_id]. Not updating paramsets for %s.", + "update_paramset_descriptions: Channel missing in central_unit.raw_devices.devices_raw_dict[_interface_id]. Not updating paramsets for %s.", device_address, ) return diff --git a/hahomematic/const.py b/hahomematic/const.py index dc4f788b..f2811047 100644 --- a/hahomematic/const.py +++ b/hahomematic/const.py @@ -45,6 +45,7 @@ # However, usually multiple of these events are fired, so we should only # act on the last one. This also only seems to fire on channel 0. EVENT_CONFIG_PENDING = "CONFIG_PENDING" +EVENT_UPDATE_PENDING = "UPDATE_PENDING" EVENT_ERROR = "ERROR" # Only available on CCU @@ -72,6 +73,7 @@ EVENT_ERROR, EVENT_STICKY_UN_REACH, EVENT_UN_REACH, + EVENT_UPDATE_PENDING, ] BUTTON_ACTIONS = ["RESET_MOTION", "RESET_PRESENCE"] @@ -125,7 +127,6 @@ "TEMPERATURE_LIMITER", "TEMPERATURE_OUT_OF_RANGE", "TIME_OF_OPERATION", - "UPDATE_PENDING", "WOCHENPROGRAMM", ] @@ -266,6 +267,7 @@ class HmEntityUsage(Enum): CE_SECONDARY = "ce_secondary" CE_SENSOR = "ce_sensor" ENTITY_NO_CREATE = "entity_no_create" + ENTITY_NO_PARAMSET_DATA = "entity_no_paramset_data" ENTITY = "ENTITY" EVENT = "event" diff --git a/hahomematic/device.py b/hahomematic/device.py index a026b55c..f89fd5b3 100644 --- a/hahomematic/device.py +++ b/hahomematic/device.py @@ -9,6 +9,7 @@ from typing import Any import hahomematic.central_unit as hm_central +import hahomematic.client as hm_client from hahomematic.const import ( ACCEPT_PARAMETER_ONLY_ON_CHANNEL, ATTR_HM_FIRMWARE, @@ -33,6 +34,7 @@ INIT_DATETIME, MANUFACTURER, OPERATION_EVENT, + OPERATION_READ, OPERATION_WRITE, PARAMSET_VALUES, RELEVANT_PARAMSETS, @@ -128,6 +130,8 @@ def __init__( device_address=device_address, device_type=self.device_type, ) + self._paramset_cache = ParamsetCache(device=self) + _LOGGER.debug( "__init__: Initialized device: %s, %s, %s, %s", self._interface_id, @@ -141,6 +145,11 @@ def central(self) -> hm_central.CentralUnit: """Return the central unit.""" return self._central + @property + def client(self) -> hm_client.Client: + """Return the client.""" + return self._client + @property def interface_id(self) -> str: """Return the interface_id.""" @@ -161,6 +170,11 @@ def room(self) -> str | None: """Return the room.""" return self._central.rooms.get_room(self._device_address) + @property + def paramset_cache(self) -> ParamsetCache: + """Return the paramset cache.""" + return self._paramset_cache + @property def _e_unreach(self) -> GenericEntity | None: """Return th UNREACH entity""" @@ -176,21 +190,6 @@ def _e_config_pending(self) -> GenericEntity | None: """Return th CONFIG_PENDING entity""" return self.entities.get((f"{self._device_address}:0", EVENT_CONFIG_PENDING)) - async def init_device_entities(self) -> None: - """initialize the device relevant entities.""" - data_load: bool = False - if self._e_unreach is not None: - if DATA_LOAD_SUCCESS == await self._e_unreach.load_entity_data(): - data_load = True - if self._e_sticky_un_reach is not None: - if DATA_LOAD_SUCCESS == await self._e_sticky_un_reach.load_entity_data(): - data_load = True - if self._e_config_pending is not None: - if DATA_LOAD_SUCCESS == await self._e_config_pending.load_entity_data(): - data_load = True - if data_load: - self._set_last_update() - def add_hm_entity(self, hm_entity: BaseEntity) -> None: """Add a hm entity to a device.""" if isinstance(hm_entity, GenericEntity): @@ -336,32 +335,19 @@ async def reload_paramset_descriptions(self) -> None: entity.update_parameter_data() self.update_device() - async def load_channel_data(self, channel_address: str) -> int: - """Load data""" - try: - paramset = await self._client.get_paramset( - channel_address=channel_address, paramset_key=PARAMSET_VALUES - ) - for parameter, value in paramset.items(): - if entity := self.entities.get((channel_address, parameter)): - entity.set_value(value=value) - - return DATA_LOAD_SUCCESS - except BaseHomematicException as bhe: - _LOGGER.debug( - " %s: Failed to get paramset for %s, %s: %s", - self.device_type, - channel_address, - bhe, - ) - return DATA_LOAD_FAIL + async def load_paramset_cache(self) -> None: + """Init the parameter cache.""" + if len(self.entities) > 0: + await self._paramset_cache.init() + _LOGGER.debug( + "init_data: Skipping load_data, missing entities for %s.", + self.device_address, + ) - # pylint: disable=too-many-nested-blocks - def create_entities(self) -> set[BaseEntity]: + def create_entities_and_append_to_device(self) -> None: """ Create the entities associated to this device. """ - new_entities: list[BaseEntity] = [] for channel_address in self._channels: if not self._central.paramset_descriptions.get_by_interface_channel_address( interface_id=self._interface_id, channel_address=channel_address @@ -386,25 +372,22 @@ def create_entities(self) -> set[BaseEntity]: channel_address=channel_address, paramset=paramset, ).items(): - entity: GenericEntity | None - if ( parameter_data[ATTR_HM_OPERATIONS] & OPERATION_EVENT and parameter in CLICK_EVENTS ): - self.create_event( + self._create_event_and_append_to_device( channel_address=channel_address, parameter=parameter, parameter_data=parameter_data, ) if self.device_type in HM_VIRTUAL_REMOTES: - entity = self.create_action( + self._create_action_and_append_to_device( channel_address=channel_address, parameter=parameter, parameter_data=parameter_data, ) - if entity is not None: - new_entities.append(entity) + if ( not parameter_data[ATTR_HM_OPERATIONS] & OPERATION_EVENT and not parameter_data[ATTR_HM_OPERATIONS] & OPERATION_WRITE @@ -422,13 +405,12 @@ def create_entities(self) -> set[BaseEntity]: ) continue if parameter not in CLICK_EVENTS: - entity = self.create_entity( + self._create_entity_and_append_to_device( channel_address=channel_address, parameter=parameter, parameter_data=parameter_data, ) - if entity is not None: - new_entities.append(entity) + # create custom entities if self.is_custom_entity: _LOGGER.debug( @@ -437,20 +419,16 @@ def create_entities(self) -> set[BaseEntity]: self._device_address, self.device_type, ) - # Call the custom creation function. + # Call the custom creation function. for (device_func, group_base_channels) in get_device_funcs( self.device_type, self.sub_type ): - custom_entities: list[CustomEntity] = device_func( - self, self._device_address, group_base_channels - ) - new_entities.extend(custom_entities) - return set(new_entities) + device_func(self, self._device_address, group_base_channels) - def create_action( + def _create_action_and_append_to_device( self, channel_address: str, parameter: str, parameter_data: dict[str, Any] - ) -> HmAction | None: + ) -> None: """Create the actions associated to this device""" unique_id = generate_unique_id( domain=self._central.domain, @@ -460,7 +438,7 @@ def create_action( prefix=f"button_{self._central.instance_name}", ) _LOGGER.debug( - "create_event: Creating action for %s, %s, %s", + "create_action_and_append_to_device: Creating action for %s, %s, %s", channel_address, parameter, self._interface_id, @@ -474,12 +452,10 @@ def create_action( parameter_data=parameter_data, ): action.add_to_collections() - return action - return None - def create_event( + def _create_event_and_append_to_device( self, channel_address: str, parameter: str, parameter_data: dict[str, Any] - ) -> BaseEvent | None: + ) -> None: """Create action event entity.""" if (channel_address, parameter) not in self._central.entity_event_subscriptions: self._central.entity_event_subscriptions[(channel_address, parameter)] = [] @@ -493,7 +469,7 @@ def create_event( ) _LOGGER.debug( - "create_event: Creating event for %s, %s, %s", + "create_event_and_append_to_device: Creating event for %s, %s, %s", channel_address, parameter, self._interface_id, @@ -510,11 +486,10 @@ def create_event( ) if action_event: action_event.add_to_collections() - return action_event - def create_entity( + def _create_entity_and_append_to_device( self, channel_address: str, parameter: str, parameter_data: dict[str, Any] - ) -> GenericEntity | None: + ) -> None: """ Helper that looks at the paramsets, decides which default platform should be used, and creates the required entities. @@ -526,7 +501,9 @@ def create_entity( channel_no=get_device_channel(channel_address), ): _LOGGER.debug( - "create_entity: Ignoring parameter: %s (%s)", parameter, channel_address + "create_entity_and_append_to_device: Ignoring parameter: %s (%s)", + parameter, + channel_address, ) return None if (channel_address, parameter) not in self._central.entity_event_subscriptions: @@ -539,10 +516,13 @@ def create_entity( parameter=parameter, ) if unique_id in self._central.hm_entities: - _LOGGER.debug("create_entity: Skipping %s (already exists)", unique_id) + _LOGGER.debug( + "create_entity_and_append_to_device: Skipping %s (already exists)", + unique_id, + ) return None _LOGGER.debug( - "create_entity: Creating entity for %s, %s, %s", + "create_entity_and_append_to_device: Creating entity for %s, %s, %s", channel_address, parameter, self._interface_id, @@ -552,7 +532,7 @@ def create_entity( if parameter_data[ATTR_HM_TYPE] == TYPE_ACTION: if parameter_data[ATTR_HM_OPERATIONS] == OPERATION_WRITE: _LOGGER.debug( - "create_entity: action (action): %s %s", + "create_entity_and_append_to_device: action (action): %s %s", channel_address, parameter, ) @@ -574,7 +554,7 @@ def create_entity( ) else: _LOGGER.debug( - "create_entity: switch (action): %s %s", + "create_entity_and_append_to_device: switch (action): %s %s", channel_address, parameter, ) @@ -588,7 +568,7 @@ def create_entity( else: if parameter_data[ATTR_HM_OPERATIONS] == OPERATION_WRITE: _LOGGER.debug( - "create_entity: action (action): %s %s", + "create_entity_and_append_to_device: action (action): %s %s", channel_address, parameter, ) @@ -601,7 +581,9 @@ def create_entity( ) elif parameter_data[ATTR_HM_TYPE] == TYPE_BOOL: _LOGGER.debug( - "create_entity: switch: %s %s", channel_address, parameter + "create_entity_and_append_to_device: switch: %s %s", + channel_address, + parameter, ) entity = HmSwitch( device=self, @@ -612,7 +594,9 @@ def create_entity( ) elif parameter_data[ATTR_HM_TYPE] == TYPE_ENUM: _LOGGER.debug( - "create_entity: select: %s %s", channel_address, parameter + "create_entity_and_append_to_device: select: %s %s", + channel_address, + parameter, ) entity = HmSelect( device=self, @@ -623,7 +607,7 @@ def create_entity( ) elif parameter_data[ATTR_HM_TYPE] == TYPE_FLOAT: _LOGGER.debug( - "create_entity: number.integer: %s %s", + "create_entity_and_append_to_device: number.integer: %s %s", channel_address, parameter, ) @@ -636,7 +620,9 @@ def create_entity( ) elif parameter_data[ATTR_HM_TYPE] == TYPE_INTEGER: _LOGGER.debug( - "create_entity: number.float: %s %s", channel_address, parameter + "create_entity_and_append_to_device: number.float: %s %s", + channel_address, + parameter, ) entity = HmInteger( device=self, @@ -648,7 +634,9 @@ def create_entity( elif parameter_data[ATTR_HM_TYPE] == TYPE_STRING: # There is currently no entity platform in HA for this. _LOGGER.debug( - "create_entity: text: %s %s", channel_address, parameter + "create_entity_and_append_to_device: text: %s %s", + channel_address, + parameter, ) entity = HmText( device=self, @@ -659,7 +647,7 @@ def create_entity( ) else: _LOGGER.warning( - "unsupported actor: %s %s %s", + "create_entity_and_append_to_device: unsupported actor: %s %s %s", channel_address, parameter, parameter_data[ATTR_HM_TYPE], @@ -668,7 +656,9 @@ def create_entity( # Also check, if sensor could be a binary_sensor due to value_list. if _is_binary_sensor(parameter_data): _LOGGER.debug( - "create_entity: binary_sensor: %s %s", channel_address, parameter + "create_entity_and_append_to_device: binary_sensor: %s %s", + channel_address, + parameter, ) entity = HmBinarySensor( device=self, @@ -679,7 +669,9 @@ def create_entity( ) else: _LOGGER.debug( - "create_entity: sensor: %s %s", channel_address, parameter + "create_entity_and_append_to_device: sensor: %s %s", + channel_address, + parameter, ) entity = HmSensor( device=self, @@ -690,15 +682,13 @@ def create_entity( ) if entity: entity.add_to_collections() - return entity -def create_devices(central: hm_central.CentralUnit) -> None: +async def create_devices(central: hm_central.CentralUnit) -> None: """ Trigger creation of the objects that expose the functionality. """ - new_devices = set[str]() - new_entities: list[BaseEntity] = [] + new_devices = set[HmDevice]() for interface_id, client in central.clients.items(): if not client: _LOGGER.debug( @@ -731,8 +721,7 @@ def create_devices(central: hm_central.CentralUnit) -> None: interface_id=interface_id, device_address=device_address, ) - new_devices.add(device_address) - central.hm_devices[device_address] = device + except Exception as err: _LOGGER.error( "create_devices: Exception (%s) Failed to create device: %s, %s", @@ -742,7 +731,16 @@ def create_devices(central: hm_central.CentralUnit) -> None: ) try: if device: - new_entities.extend(device.create_entities()) + device.create_entities_and_append_to_device() + if DATA_LOAD_FAIL == await device.load_paramset_cache(): + _LOGGER.debug( + "create_devices: Data load failed for %s, %s", + interface_id, + device_address, + ) + device.paramset_cache.init_device_entities() + new_devices.add(device) + central.hm_devices[device_address] = device except Exception as err: _LOGGER.error( "create_devices: Exception (%s) Failed to create entities: %s, %s", @@ -751,9 +749,98 @@ def create_devices(central: hm_central.CentralUnit) -> None: device_address, ) if callable(central.callback_system_event): - central.callback_system_event( - HH_EVENT_DEVICES_CREATED, new_devices, set(new_entities) - ) + central.callback_system_event(HH_EVENT_DEVICES_CREATED, new_devices) + + +class ParamsetCache: + """A Cache to temporaily stoere paramsets""" + + def __init__(self, device: HmDevice): + self._device = device + self._client = device.client + self._cache: dict[str, dict[str, Any]] = {} + self._last_update = INIT_DATETIME + + def get_value(self, channel_address: str, parameter: str) -> Any | None: + """Get Value from paramset cache.""" + if not self.is_initialized: + return None + if paramset := self._cache.get(channel_address): + return paramset.get(parameter) + return None + + @property + def is_initialized(self) -> bool: + """Return im cache is initialized""" + if not _updated_within_minutes(last_update=self._last_update): + #self._cache.clear() + self._last_update = INIT_DATETIME + return False + + return True + + def paramset_has_no_entry_for_parameter( + self, channel_address: str, parameter: str + ) -> bool | None: + """Check if paramset has no entry for parameter.""" + if paramset := self._cache.get(channel_address): + return paramset.get(parameter) is None + return None + + async def init(self) -> None: + """Load data""" + try: + for channel_address in self._get_channel_addresses(): + await self._load_channel_data(channel_address=channel_address) + self._last_update = datetime.now() + except BaseHomematicException as bhe: + _LOGGER.debug( + " load_data: Failed to init cache for %s, %s (%s)", + self._device.device_type, + self._device.device_address, + bhe, + ) + + def init_device_entities(self) -> None: + """Init the device entities with cached data.""" + for entity in self._device.entities.values(): + entity.init_entity_value() + + def _get_channel_addresses(self) -> set[str]: + """Get entities by channel address.""" + channel_addresses: list[str] = [] + for entity in self._device.entities.values(): + if entity.operations & OPERATION_READ: + channel_addresses.append(entity.channel_address) + return set(channel_addresses) + + async def _load_channel_data(self, channel_address: str) -> int: + """Load data""" + try: + paramset = await self._client.get_paramset( + channel_address=channel_address, paramset_key=PARAMSET_VALUES + ) + if len(paramset) > 0: + self._cache[channel_address] = paramset + return DATA_LOAD_SUCCESS + except BaseHomematicException as bhe: + _LOGGER.debug( + "_load_channel_data: Failed to get paramset for %s, %s: %s", + self._device.device_type, + channel_address, + bhe, + ) + return DATA_LOAD_FAIL + + +def _updated_within_minutes(last_update: datetime, minutes: int = 5) -> bool: + """Entity has been updated within X minutes.""" + if last_update == INIT_DATETIME: + return False + delta = datetime.now() - last_update + if delta.seconds < (minutes * 60): + return True + return False def _is_binary_sensor(parameter_data: dict[str, Any]) -> bool: diff --git a/hahomematic/entity.py b/hahomematic/entity.py index ed7bd090..b15ab2c9 100644 --- a/hahomematic/entity.py +++ b/hahomematic/entity.py @@ -28,9 +28,6 @@ ATTR_SUBTYPE, ATTR_TYPE, ATTR_VALUE, - DATA_LOAD_FAIL, - DATA_LOAD_SUCCESS, - DATA_NO_LOAD, EVENT_CONFIG_PENDING, EVENT_STICKY_UN_REACH, EVENT_UN_REACH, @@ -113,8 +110,11 @@ def _set_last_update(self) -> None: self.last_update = datetime.now() def _updated_within_minutes(self, minutes: int = 5) -> bool: + """Entity has been updated within X minutes.""" + if self.last_update == INIT_DATETIME: + return False delta = datetime.now() - self.last_update - if delta.seconds < minutes * 60: + if delta.seconds < (minutes * 60): return True return False @@ -300,6 +300,11 @@ def hmtype(self) -> str: """Return the homematic type.""" return self._type + @property + def operations(self) -> int: + """Return the operations mode of the entity.""" + return self._operations + @property def visible(self) -> bool: """Return the if entity is visible in ccu.""" @@ -390,6 +395,26 @@ def __init__( (self.channel_address, self.parameter) ].append(self.event) + def init_entity_value(self) -> None: + """Init the entity data.""" + if self._updated_within_minutes(): + return None + + self.set_value( + value=self._device.paramset_cache.get_value( + channel_address=self.channel_address, parameter=self.parameter + ) + ) + + if ( + self.usage != HmEntityUsage.ENTITY_NO_CREATE + and self.operations & OPERATION_READ + and self._device.paramset_cache.paramset_has_no_entry_for_parameter( + channel_address=self.channel_address, parameter=self.parameter + ) + ): + self.usage = HmEntityUsage.ENTITY_NO_PARAMSET_DATA + def event( self, interface_id: str, channel_address: str, parameter: str, raw_value: Any ) -> None: @@ -470,7 +495,7 @@ def value(self) -> ParameterType | None: """Return the value of the entity.""" return self._value - def set_value(self, value: ParameterType) -> None: + def set_value(self, value: Any) -> None: """Set value to the entity.""" converted_value = self._convert_value(value) if self._value != converted_value: @@ -485,35 +510,6 @@ def attributes(self) -> dict[str, Any]: state_attr[ATTR_ENTITY_TYPE] = HmEntityType.GENERIC.value return state_attr - async def init_entity_data(self) -> int: - """initial data load""" - await self._device.init_device_entities() - if not self.available: - return DATA_NO_LOAD - return await self.load_entity_data() - - async def load_entity_data(self) -> int: - """Load data""" - if self._updated_within_minutes(): - return DATA_NO_LOAD - - try: - if self._operations & OPERATION_READ: - return await self._device.load_channel_data( - channel_address=self.channel_address - ) - return DATA_NO_LOAD - except BaseHomematicException as bhe: - _LOGGER.debug( - " %s: Failed to get value for %s, %s, %s: %s", - self.platform, - self._device.device_type, - self.channel_address, - self.parameter, - bhe, - ) - return DATA_LOAD_FAIL - def remove_event_subscriptions(self) -> None: """Remove existing event subscriptions""" del self._central.entity_event_subscriptions[ @@ -604,6 +600,13 @@ async def put_paramset( rx_mode=rx_mode, ) + def init_entity_value(self) -> None: + """Init the entity values.""" + for entity in self.data_entities.values(): + if entity: + entity.init_entity_value() + self.update_entity() + def _init_entities(self) -> None: """init entity collection""" @@ -683,21 +686,6 @@ def _remove_entity(self, field_name: str, entity: GenericEntity | None) -> None: entity.unregister_update_callback(self.update_entity) del self.data_entities[field_name] - async def init_entity_data(self) -> int: - """Init entity data""" - await self._device.init_device_entities() - if self._updated_within_minutes(): - return DATA_NO_LOAD - if not self.available: - return DATA_NO_LOAD - - for entity in self.data_entities.values(): - if entity: - await entity.load_entity_data() - - self.update_entity() - return DATA_LOAD_SUCCESS - def _get_entity( self, field_name: str, entity_type: type[_EntityType] ) -> _EntityType: diff --git a/hahomematic/support.py b/hahomematic/support.py index b94af17c..ff48361e 100644 --- a/hahomematic/support.py +++ b/hahomematic/support.py @@ -46,7 +46,7 @@ async def export_data(self) -> None: ] = self._central.raw_devices.get_device_with_channels( interface_id=self._interface_id, device_address=self._device_address ) - paramset_descriptions: dict[str, Any] = await self._client.get_all_paramsets( + paramset_descriptions: dict[str, Any] = await self._client.get_all_paramset_descriptions( list(device_descriptions.values()) ) device_type = device_descriptions[self._device_address][ATTR_HM_TYPE] diff --git a/setup.py b/setup.py index 6fe7c536..f2516ea2 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ def readme(): }, PACKAGE_NAME = "hahomematic" HERE = os.path.abspath(os.path.dirname(__file__)) -VERSION = "0.27.2" +VERSION = "0.28.0" PACKAGES = find_packages(exclude=["tests", "tests.*", "dist", "build"]) diff --git a/tests/test_central.py b/tests/test_central.py index cd216ae0..72526a32 100644 --- a/tests/test_central.py +++ b/tests/test_central.py @@ -20,7 +20,7 @@ async def test_central(central, loop) -> None: assert central.get_client_by_interface_id("ccu-dev-hm").model == "PyDevCCU" assert central.get_client().model == "PyDevCCU" assert len(central.hm_devices) == 342 - assert len(central.hm_entities) == 4025 + assert len(central.hm_entities) == 4213 data = {} for device in central.hm_devices.values(): @@ -61,7 +61,7 @@ async def test_central(central, loop) -> None: assert len(ce_channels) == 87 assert len(entity_types) == 6 - assert len(parameters) == 174 + assert len(parameters) == 175 @pytest.mark.asyncio