diff --git a/changelog.md b/changelog.md index 97142400..9aa20ca7 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,14 @@ +# Version 2024.11.8 (2024-11-21) + +- Add missing @service annotations +- Add performance measurement to @service +- Don't re-raise exception on internal services +- Move @service +- Remove @service from abstract methods + # Version 2024.11.7 (2024-11-19) - Set state_uncertain on value write -- # Version 2024.11.6 (2024-11-19) diff --git a/hahomematic/caches/dynamic.py b/hahomematic/caches/dynamic.py index 55415287..1bac26f3 100644 --- a/hahomematic/caches/dynamic.py +++ b/hahomematic/caches/dynamic.py @@ -185,7 +185,7 @@ def get_address_id(self, address: str) -> str: async def _get_all_rooms(self) -> dict[str, set[str]]: """Get all rooms, if available.""" if client := self._central.primary_client: - return await client.get_all_rooms() # type: ignore[no-any-return] + return await client.get_all_rooms() return {} def get_device_rooms(self, device_address: str) -> set[str]: @@ -203,7 +203,7 @@ def get_channel_rooms(self, channel_address: str) -> set[str]: async def _get_all_functions(self) -> dict[str, set[str]]: """Get all functions, if available.""" if client := self._central.primary_client: - return await client.get_all_functions() # type: ignore[no-any-return] + return await client.get_all_functions() return {} def get_function_text(self, address: str) -> str | None: diff --git a/hahomematic/caches/persistent.py b/hahomematic/caches/persistent.py index dd454979..8ccfd79d 100644 --- a/hahomematic/caches/persistent.py +++ b/hahomematic/caches/persistent.py @@ -16,10 +16,10 @@ from hahomematic import central as hmcu from hahomematic.const import ( CACHE_PATH, - DEFAULT_ENCODING, FILE_DEVICES, FILE_PARAMSETS, INIT_DATETIME, + UTF8, DataOperationResult, DeviceDescription, ParameterData, @@ -106,7 +106,7 @@ async def load(self) -> DataOperationResult: def _load() -> DataOperationResult: with open( file=os.path.join(self._cache_dir, self._filename), - encoding=DEFAULT_ENCODING, + encoding=UTF8, ) as fptr: data = orjson.loads(fptr.read()) if (converted_hash := hash_sha256(value=data)) == self.last_hash_saved: diff --git a/hahomematic/caches/visibility.py b/hahomematic/caches/visibility.py index ea9c8b39..5a502f0b 100644 --- a/hahomematic/caches/visibility.py +++ b/hahomematic/caches/visibility.py @@ -9,13 +9,7 @@ from typing import Any, Final from hahomematic import central as hmcu, support as hms -from hahomematic.const import ( - CLICK_EVENTS, - DEFAULT_ENCODING, - UN_IGNORE_WILDCARD, - Parameter, - ParamsetKey, -) +from hahomematic.const import CLICK_EVENTS, UN_IGNORE_WILDCARD, UTF8, Parameter, ParamsetKey from hahomematic.model.custom import get_required_parameters from hahomematic.support import element_matches_key, reduce_args @@ -713,7 +707,7 @@ def _load() -> None: self._storage_folder, _FILE_CUSTOM_UN_IGNORE_PARAMETERS, ), - encoding=DEFAULT_ENCODING, + encoding=UTF8, ) as fptr: for file_line in fptr.readlines(): if "#" not in file_line: diff --git a/hahomematic/central/__init__.py b/hahomematic/central/__init__.py index 35319bd9..008a399a 100644 --- a/hahomematic/central/__init__.py +++ b/hahomematic/central/__init__.py @@ -65,6 +65,7 @@ ProxyInitState, SystemInformation, ) +from hahomematic.decorators import service from hahomematic.exceptions import ( BaseHomematicException, HaHomematicConfigException, @@ -75,13 +76,12 @@ from hahomematic.model import create_data_points_and_events from hahomematic.model.custom import CustomDataPoint, create_custom_data_points from hahomematic.model.data_point import BaseParameterDataPoint, CallbackDataPoint -from hahomematic.model.decorators import info_property, service +from hahomematic.model.decorators import info_property from hahomematic.model.device import Device from hahomematic.model.event import GenericEvent from hahomematic.model.generic import GenericDataPoint from hahomematic.model.hub import GenericHubDataPoint, GenericSysvarDataPoint, Hub, ProgramDpButton from hahomematic.model.support import PayloadMixin -from hahomematic.performance import measure_execution_time from hahomematic.support import ( check_config, get_channel_no, @@ -917,7 +917,7 @@ async def add_new_devices( interface_id=interface_id, device_descriptions=device_descriptions ) - @measure_execution_time + @service(measure_performance=True) async def _add_new_devices( self, interface_id: str, device_descriptions: tuple[DeviceDescription, ...] ) -> None: @@ -1163,18 +1163,20 @@ def set_last_event_dt(self, interface_id: str) -> None: async def execute_program(self, pid: str) -> bool: """Execute a program on CCU / Homegear.""" if client := self.primary_client: - return await client.execute_program(pid=pid) # type: ignore[no-any-return] + return await client.execute_program(pid=pid) return False + @service(re_raise=False) async def fetch_sysvar_data(self, scheduled: bool) -> None: """Fetch sysvar data for the hub.""" await self._hub.fetch_sysvar_data(scheduled=scheduled) + @service(re_raise=False) async def fetch_program_data(self, scheduled: bool) -> None: """Fetch program data for the hub.""" await self._hub.fetch_program_data(scheduled=scheduled) - @measure_execution_time + @service(measure_performance=True) async def load_and_refresh_data_point_data( self, interface: Interface, diff --git a/hahomematic/client/__init__.py b/hahomematic/client/__init__.py index 04ed31b8..23713c54 100644 --- a/hahomematic/client/__init__.py +++ b/hahomematic/client/__init__.py @@ -39,11 +39,10 @@ SystemInformation, SystemVariableData, ) +from hahomematic.decorators import measure_execution_time, service from hahomematic.exceptions import BaseHomematicException, ClientException, NoConnectionException -from hahomematic.model.decorators import service from hahomematic.model.device import Device from hahomematic.model.support import convert_value -from hahomematic.performance import measure_execution_time from hahomematic.support import ( build_headers, build_xml_rpc_uri, @@ -294,15 +293,14 @@ async def stop(self) -> None: await self._proxy_read.stop() @abstractmethod - @service() async def fetch_all_device_data(self) -> None: """Fetch all device data from CCU.""" @abstractmethod - @service() async def fetch_device_details(self) -> None: """Fetch names from backend.""" + @service(re_raise=False, no_raise_return=False) async def is_connected(self) -> bool: """ Perform actions required for connectivity check. @@ -364,44 +362,36 @@ async def check_connection_availability(self, handle_ping_pong: bool) -> bool: """Send ping to CCU to generate PONG event.""" @abstractmethod - @service() async def execute_program(self, pid: str) -> bool: """Execute a program on CCU / Homegear..""" @abstractmethod - @service() async def set_system_variable(self, name: str, value: Any) -> bool: """Set a system variable on CCU / Homegear.""" @abstractmethod - @service() async def delete_system_variable(self, name: str) -> bool: """Delete a system variable from CCU / Homegear.""" @abstractmethod - @service() async def get_system_variable(self, name: str) -> str: """Get single system variable from CCU / Homegear.""" @abstractmethod - @service(re_raise=False, no_raise_return=()) async def get_all_system_variables( self, include_internal: bool ) -> tuple[SystemVariableData, ...]: """Get all system variables from CCU / Homegear.""" @abstractmethod - @service(re_raise=False, no_raise_return=()) async def get_all_programs(self, include_internal: bool) -> tuple[ProgramData, ...]: """Get all programs, if available.""" @abstractmethod - @service(re_raise=False, no_raise_return={}) async def get_all_rooms(self) -> dict[str, set[str]]: """Get all rooms, if available.""" @abstractmethod - @service(re_raise=False, no_raise_return={}) async def get_all_functions(self) -> dict[str, set[str]]: """Get all functions, if available.""" @@ -555,8 +545,7 @@ async def get_value( f"GET_VALUE failed with for: {channel_address}/{parameter}/{paramset_key}: {reduce_args(args=ex.args)}" ) from ex - @measure_execution_time - @service() + @service(measure_performance=True) async def _set_value( self, channel_address: str, @@ -653,6 +642,7 @@ def _write_temporary_value(self, data_point_key_values: set[DP_KEY_VALUE]) -> No ): data_point.write_temporary_value(value=value) + @service(re_raise=False, no_raise_return=set()) async def set_value( self, channel_address: str, @@ -708,8 +698,7 @@ async def get_paramset( f"GET_PARAMSET failed with for {address}/{paramset_key}: {reduce_args(args=ex.args)}" ) from ex - @measure_execution_time - @service() + @service(measure_performance=True) async def put_paramset( self, channel_address: str, @@ -872,7 +861,7 @@ def _get_parameter_type( return parameter_data["TYPE"] return None - @service() + @service(re_raise=False) async def fetch_paramset_description( self, channel_address: str, paramset_key: ParamsetKey ) -> None: @@ -889,7 +878,7 @@ async def fetch_paramset_description( paramset_description=paramset_description, ) - @service() + @service(re_raise=False) async def fetch_paramset_descriptions(self, device_description: DeviceDescription) -> None: """Fetch paramsets for provided device description.""" data = await self.get_paramset_descriptions(device_description=device_description) @@ -956,8 +945,7 @@ async def has_program_ids(self, channel_hmid: str) -> bool: """Return if a channel has program ids.""" return False - @measure_execution_time - @service(re_raise=False) + @service(re_raise=False, measure_performance=True) async def list_devices(self) -> tuple[DeviceDescription, ...] | None: """List devices of homematic backend.""" try: @@ -1006,7 +994,7 @@ async def update_device_firmware(self, device_address: str) -> bool: return result return False - @service() + @service(re_raise=False) async def update_paramset_descriptions(self, device_address: str) -> None: """Update paramsets descriptions for provided device_address.""" if not self.central.device_descriptions.get_device_descriptions( @@ -1052,8 +1040,7 @@ def supports_ping_pong(self) -> bool: """Return the supports_ping_pong info of the backend.""" return True - @measure_execution_time - @service() + @service(re_raise=False, measure_performance=True) async def fetch_device_details(self) -> None: """Get all names via JSON-RPS and store in data.NAMES.""" if json_result := await self._json_rpc_client.get_device_details(): @@ -1083,8 +1070,7 @@ async def fetch_device_details(self) -> None: else: _LOGGER.debug("FETCH_DEVICE_DETAILS: Unable to fetch device details via JSON-RPC") - @measure_execution_time - @service() + @service(re_raise=False, measure_performance=True) async def fetch_all_device_data(self) -> None: """Fetch all device data from CCU.""" try: @@ -1105,12 +1091,14 @@ async def fetch_all_device_data(self) -> None: interface_event_type=InterfaceEventType.FETCH_DATA, data={EventKey.AVAILABLE: False}, ) + raise _LOGGER.debug( "FETCH_ALL_DEVICE_DATA: Unable to get all device data via JSON-RPC RegaScript for interface %s", self.interface, ) + @service(re_raise=False, no_raise_return=False) async def check_connection_availability(self, handle_ping_pong: bool) -> bool: """Check if _proxy is still initialized.""" try: @@ -1155,8 +1143,7 @@ async def report_value_usage(self, address: str, value_id: str, ref_counter: int f"REPORT_VALUE_USAGE failed with: {address}/{value_id}/{ref_counter}: {reduce_args(args=ex.args)}" ) from ex - @measure_execution_time - @service() + @service(measure_performance=True) async def set_system_variable(self, name: str, value: Any) -> bool: """Set a system variable on CCU / Homegear.""" return await self._json_rpc_client.set_system_variable(name=name, value=value) @@ -1314,8 +1301,7 @@ async def get_value( f"GET_VALUE failed with for: {channel_address}/{parameter}/{paramset_key}: {reduce_args(args=ex.args)}" ) from ex - @measure_execution_time - @service(re_raise=False) + @service(re_raise=False, measure_performance=True) async def list_devices(self) -> tuple[DeviceDescription, ...] | None: """List devices of homematic backend.""" try: @@ -1440,13 +1426,12 @@ def supports_ping_pong(self) -> bool: """Return the supports_ping_pong info of the backend.""" return False - @measure_execution_time + @service(re_raise=False) async def fetch_all_device_data(self) -> None: """Fetch all device data from CCU.""" return - @measure_execution_time - @service() + @service(re_raise=False, measure_performance=True) async def fetch_device_details(self) -> None: """Get all names from metadata (Homegear).""" _LOGGER.debug("FETCH_DEVICE_DETAILS: Fetching names via Metadata") @@ -1466,6 +1451,7 @@ async def fetch_device_details(self) -> None: address, ) + @service(re_raise=False, no_raise_return=False) async def check_connection_availability(self, handle_ping_pong: bool) -> bool: """Check if proxy is still initialized.""" try: @@ -1487,8 +1473,7 @@ async def execute_program(self, pid: str) -> bool: """Execute a program on Homegear.""" return True - @measure_execution_time - @service() + @service(measure_performance=True) async def set_system_variable(self, name: str, value: Any) -> bool: """Set a system variable on CCU / Homegear.""" try: diff --git a/hahomematic/client/json_rpc.py b/hahomematic/client/json_rpc.py index 3070daa1..6ac99e48 100644 --- a/hahomematic/client/json_rpc.py +++ b/hahomematic/client/json_rpc.py @@ -23,7 +23,6 @@ from hahomematic import central as hmcu, config from hahomematic.async_support import Looper from hahomematic.const import ( - DEFAULT_ENCODING, HTMLTAG_PATTERN, PATH_JSON_RPC, REGA_SCRIPT_FETCH_ALL_DEVICE_DATA, @@ -31,6 +30,7 @@ REGA_SCRIPT_PATH, REGA_SCRIPT_SET_SYSTEM_VARIABLE, REGA_SCRIPT_SYSTEM_VARIABLES_EXT_MARKER, + UTF8, DeviceDescription, Interface, ParameterData, @@ -303,7 +303,7 @@ def _load_script(script_name: str) -> str | None: script_file = os.path.join( Path(__file__).resolve().parent, REGA_SCRIPT_PATH, script_name ) - if script := Path(script_file).read_text(encoding=DEFAULT_ENCODING): + if script := Path(script_file).read_text(encoding=UTF8): self._script_cache[script_name] = script return script return None @@ -396,14 +396,14 @@ async def _do_post( async def _get_json_reponse(self, response: ClientResponse) -> dict[str, Any] | Any: """Return the json object from response.""" try: - return await response.json(encoding="utf-8") + return await response.json(encoding=UTF8) except ValueError as ver: _LOGGER.debug( "DO_POST: ValueError [%s] Unable to parse JSON. Trying workaround", reduce_args(args=ver.args), ) # Workaround for bug in CCU - return orjson.loads((await response.json(encoding="utf-8")).replace("\\", "")) + return orjson.loads((await response.read()).decode(UTF8)) async def logout(self) -> None: """Logout of CCU.""" diff --git a/hahomematic/const.py b/hahomematic/const.py index 3b14aac0..1d57a1c0 100644 --- a/hahomematic/const.py +++ b/hahomematic/const.py @@ -9,11 +9,10 @@ import re from typing import Any, Final, Required, TypedDict -VERSION: Final = "2024.11.7" +VERSION: Final = "2024.11.8" DEFAULT_CONNECTION_CHECKER_INTERVAL: Final = 15 # check if connection is available via rpc ping DEFAULT_CUSTOM_ID: Final = "custom_id" -DEFAULT_ENCODING: Final = "UTF-8" DEFAULT_INCLUDE_INTERNAL_PROGRAMS: Final = False DEFAULT_INCLUDE_INTERNAL_SYSVARS: Final = True DEFAULT_JSON_SESSION_AGE: Final = 90 @@ -33,6 +32,8 @@ DEFAULT_VERIFY_TLS: Final = False DEFAULT_WAIT_FOR_CALLBACK: Final[int | None] = None +UTF8: Final = "utf-8" + MAX_WAIT_FOR_CALLBACK: Final = 60 MAX_CACHE_AGE: Final = 10 diff --git a/hahomematic/decorators.py b/hahomematic/decorators.py new file mode 100644 index 00000000..835cd3fc --- /dev/null +++ b/hahomematic/decorators.py @@ -0,0 +1,132 @@ +"""Common Decorators used within hahomematic.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +from contextvars import Token +from datetime import datetime +from functools import wraps +import logging +from typing import Any, Final, ParamSpec, TypeVar, cast + +from hahomematic.context import IN_SERVICE_VAR +from hahomematic.exceptions import BaseHomematicException +from hahomematic.support import reduce_args + +P = ParamSpec("P") +T = TypeVar("T") + +_LOGGER: Final = logging.getLogger(__name__) + + +def service( + log_level: int = logging.ERROR, + re_raise: bool = True, + no_raise_return: Any = None, + measure_performance: bool = False, +) -> Callable: + """Mark function as service call and log exceptions.""" + + def service_decorator(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]: + """Decorate service.""" + + do_measure_performance = measure_performance and _LOGGER.isEnabledFor(level=logging.DEBUG) + + @wraps(func) + async def service_wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + """Wrap service to log exception.""" + if do_measure_performance: + start = datetime.now() + + token: Token | None = None + if not IN_SERVICE_VAR.get(): + token = IN_SERVICE_VAR.set(True) + try: + return_value = await func(*args, **kwargs) + if token: + IN_SERVICE_VAR.reset(token) + return return_value # noqa: TRY300 + except BaseHomematicException as bhe: + if token: + IN_SERVICE_VAR.reset(token) + if not IN_SERVICE_VAR.get() and log_level > logging.NOTSET: + message = f"{func.__name__.upper()} failed: {reduce_args(args=bhe.args)}" + logging.getLogger(args[0].__module__).log( + level=log_level, + msg=message, + ) + if re_raise: + raise + return cast(T, no_raise_return) + finally: + if do_measure_performance: + _log_performance_message(func, start, *args, **kwargs) + + setattr(service_wrapper, "ha_service", True) + return service_wrapper + + return service_decorator + + +def _log_performance_message( + func: Callable, start: datetime, *args: P.args, **kwargs: P.kwargs +) -> None: + """Log the performance message.""" + delta = (datetime.now() - start).total_seconds() + caller = str(args[0]) if len(args) > 0 else "" + + iface: str = "" + if interface := str(kwargs.get("interface", "")): + iface = f"interface: {interface}" + if interface_id := kwargs.get("interface_id", ""): + iface = f"interface_id: {interface_id}" + + message = f"Execution of {func.__name__.upper()} took {delta}s from {caller}" + if iface: + message += f"/{iface}" + + _LOGGER.info(message) + + +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") + } + + +def measure_execution_time[_CallableT: Callable[..., Any]](func: _CallableT) -> _CallableT: + """Decorate function to measure the function execution time.""" + + is_enabled = _LOGGER.isEnabledFor(level=logging.DEBUG) + + @wraps(func) + async def async_measure_wrapper(*args: Any, **kwargs: Any) -> Any: + """Wrap method.""" + if is_enabled: + start = datetime.now() + try: + return await func(*args, **kwargs) + finally: + if is_enabled: + _log_performance_message(func, start, *args, **kwargs) + + @wraps(func) + def measure_wrapper(*args: Any, **kwargs: Any) -> Any: + """Wrap method.""" + if is_enabled: + start = datetime.now() + try: + return func(*args, **kwargs) + finally: + if is_enabled: + _log_performance_message(func, start, *args, **kwargs) + + if asyncio.iscoroutinefunction(func): + return async_measure_wrapper # type: ignore[return-value] + return measure_wrapper # type: ignore[return-value] diff --git a/hahomematic/model/custom/climate.py b/hahomematic/model/custom/climate.py index eb75bc3a..1bb78176 100644 --- a/hahomematic/model/custom/climate.py +++ b/hahomematic/model/custom/climate.py @@ -14,6 +14,7 @@ DataPointCategory, ParamsetKey, ) +from hahomematic.decorators import service from hahomematic.exceptions import ClientException, ValidationException from hahomematic.model import device as hmd from hahomematic.model.custom import definition as hmed @@ -21,7 +22,7 @@ from hahomematic.model.custom.data_point import CustomDataPoint from hahomematic.model.custom.support import CustomConfig from hahomematic.model.data_point import CallParameterCollector, bind_collector -from hahomematic.model.decorators import config_property, service, state_property +from hahomematic.model.decorators import config_property, state_property from hahomematic.model.generic import ( DpAction, DpBinarySensor, diff --git a/hahomematic/model/custom/data_point.py b/hahomematic/model/custom/data_point.py index 115af849..1e00763f 100644 --- a/hahomematic/model/custom/data_point.py +++ b/hahomematic/model/custom/data_point.py @@ -8,12 +8,13 @@ from typing import Any, Final, cast from hahomematic.const import CALLBACK_TYPE, DP_KEY, INIT_DATETIME, CallSource, DataPointUsage +from hahomematic.decorators import get_service_calls from hahomematic.model import device as hmd from hahomematic.model.custom import definition as hmed from hahomematic.model.custom.const import CDPD, DeviceProfile, Field from hahomematic.model.custom.support import CustomConfig from hahomematic.model.data_point import BaseDataPoint, CallParameterCollector -from hahomematic.model.decorators import get_service_calls, state_property +from hahomematic.model.decorators import state_property from hahomematic.model.generic import data_point as hmge from hahomematic.model.support import ( DataPointNameData, diff --git a/hahomematic/model/data_point.py b/hahomematic/model/data_point.py index db4be507..81ae7d94 100644 --- a/hahomematic/model/data_point.py +++ b/hahomematic/model/data_point.py @@ -37,9 +37,10 @@ ParamsetKey, ) from hahomematic.context import IN_SERVICE_VAR +from hahomematic.decorators import get_service_calls from hahomematic.exceptions import BaseHomematicException, HaHomematicException from hahomematic.model import device as hmd -from hahomematic.model.decorators import config_property, get_service_calls, state_property +from hahomematic.model.decorators import config_property, state_property from hahomematic.model.support import ( DataPointNameData, DataPointPathData, diff --git a/hahomematic/model/decorators.py b/hahomematic/model/decorators.py index 2536c0e3..99a00363 100644 --- a/hahomematic/model/decorators.py +++ b/hahomematic/model/decorators.py @@ -2,26 +2,17 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable -from contextvars import Token +from collections.abc import Callable from datetime import datetime from enum import Enum -from functools import wraps -import logging -from typing import Any, ParamSpec, TypeVar, cast - -from hahomematic.context import IN_SERVICE_VAR -from hahomematic.exceptions import BaseHomematicException -from hahomematic.support import reduce_args +from typing import Any, ParamSpec, TypeVar __all__ = [ "config_property", "get_public_attributes_for_config_property", "get_public_attributes_for_info_property", "get_public_attributes_for_state_property", - "get_service_calls", "info_property", - "service", "state_property", ] @@ -140,52 +131,3 @@ 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( - log_level: int = logging.ERROR, - re_raise: bool = True, - no_raise_return: Any = None, -) -> 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.""" - token: Token | None = None - if not IN_SERVICE_VAR.get(): - token = IN_SERVICE_VAR.set(True) - try: - return_value = await func(*args, **kwargs) - if token: - IN_SERVICE_VAR.reset(token) - return return_value # noqa: TRY300 - except BaseHomematicException as bhe: - if token: - IN_SERVICE_VAR.reset(token) - if not IN_SERVICE_VAR.get() and log_level > logging.NOTSET: - logging.getLogger(args[0].__module__).log( - level=log_level, msg=reduce_args(args=bhe.args) - ) - if re_raise: - raise - return cast(T, no_raise_return) - - 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/model/device.py b/hahomematic/model/device.py index 7b1a1a96..5b3aad34 100644 --- a/hahomematic/model/device.py +++ b/hahomematic/model/device.py @@ -45,10 +45,11 @@ ProductGroup, RxMode, ) +from hahomematic.decorators import service from hahomematic.exceptions import BaseHomematicException, HaHomematicException from hahomematic.model.custom import data_point as hmce, definition as hmed from hahomematic.model.data_point import BaseParameterDataPoint, CallbackDataPoint -from hahomematic.model.decorators import info_property, service, state_property +from hahomematic.model.decorators import info_property, state_property from hahomematic.model.event import GenericEvent from hahomematic.model.generic import GenericDataPoint from hahomematic.model.support import ( @@ -534,6 +535,7 @@ def refresh_firmware_data(self) -> None: for callback_handler in self._firmware_update_callbacks: callback_handler() + @service() async def update_firmware(self, refresh_after_update_intervals: tuple[int, ...]) -> bool: """Update the firmware of the homematic device.""" update_result = await self._client.update_device_firmware(device_address=self._address) @@ -548,6 +550,7 @@ async def refresh_data() -> None: return update_result # type: ignore[no-any-return] + @service() async def load_value_cache(self) -> None: """Init the parameter cache.""" if len(self.generic_data_points) > 0: @@ -559,6 +562,7 @@ async def load_value_cache(self) -> None: self._address, ) + @service() async def reload_paramset_descriptions(self) -> None: """Reload paramset for device.""" for ( @@ -744,6 +748,7 @@ def unique_id(self) -> str: """Return the unique_id of the channel.""" return self._unique_id + @service() async def create_central_link(self) -> None: """Create a central link to support press events.""" if self._has_key_press_events and not await self._has_central_link(): @@ -751,6 +756,7 @@ async def create_central_link(self) -> None: address=self._address, value_id=REPORT_VALUE_USAGE_VALUE_ID, ref_counter=1 ) + @service() async def remove_central_link(self) -> None: """Remove a central link.""" if ( @@ -762,6 +768,7 @@ async def remove_central_link(self) -> None: address=self._address, value_id=REPORT_VALUE_USAGE_VALUE_ID, ref_counter=0 ) + @service() async def cleanup_central_link_metadata(self) -> None: """Cleanup the metadata for central links.""" if metadata := await self._device.client.get_metadata( @@ -1141,6 +1148,7 @@ def __init__(self, device: Device) -> None: self._device_address: Final = device.address self._random_id: Final[str] = f"VCU{int(random.randint(1000000, 9999999))}" + @service() async def export_data(self) -> None: """Export data.""" device_descriptions: Mapping[str, DeviceDescription] = ( diff --git a/hahomematic/model/generic/button.py b/hahomematic/model/generic/button.py index 11850cbd..45852107 100644 --- a/hahomematic/model/generic/button.py +++ b/hahomematic/model/generic/button.py @@ -3,7 +3,7 @@ from __future__ import annotations from hahomematic.const import DataPointCategory -from hahomematic.model.decorators import service +from hahomematic.decorators import service from hahomematic.model.generic.data_point import GenericDataPoint diff --git a/hahomematic/model/generic/data_point.py b/hahomematic/model/generic/data_point.py index 593e38ef..9534e71b 100644 --- a/hahomematic/model/generic/data_point.py +++ b/hahomematic/model/generic/data_point.py @@ -14,8 +14,8 @@ ParameterData, ParamsetKey, ) +from hahomematic.decorators import service from hahomematic.model import data_point as hme, device as hmd -from hahomematic.model.decorators import service from hahomematic.model.support import ( DataPointNameData, GenericParameterType, @@ -121,7 +121,7 @@ async def send_value( if self._validate_state_change and not self.is_state_change(value=converted_value): return set() - return await self._client.set_value( + return await self._client.set_value( # type: ignore[no-any-return] channel_address=self._channel.address, paramset_key=self._paramset_key, parameter=self._parameter, diff --git a/hahomematic/model/generic/switch.py b/hahomematic/model/generic/switch.py index 536c9f05..556a1f65 100644 --- a/hahomematic/model/generic/switch.py +++ b/hahomematic/model/generic/switch.py @@ -5,8 +5,9 @@ from typing import Final from hahomematic.const import DataPointCategory, ParameterType +from hahomematic.decorators import service from hahomematic.model.data_point import CallParameterCollector -from hahomematic.model.decorators import service, state_property +from hahomematic.model.decorators import state_property from hahomematic.model.generic.data_point import GenericDataPoint _PARAM_ON_TIME: Final = "ON_TIME" diff --git a/hahomematic/model/hub/__init__.py b/hahomematic/model/hub/__init__.py index 05ce51fd..516bed7a 100644 --- a/hahomematic/model/hub/__init__.py +++ b/hahomematic/model/hub/__init__.py @@ -17,7 +17,7 @@ SystemVariableData, SysvarType, ) -from hahomematic.model.decorators import service +from hahomematic.decorators import service from hahomematic.model.hub.binary_sensor import SysvarDpBinarySensor from hahomematic.model.hub.button import ProgramDpButton from hahomematic.model.hub.data_point import GenericHubDataPoint, GenericSysvarDataPoint diff --git a/hahomematic/model/hub/button.py b/hahomematic/model/hub/button.py index 7f89063b..b9484d6d 100644 --- a/hahomematic/model/hub/button.py +++ b/hahomematic/model/hub/button.py @@ -6,7 +6,8 @@ from hahomematic import central as hmcu from hahomematic.const import PROGRAM_ADDRESS, DataPointCategory, HubData, ProgramData -from hahomematic.model.decorators import get_service_calls, service, state_property +from hahomematic.decorators import get_service_calls, service +from hahomematic.model.decorators import state_property from hahomematic.model.hub.data_point import GenericHubDataPoint diff --git a/hahomematic/model/hub/data_point.py b/hahomematic/model/hub/data_point.py index 073dc183..942defbb 100644 --- a/hahomematic/model/hub/data_point.py +++ b/hahomematic/model/hub/data_point.py @@ -9,13 +9,9 @@ from hahomematic import central as hmcu from hahomematic.const import SYSVAR_ADDRESS, SYSVAR_TYPE, HubData, SystemVariableData +from hahomematic.decorators import get_service_calls, service from hahomematic.model.data_point import CallbackDataPoint -from hahomematic.model.decorators import ( - config_property, - get_service_calls, - service, - state_property, -) +from hahomematic.model.decorators import config_property, state_property from hahomematic.model.support import ( PathData, PayloadMixin, diff --git a/hahomematic/model/hub/number.py b/hahomematic/model/hub/number.py index 9a6fb551..af53fd33 100644 --- a/hahomematic/model/hub/number.py +++ b/hahomematic/model/hub/number.py @@ -6,7 +6,7 @@ from typing import Final from hahomematic.const import DataPointCategory -from hahomematic.model.decorators import service +from hahomematic.decorators import service from hahomematic.model.hub.data_point import GenericSysvarDataPoint _LOGGER: Final = logging.getLogger(__name__) diff --git a/hahomematic/model/hub/select.py b/hahomematic/model/hub/select.py index 7201b79e..7fe9bde2 100644 --- a/hahomematic/model/hub/select.py +++ b/hahomematic/model/hub/select.py @@ -6,7 +6,8 @@ from typing import Final from hahomematic.const import DataPointCategory -from hahomematic.model.decorators import service, state_property +from hahomematic.decorators import service +from hahomematic.model.decorators import state_property from hahomematic.model.hub.data_point import GenericSysvarDataPoint from hahomematic.model.support import get_value_from_value_list diff --git a/hahomematic/model/update.py b/hahomematic/model/update.py index cd025ac2..a8298f1c 100644 --- a/hahomematic/model/update.py +++ b/hahomematic/model/update.py @@ -14,10 +14,11 @@ DataPointCategory, Interface, ) +from hahomematic.decorators import get_service_calls, service from hahomematic.exceptions import HaHomematicException from hahomematic.model import device as hmd from hahomematic.model.data_point import CallbackDataPoint -from hahomematic.model.decorators import config_property, get_service_calls, state_property +from hahomematic.model.decorators import config_property, state_property from hahomematic.model.support import DataPointPathData, PayloadMixin, generate_unique_id __all__ = ["DpUpdate"] @@ -125,12 +126,14 @@ def _unregister_data_point_updated_callback(self, cb: Callable, custom_id: str) self._custom_id = None self._device.unregister_firmware_update_callback(cb) + @service() async def update_firmware(self, refresh_after_update_intervals: tuple[int, ...]) -> bool: """Turn the update on.""" - return await self._device.update_firmware( + return await self._device.update_firmware( # type: ignore[no-any-return] refresh_after_update_intervals=refresh_after_update_intervals ) + @service() async def refresh_firmware_data(self) -> None: """Refresh device firmware data.""" await self._device.central.refresh_firmware_data(device_address=self._device.address) diff --git a/hahomematic/performance.py b/hahomematic/performance.py deleted file mode 100644 index eeb31fac..00000000 --- a/hahomematic/performance.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Decorators used within hahomematic.""" - -from __future__ import annotations - -import asyncio -from collections.abc import Callable -from datetime import datetime -from functools import wraps -import logging -from typing import Any, Final - -_LOGGER: Final = logging.getLogger(__name__) - - -def measure_execution_time[_CallableT: Callable[..., Any]](func: _CallableT) -> _CallableT: - """Decorate function to measure the function execution time.""" - - is_enabled = _LOGGER.isEnabledFor(level=logging.DEBUG) - - @wraps(func) - async def async_measure_wrapper(*args: Any, **kwargs: Any) -> Any: - """Wrap method.""" - if is_enabled: - start = datetime.now() - try: - return await func(*args, **kwargs) - finally: - if is_enabled: - delta = (datetime.now() - start).total_seconds() - caller = str(args[0]) if len(args) > 0 else "" - - iface: str = "" - if interface := str(kwargs.get("interface", "")): - iface = f"interface: {interface}" - if interface_id := kwargs.get("interface_id", ""): - iface = f"interface_id: {interface_id}" - - _LOGGER.info( - "Execution of %s took %ss (%s) (%s)", - func.__name__, - delta, - caller, - iface, - ) - - @wraps(func) - def measure_wrapper(*args: Any, **kwargs: Any) -> Any: - """Wrap method.""" - if is_enabled: - start = datetime.now() - try: - return func(*args, **kwargs) - finally: - if is_enabled: - delta = (datetime.now() - start).total_seconds() - _LOGGER.info( - "Execution of %s took %ss args(%s) kwargs(%s) ", - func.__name__, - delta, - args, - kwargs, - ) - - if asyncio.iscoroutinefunction(func): - return async_measure_wrapper # type: ignore[return-value] - return measure_wrapper # type: ignore[return-value] diff --git a/hahomematic/support.py b/hahomematic/support.py index 7c55cd6b..6427d187 100644 --- a/hahomematic/support.py +++ b/hahomematic/support.py @@ -34,6 +34,7 @@ MAX_CACHE_AGE, NO_CACHE_ENTRY, PRIMARY_CLIENT_CANDIDATE_INTERFACES, + UTF8, CommandRxMode, ParamsetKey, RxMode, @@ -73,7 +74,7 @@ def build_headers( ) -> list[tuple[str, str]]: """Build XML-RPC API header.""" cred_bytes = f"{username}:{password}".encode() - base64_message = base64.b64encode(cred_bytes).decode("utf-8") + base64_message = base64.b64encode(cred_bytes).decode(UTF8) return [("Authorization", f"Basic {base64_message}")] diff --git a/hahomematic_support/client_local.py b/hahomematic_support/client_local.py index db6d8371..97456d20 100644 --- a/hahomematic_support/client_local.py +++ b/hahomematic_support/client_local.py @@ -13,8 +13,8 @@ from hahomematic.client import _LOGGER, Client, _ClientConfig from hahomematic.config import WAIT_FOR_CALLBACK from hahomematic.const import ( - DEFAULT_ENCODING, DP_KEY_VALUE, + UTF8, CallSource, CommandRxMode, DeviceDescription, @@ -27,6 +27,7 @@ SystemInformation, SystemVariableData, ) +from hahomematic.decorators import service from hahomematic.support import is_channel_address LOCAL_SERIAL: Final = "0815_4711" @@ -111,6 +112,7 @@ def is_callback_alive(self) -> bool: """Return if XmlRPC-Server is alive based on received events for this client.""" return True + @service(re_raise=False, no_raise_return=False) async def check_connection_availability(self, handle_ping_pong: bool) -> bool: """Send ping to CCU to generate PONG event.""" if handle_ping_pong and self.supports_ping_pong: @@ -326,7 +328,7 @@ async def _load_json_file(self, anchor: str, resource: str, filename: str) -> An def _load() -> Any | None: with open( file=os.path.join(package_path, resource, filename), - encoding=DEFAULT_ENCODING, + encoding=UTF8, ) as fptr: return orjson.loads(fptr.read()) diff --git a/tests/helper.py b/tests/helper.py index 61ae584b..1f2499f4 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -197,7 +197,7 @@ def _load_json_file(anchor: str, resource: str, filename: str) -> Any | None: package_path = str(importlib.resources.files(anchor)) with open( file=os.path.join(package_path, resource, filename), - encoding=hahomematic_const.DEFAULT_ENCODING, + encoding=hahomematic_const.UTF8, ) as fptr: return orjson.loads(fptr.read())