Skip to content

Commit

Permalink
Return affected entity keys for service calls (#1517)
Browse files Browse the repository at this point in the history
* Use ENTITY_KEY type alias

* Return entity_key from command cache calls

* Add rv to set_value and put_paramset

* Update dynamic.py

* Update entity.py

* Use more entity_key

* Reduce entity_key to minimum

* Add get_custom_ids_by_entity_keys

* Add has_entity_key

* Return affected entity keys for service calls
  • Loading branch information
SukramJ authored Apr 19, 2024
1 parent c344fd0 commit cf886f5
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 115 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Version 2024.4.9 (2024-04-16)

- Decompose combined parameter
- Return affected entity keys for service calls

# Version 2024.4.8 (2024-04-13)

Expand Down
84 changes: 29 additions & 55 deletions hahomematic/caches/dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
PING_PONG_MISMATCH_COUNT_TTL,
)
from hahomematic.const import (
ENTITY_KEY,
EVENT_DATA,
EVENT_INSTANCE_NAME,
EVENT_INTERFACE_ID,
Expand All @@ -30,7 +31,7 @@
)
from hahomematic.converter import CONVERTABLE_PARAMETERS, convert_combined_parameter_to_paramset
from hahomematic.platforms.device import HmDevice
from hahomematic.support import changed_within_seconds, get_channel_no, get_device_address
from hahomematic.support import changed_within_seconds, get_entity_key

_LOGGER: Final = logging.getLogger(__name__)

Expand All @@ -42,109 +43,82 @@ def __init__(self, interface_id: str) -> None:
"""Init command cache."""
self._interface_id: Final = interface_id
# (paramset_key, device_address, channel_no, parameter)
self._last_send_command: Final[
dict[tuple[str, str, int | None, str], tuple[Any, datetime]]
] = {}
self._last_send_command: Final[dict[ENTITY_KEY, tuple[Any, datetime]]] = {}

