diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07efa477..e066ebbd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.6.7 + rev: v0.6.8 hooks: - id: ruff args: diff --git a/hahomematic/client/__init__.py b/hahomematic/client/__init__.py index 29f5e3cf..846760fc 100644 --- a/hahomematic/client/__init__.py +++ b/hahomematic/client/__init__.py @@ -40,8 +40,8 @@ ) from hahomematic.exceptions import BaseHomematicException, ClientException, NoConnection from hahomematic.performance import measure_execution_time +from hahomematic.platforms.decorators import service from hahomematic.platforms.device import HmDevice -from hahomematic.platforms.entity import service from hahomematic.platforms.support import convert_value from hahomematic.support import ( build_headers, @@ -435,7 +435,7 @@ async def get_link_peers(self, address: str) -> tuple[str, ...] | None: f"GET_LINK_PEERS failed with for: {address}: {reduce_args(args=ex.args)}" ) from ex - @service(level=logging.DEBUG) + @service(level=logging.NOTSET) async def get_value( self, channel_address: str, diff --git a/hahomematic/performance.py b/hahomematic/performance.py index f5e15a6f..cb867f87 100644 --- a/hahomematic/performance.py +++ b/hahomematic/performance.py @@ -18,7 +18,7 @@ def measure_execution_time[_CallableT: Callable[..., Any]](func: _CallableT) -> is_enabled = _LOGGER.isEnabledFor(level=logging.DEBUG) @wraps(func) - async def async_wrapper(*args: Any, **kwargs: Any) -> Any: + async def async_measure_wrapper(*args: Any, **kwargs: Any) -> Any: """Wrap method.""" if is_enabled: start = datetime.now() @@ -36,7 +36,7 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> Any: ) @wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Any: + def measure_wrapper(*args: Any, **kwargs: Any) -> Any: """Wrap method.""" if is_enabled: start = datetime.now() @@ -54,5 +54,5 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: ) if asyncio.iscoroutinefunction(func): - return async_wrapper # type: ignore[return-value] - return wrapper # type: ignore[return-value] + return async_measure_wrapper # type: ignore[return-value] + return measure_wrapper # type: ignore[return-value] diff --git a/hahomematic/platforms/custom/climate.py b/hahomematic/platforms/custom/climate.py index 44d642f5..c566b1d0 100644 --- a/hahomematic/platforms/custom/climate.py +++ b/hahomematic/platforms/custom/climate.py @@ -18,8 +18,8 @@ from hahomematic.platforms.custom.const import DeviceProfile, Field from hahomematic.platforms.custom.entity import CustomEntity from hahomematic.platforms.custom.support import CustomConfig -from hahomematic.platforms.decorators import config_property, state_property -from hahomematic.platforms.entity import CallParameterCollector, bind_collector, service +from hahomematic.platforms.decorators import config_property, service, state_property +from hahomematic.platforms.entity import CallParameterCollector, bind_collector from hahomematic.platforms.generic.action import HmAction from hahomematic.platforms.generic.binary_sensor import HmBinarySensor from hahomematic.platforms.generic.number import HmFloat, HmInteger diff --git a/hahomematic/platforms/custom/entity.py b/hahomematic/platforms/custom/entity.py index 2aefe77e..96f3f31f 100644 --- a/hahomematic/platforms/custom/entity.py +++ b/hahomematic/platforms/custom/entity.py @@ -12,8 +12,8 @@ from hahomematic.platforms.custom import definition as hmed from hahomematic.platforms.custom.const import ED, DeviceProfile, Field from hahomematic.platforms.custom.support import CustomConfig -from hahomematic.platforms.decorators import state_property -from hahomematic.platforms.entity import BaseEntity, CallParameterCollector, get_service_calls +from hahomematic.platforms.decorators import get_service_calls, state_property +from hahomematic.platforms.entity import BaseEntity, CallParameterCollector from hahomematic.platforms.generic import entity as hmge from hahomematic.platforms.support import ( EntityNameData, diff --git a/hahomematic/platforms/decorators.py b/hahomematic/platforms/decorators.py index 0639f72d..70fed818 100644 --- a/hahomematic/platforms/decorators.py +++ b/hahomematic/platforms/decorators.py @@ -2,10 +2,18 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Awaitable, Callable from datetime import datetime from enum import Enum -from typing import Any +from functools import wraps +import logging +from typing import Any, ParamSpec, TypeVar + +from hahomematic.exceptions import BaseHomematicException +from hahomematic.support import reduce_args + +P = ParamSpec("P") +T = TypeVar("T") # pylint: disable=invalid-name @@ -119,3 +127,38 @@ def get_public_attributes_for_state_property(data_object: Any) -> dict[str, Any] return get_public_attributes_by_class_decorator( data_object=data_object, class_decorator=state_property ) + + +def service(level: int = logging.ERROR) -> Callable: + """Mark function as service call and log exceptions.""" + + def service_decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]: + """Decorate service.""" + + @wraps(func) + async def service_wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + """Wrap service to log exception.""" + try: + return await func(*args, **kwargs) + except BaseHomematicException as bhe: + if level > logging.NOTSET: + logging.getLogger(args[0].__module__).log( + level=level, msg=reduce_args(args=bhe.args) + ) + raise + + setattr(service_wrapper, "ha_service", True) + return service_wrapper + + return service_decorator + + +def get_service_calls(obj: object) -> dict[str, Callable]: + """Get all methods decorated with the "bind_collector" or "service_call" decorator.""" + return { + name: getattr(obj, name) + for name in dir(obj) + if not name.startswith("_") + and callable(getattr(obj, name)) + and hasattr(getattr(obj, name), "ha_service") + } diff --git a/hahomematic/platforms/entity.py b/hahomematic/platforms/entity.py index 69583081..cda30889 100644 --- a/hahomematic/platforms/entity.py +++ b/hahomematic/platforms/entity.py @@ -3,12 +3,12 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Awaitable, Callable, Mapping +from collections.abc import Callable, Mapping from datetime import datetime from functools import partial, wraps from inspect import getfullargspec import logging -from typing import Any, Final, ParamSpec, TypeVar, cast +from typing import Any, Final, cast import voluptuous as vol @@ -41,7 +41,7 @@ ) from hahomematic.exceptions import BaseHomematicException, HaHomematicException from hahomematic.platforms import device as hmd -from hahomematic.platforms.decorators import config_property, state_property +from hahomematic.platforms.decorators import config_property, get_service_calls, state_property from hahomematic.platforms.support import ( EntityNameData, GenericParameterType, @@ -750,7 +750,9 @@ def __init__(self, client: hmcl.Client) -> None: """Init the generator.""" self._client: Final = client self._central: Final = client.central - self._paramsets: Final[dict[ParamsetKey, dict[int, dict[str, dict[str, Any]]]]] = {} + self._paramsets: Final[ + dict[ParamsetKey, dict[int, dict[str, dict[str, Any]]]] + ] = {} # {"VALUES": {50: {"00021BE9957782:3": {"STATE3": True}}}} def add_entity( self, @@ -816,26 +818,31 @@ def bind_collector( use_command_queue: bool = False, use_put_paramset: bool = True, enabled: bool = True, + log_level: int = logging.ERROR, ) -> Callable: - """Decorate function to automatically add collector if not set.""" + """ + Decorate function to automatically add collector if not set. - def decorator[_CallableT: Callable[..., Any]](func: _CallableT) -> _CallableT: + Additionally, thrown exceptions are logged. + """ + + def bind_decorator[_CallableT: Callable[..., Any]](func: _CallableT) -> _CallableT: """Decorate function to automatically add collector if not set.""" argument_index = getfullargspec(func).args.index(_COLLECTOR_ARGUMENT_NAME) @wraps(func) - async def wrapper(*args: Any, **kwargs: Any) -> Any: + async def bind_wrapper(*args: Any, **kwargs: Any) -> Any: """Wrap method to add collector.""" - if not enabled: - return await func(*args, **kwargs) try: - collector_exists = args[argument_index] is not None - except IndexError: - collector_exists = kwargs.get(_COLLECTOR_ARGUMENT_NAME) is not None - - if collector_exists: - return_value = await func(*args, **kwargs) - else: + if not enabled: + return await func(*args, **kwargs) + try: + collector_exists = args[argument_index] is not None + except IndexError: + collector_exists = kwargs.get(_COLLECTOR_ARGUMENT_NAME) is not None + + if collector_exists: + return await func(*args, **kwargs) collector = CallParameterCollector(client=args[0].channel.device.client) kwargs[_COLLECTOR_ARGUMENT_NAME] = collector return_value = await func(*args, **kwargs) @@ -844,48 +851,15 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any: use_command_queue=use_command_queue, use_put_paramset=use_put_paramset, ) - return return_value - - setattr(func, "ha_service", True) - return wrapper # type: ignore[return-value] - - return decorator - - -P = ParamSpec("P") -T = TypeVar("T") - - -def service(level: int = logging.ERROR) -> Callable: - """Mark function as service call and log exceptions.""" - - def decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]: - """Decorate service.""" - - @wraps(func) - async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - """Wrap service to log exception.""" - try: - return await func(*args, **kwargs) + return return_value # noqa:TRY300 except BaseHomematicException as bhe: - if level > logging.NOTSET: + if log_level > logging.NOTSET: logging.getLogger(args[0].__module__).log( - level=level, msg=reduce_args(args=bhe.args) + level=log_level, msg=reduce_args(args=bhe.args) ) - raise - - setattr(wrapper, "ha_service", True) - return wrapper - - return decorator + return None + setattr(bind_wrapper, "ha_service", True) + return bind_wrapper # type: ignore[return-value] -def get_service_calls(obj: object) -> dict[str, Callable]: - """Get all methods decorated with the "bind_collector" or "service_call" decorator.""" - return { - name: getattr(obj, name) - for name in dir(obj) - if not name.startswith("_") - and callable(getattr(obj, name)) - and hasattr(getattr(obj, name), "ha_service") - } + return bind_decorator diff --git a/hahomematic/platforms/generic/button.py b/hahomematic/platforms/generic/button.py index b56f52a5..db83d26b 100644 --- a/hahomematic/platforms/generic/button.py +++ b/hahomematic/platforms/generic/button.py @@ -7,7 +7,7 @@ from __future__ import annotations from hahomematic.const import HmPlatform -from hahomematic.platforms.entity import service +from hahomematic.platforms.decorators import service from hahomematic.platforms.generic.entity import GenericEntity diff --git a/hahomematic/platforms/generic/entity.py b/hahomematic/platforms/generic/entity.py index 1564d8b2..0d1e7c37 100644 --- a/hahomematic/platforms/generic/entity.py +++ b/hahomematic/platforms/generic/entity.py @@ -14,7 +14,6 @@ ParamsetKey, ) from hahomematic.platforms import device as hmd, entity as hme -from hahomematic.platforms.entity import service from hahomematic.platforms.support import ( EntityNameData, GenericParameterType, @@ -90,7 +89,6 @@ async def event(self, value: Any) -> None: event_data=self.get_event_data(new_value), ) - @service() async def send_value( self, value: InputParameterT, diff --git a/hahomematic/platforms/generic/switch.py b/hahomematic/platforms/generic/switch.py index 974d4cc6..b0c57248 100644 --- a/hahomematic/platforms/generic/switch.py +++ b/hahomematic/platforms/generic/switch.py @@ -9,8 +9,8 @@ from typing import Final from hahomematic.const import HmPlatform, ParameterType -from hahomematic.platforms.decorators import state_property -from hahomematic.platforms.entity import CallParameterCollector, service +from hahomematic.platforms.decorators import service, state_property +from hahomematic.platforms.entity import CallParameterCollector from hahomematic.platforms.generic.entity import GenericEntity _PARAM_ON_TIME: Final = "ON_TIME" diff --git a/hahomematic/platforms/hub/button.py b/hahomematic/platforms/hub/button.py index d508c1d4..3d0d11ac 100644 --- a/hahomematic/platforms/hub/button.py +++ b/hahomematic/platforms/hub/button.py @@ -10,8 +10,7 @@ from hahomematic import central as hmcu from hahomematic.const import PROGRAM_ADDRESS, HmPlatform, HubData, ProgramData -from hahomematic.platforms.decorators import state_property -from hahomematic.platforms.entity import get_service_calls, service +from hahomematic.platforms.decorators import get_service_calls, service, state_property from hahomematic.platforms.hub.entity import GenericHubEntity diff --git a/hahomematic/platforms/hub/entity.py b/hahomematic/platforms/hub/entity.py index 5f46cce2..a3e3565b 100644 --- a/hahomematic/platforms/hub/entity.py +++ b/hahomematic/platforms/hub/entity.py @@ -9,8 +9,13 @@ from hahomematic import central as hmcu from hahomematic.const import HUB_PATH, SYSVAR_ADDRESS, HubData, SystemVariableData -from hahomematic.platforms.decorators import config_property, state_property -from hahomematic.platforms.entity import CallbackEntity, get_service_calls, service +from hahomematic.platforms.decorators import ( + config_property, + get_service_calls, + service, + state_property, +) +from hahomematic.platforms.entity import CallbackEntity from hahomematic.platforms.support import PayloadMixin, generate_unique_id from hahomematic.support import parse_sys_var diff --git a/hahomematic/platforms/hub/number.py b/hahomematic/platforms/hub/number.py index 4a3c67e5..3e36ed7c 100644 --- a/hahomematic/platforms/hub/number.py +++ b/hahomematic/platforms/hub/number.py @@ -10,7 +10,7 @@ from typing import Final from hahomematic.const import HmPlatform -from hahomematic.platforms.entity import service +from hahomematic.platforms.decorators import service from hahomematic.platforms.hub.entity import GenericSystemVariable _LOGGER: Final = logging.getLogger(__name__) diff --git a/hahomematic/platforms/hub/select.py b/hahomematic/platforms/hub/select.py index a88fb4a5..9f6a1b43 100644 --- a/hahomematic/platforms/hub/select.py +++ b/hahomematic/platforms/hub/select.py @@ -10,8 +10,7 @@ from typing import Final from hahomematic.const import HmPlatform -from hahomematic.platforms.decorators import state_property -from hahomematic.platforms.entity import service +from hahomematic.platforms.decorators import service, state_property from hahomematic.platforms.hub.entity import GenericSystemVariable from hahomematic.platforms.support import get_value_from_value_list diff --git a/hahomematic/platforms/update.py b/hahomematic/platforms/update.py index c826cd98..cb534aed 100644 --- a/hahomematic/platforms/update.py +++ b/hahomematic/platforms/update.py @@ -20,8 +20,8 @@ ) from hahomematic.exceptions import HaHomematicException from hahomematic.platforms import device as hmd -from hahomematic.platforms.decorators import config_property, state_property -from hahomematic.platforms.entity import CallbackEntity, get_service_calls +from hahomematic.platforms.decorators import config_property, get_service_calls, state_property +from hahomematic.platforms.entity import CallbackEntity from hahomematic.platforms.support import PayloadMixin, generate_unique_id diff --git a/requirements.txt b/requirements.txt index e36b1e13..e87b26f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aiohttp>=3.10.6 +aiohttp>=3.10.8 orjson>=3.10.7 python-slugify>=8.0.4 voluptuous>=0.15.2 diff --git a/requirements_test.txt b/requirements_test.txt index 57ab31ad..8cc504c5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -20,4 +20,4 @@ pytest-socket==0.7.0 pytest-timeout==2.3.1 pytest==8.3.3 types-python-slugify==8.0.2.20240310 -uv==0.4.16 +uv==0.4.17 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 701a87c2..9c458171 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,4 +1,4 @@ bandit==1.7.10 codespell==2.3.0 -ruff==0.6.7 +ruff==0.6.8 yamllint==1.35.1