def add_set_value(
self,
channel_address: str,
parameter: str,
value: Any,
) -> None:
) -> set[ENTITY_KEY]:
"""Add data from set value command."""
if parameter in CONVERTABLE_PARAMETERS:
self.add_combined_parameter(
return self.add_combined_parameter(
parameter=parameter, channel_address=channel_address, combined_parameter=value
)
return

key = (
ParamsetKey.VALUES.value,
get_device_address(channel_address),
get_channel_no(channel_address),
parameter,
entity_key = get_entity_key(
channel_address=channel_address,
parameter=parameter,
)
self._last_send_command[key] = (value, datetime.now())
self._last_send_command[entity_key] = (value, datetime.now())
return {entity_key}

def add_put_paramset(
self,
channel_address: str,
paramset_key: str,
values: dict[str, Any],
) -> None:
self, channel_address: str, paramset_key: str, values: dict[str, Any]
) -> set[ENTITY_KEY]:
"""Add data from put paramset command."""

entity_keys: set[ENTITY_KEY] = set()
for parameter, value in values.items():
key = (
paramset_key,
get_device_address(channel_address),
get_channel_no(channel_address),
parameter,
entity_key = get_entity_key(
channel_address=channel_address,
parameter=parameter,
)
self._last_send_command[key] = (value, datetime.now())
self._last_send_command[entity_key] = (value, datetime.now())
entity_keys.add(entity_key)
return entity_keys

def add_combined_parameter(
self, parameter: str, channel_address: str, combined_parameter: str
) -> None:
) -> set[ENTITY_KEY]:
"""Add data from combined parameter."""
if values := convert_combined_parameter_to_paramset(
parameter=parameter, cpv=combined_parameter
):
self.add_put_paramset(
return self.add_put_paramset(
channel_address=channel_address,
paramset_key=ParamsetKey.VALUES.value,
paramset_key=ParamsetKey.VALUES,
values=values,
)
return set()

def get_last_value_send(
self,
paramset_key: str,
channel_address: str,
parameter: str,
max_age: int = LAST_COMMAND_SEND_STORE_TIMEOUT,
self, entity_key: ENTITY_KEY, max_age: int = LAST_COMMAND_SEND_STORE_TIMEOUT
) -> Any:
"""Return the last send values."""
key = (
paramset_key,
get_device_address(channel_address),
get_channel_no(channel_address),
parameter,
)
if result := self._last_send_command.get(key):
if result := self._last_send_command.get(entity_key):
value, last_send_dt = result
if last_send_dt and changed_within_seconds(last_change=last_send_dt, max_age=max_age):
return value
self.remove_last_value_send(
paramset_key=paramset_key,
channel_address=channel_address,
parameter=parameter,
entity_key=entity_key,
max_age=max_age,
)
return None

def remove_last_value_send(
self,
paramset_key: str,
channel_address: str,
parameter: str,
entity_key: ENTITY_KEY,
value: Any = None,
max_age: int = LAST_COMMAND_SEND_STORE_TIMEOUT,
) -> None:
"""Remove the last send value."""
key = (
paramset_key,
get_device_address(channel_address),
get_channel_no(channel_address),
parameter,
)

if result := self._last_send_command.get(key):
if result := self._last_send_command.get(entity_key):
stored_value, last_send_dt = result
if not changed_within_seconds(last_change=last_send_dt, max_age=max_age) or (
value is not None and stored_value == value
):
del self._last_send_command[key]
del self._last_send_command[entity_key]


class DeviceDetailsCache:
Expand Down
57 changes: 39 additions & 18 deletions hahomematic/central/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
DEFAULT_TLS,
DEFAULT_VERIFY_TLS,
ENTITY_EVENTS,
ENTITY_KEY,
EVENT_AVAILABLE,
EVENT_DATA,
EVENT_INTERFACE_ID,
Expand Down Expand Up @@ -60,13 +61,13 @@
from hahomematic.platforms import create_entities_and_append_to_device
from hahomematic.platforms.custom.entity import CustomEntity
from hahomematic.platforms.device import HmDevice
from hahomematic.platforms.entity import BaseEntity, CallbackEntity
from hahomematic.platforms.entity import BaseParameterEntity, CallbackEntity
from hahomematic.platforms.event import GenericEvent
from hahomematic.platforms.generic.entity import GenericEntity
from hahomematic.platforms.hub import Hub
from hahomematic.platforms.hub.button import HmProgramButton
from hahomematic.platforms.hub.entity import GenericHubEntity, GenericSystemVariable
from hahomematic.support import check_config, get_device_address, reduce_args
from hahomematic.support import check_config, get_device_address, get_entity_key, reduce_args

_LOGGER: Final = logging.getLogger(__name__)

Expand Down Expand Up @@ -130,7 +131,7 @@ def __init__(self, central_config: CentralConfig) -> None:
self._clients: Final[dict[str, hmcl.Client]] = {}
# {{channel_address, parameter}, event_handle}
self._entity_event_subscriptions: Final[
dict[tuple[str, str], list[Callable[[Any], Coroutine[Any, Any, None]]]]
dict[ENTITY_KEY, list[Callable[[Any], Coroutine[Any, Any, None]]]]
] = {}
# {device_address, device}
self._devices: Final[dict[str, HmDevice]] = {}
Expand Down Expand Up @@ -615,6 +616,23 @@ def get_readable_generic_entities(
)
)

def get_custom_ids_by_entity_keys(self, entity_keys: set[ENTITY_KEY]) -> set[str]:
"""Return the custom ids by entity keys."""
return {
entity.custom_id
for entity in self.get_entities(exclude_no_create=False, registered=True)
if (
(
(isinstance(entity, BaseParameterEntity) and entity.entity_key in entity_keys)
or (
isinstance(entity, CustomEntity)
and entity.has_entity_key(entity_keys=entity_keys)
)
)
and entity.custom_id is not None
)
}

def _get_primary_client(self) -> hmcl.Client | None:
"""Return the client by interface_id or the first with a virtual remote."""
client: hmcl.Client | None = None
Expand Down Expand Up @@ -850,9 +868,15 @@ async def event(
pong_ts=datetime.strptime(v_timestamp, DATETIME_FORMAT_MILLIS)
)
return
if (channel_address, parameter) in self._entity_event_subscriptions:

entity_key = get_entity_key(
channel_address=channel_address,
parameter=parameter,
)

if entity_key in self._entity_event_subscriptions:
try:
for callback in self._entity_event_subscriptions[(channel_address, parameter)]:
for callback in self._entity_event_subscriptions[entity_key]:
await callback(value)
except RuntimeError as rte: # pragma: no cover
_LOGGER.debug(
Expand Down Expand Up @@ -880,17 +904,12 @@ def list_devices(self, interface_id: str) -> list[dict[str, Any]]:
)
return result

def add_event_subscription(self, entity: BaseEntity) -> None:
def add_event_subscription(self, entity: BaseParameterEntity) -> None:
"""Add entity to central event subscription."""
if isinstance(entity, (GenericEntity, GenericEvent)) and entity.supports_events:
if (
entity.channel_address,
entity.parameter,
) not in self._entity_event_subscriptions:
self._entity_event_subscriptions[(entity.channel_address, entity.parameter)] = []
self._entity_event_subscriptions[(entity.channel_address, entity.parameter)].append(
entity.event
)
if entity.entity_key not in self._entity_event_subscriptions:
self._entity_event_subscriptions[entity.entity_key] = []
self._entity_event_subscriptions[entity.entity_key].append(entity.event)

async def remove_device(self, device: HmDevice) -> None:
"""Remove device to central collections."""
Expand All @@ -907,14 +926,14 @@ async def remove_device(self, device: HmDevice) -> None:
self.device_details.remove_device(device=device)
del self._devices[device.device_address]

def remove_event_subscription(self, entity: BaseEntity) -> None:
def remove_event_subscription(self, entity: BaseParameterEntity) -> None:
"""Remove event subscription from central collections."""
if (
isinstance(entity, (GenericEntity, GenericEvent))
and entity.supports_events
and (entity.channel_address, entity.parameter) in self._entity_event_subscriptions
and entity.entity_key in self._entity_event_subscriptions
):
del self._entity_event_subscriptions[(entity.channel_address, entity.parameter)]
del self._entity_event_subscriptions[entity.entity_key]

async def execute_program(self, pid: str) -> bool:
"""Execute a program on CCU / Homegear."""
Expand Down Expand Up @@ -978,7 +997,9 @@ def _get_virtual_remote(self, device_address: str) -> HmDevice | None:
return virtual_remote
return None

def get_generic_entity(self, channel_address: str, parameter: str) -> GenericEntity | None:
def get_generic_entity(
self, channel_address: str, parameter: str, paramset_key: ParamsetKey = ParamsetKey.VALUES
) -> GenericEntity | None:
"""Get entity by channel_address and parameter."""
if device := self.get_device(address=channel_address):
return device.get_generic_entity(channel_address=channel_address, parameter=parameter)
Expand Down
19 changes: 10 additions & 9 deletions hahomematic/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from hahomematic.config import CALLBACK_WARN_INTERVAL, RECONNECT_WAIT
from hahomematic.const import (
DATETIME_FORMAT_MILLIS,
ENTITY_KEY,
EVENT_AVAILABLE,
EVENT_SECONDS_SINCE_LAST_EVENT,
HOMEGEAR_SERIAL,
Expand Down Expand Up @@ -422,15 +423,16 @@ async def _set_value(
parameter: str,
value: Any,
rx_mode: str | None = None,
) -> bool:
) -> set[ENTITY_KEY]:
"""Set single value on paramset VALUES."""
try:
_LOGGER.debug("SET_VALUE: %s, %s, %s", channel_address, parameter, value)
if rx_mode:
await self._proxy.setValue(channel_address, parameter, value, rx_mode)
else:
await self._proxy.setValue(channel_address, parameter, value)
self._last_value_send_cache.add_set_value(
# store the send value in the last_value_send_cache
return self._last_value_send_cache.add_set_value(
channel_address=channel_address, parameter=parameter, value=value
)
except BaseHomematicException as ex:
Expand All @@ -442,8 +444,7 @@ async def _set_value(
parameter,
value,
)
return False
return True
return set()

async def set_value(
self,
Expand All @@ -452,7 +453,7 @@ async def set_value(
parameter: str,
value: Any,
rx_mode: str | None = None,
) -> bool:
) -> set[ENTITY_KEY]:
"""Set single value on paramset VALUES."""
if paramset_key == ParamsetKey.VALUES:
return await self._set_value(
Expand Down Expand Up @@ -499,7 +500,7 @@ async def put_paramset(
paramset_key: str,
values: dict[str, Any],
rx_mode: str | None = None,
) -> bool:
) -> set[ENTITY_KEY]:
"""
Set paramsets manually.
Expand All @@ -512,7 +513,8 @@ async def put_paramset(
await self._proxy.putParamset(channel_address, paramset_key, values, rx_mode)
else:
await self._proxy.putParamset(channel_address, paramset_key, values)
self._last_value_send_cache.add_put_paramset(
# store the send value in the last_value_send_cache
return self._last_value_send_cache.add_put_paramset(
channel_address=channel_address, paramset_key=paramset_key, values=values
)
except BaseHomematicException as ex:
Expand All @@ -524,8 +526,7 @@ async def put_paramset(
paramset_key,
values,
)
return False
return True
return set()

async def fetch_paramset_description(
self, channel_address: str, paramset_key: str, save_to_file: bool = True
Expand Down
3 changes: 3 additions & 0 deletions hahomematic/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@

NO_CACHE_ENTRY: Final = "NO_CACHE_ENTRY"

# channel_address, parameter
ENTITY_KEY = tuple[str, str]


class Backend(StrEnum):
"""Enum with supported hahomematic backends."""
Expand Down
9 changes: 8 additions & 1 deletion hahomematic/platforms/custom/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import logging
from typing import Any, Final, TypeVar, cast

from hahomematic.const import INIT_DATETIME, CallSource, EntityUsage
from hahomematic.const import ENTITY_KEY, INIT_DATETIME, CallSource, EntityUsage
from hahomematic.platforms import device as hmd
from hahomematic.platforms.custom import definition as hmed
from hahomematic.platforms.custom.const import DeviceProfile, Field
Expand Down Expand Up @@ -298,6 +298,13 @@ def _get_entity(self, field: Field, entity_type: type[_EntityT]) -> _EntityT:
NoneTypeEntity(),
)

def has_entity_key(self, entity_keys: set[ENTITY_KEY]) -> bool:
"""Return if an entity with one of the entities is part of this entity."""
result = [
entity for entity in self._data_entities.values() if entity.entity_key in entity_keys
]
return len(result) > 0


class NoneTypeEntity:
"""Entity to return an empty value."""
Expand Down
Loading

0 comments on commit cf886f5

Please sign in to comment.