Skip to content

Commit

Permalink
Make sysvars eventable (#1872)
Browse files Browse the repository at this point in the history
* Make sysvars eventable

* Fix tests

* Update requirements
  • Loading branch information
SukramJ authored Nov 22, 2024
1 parent edf8ecb commit 649625e
Show file tree
Hide file tree
Showing 15 changed files with 116 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.7.4
rev: v0.8.0
hooks:
- id: ruff
args:
Expand Down
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Version 2024.11.9 (2024-11-22)

- Make sysvars eventable

# Version 2024.11.8 (2024-11-21)

- Add missing @service annotations
Expand Down
13 changes: 13 additions & 0 deletions hahomematic/central/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ def __init__(self, central_config: CentralConfig) -> None:
dict[DP_KEY, list[Callable[[Any], Coroutine[Any, Any, None]]]]
] = {}
self._data_point_path_event_subscriptions: Final[dict[str, DP_KEY]] = {}
self._sysvar_data_point_event_subscriptions: Final[
dict[str, Callable[[Any], Coroutine[Any, Any, None]]]
] = {}
# {device_address, device}
self._devices: Final[dict[str, Device]] = {}
# {sysvar_name, sysvar_data_point}
Expand Down Expand Up @@ -330,12 +333,18 @@ def add_sysvar_data_point(self, sysvar_data_point: GenericSysvarDataPoint) -> No
"""Add new program button."""
if (ccu_var_name := sysvar_data_point.ccu_var_name) is not None:
self._sysvar_data_points[ccu_var_name] = sysvar_data_point
if sysvar_data_point.state_path not in self._sysvar_data_point_event_subscriptions:
self._sysvar_data_point_event_subscriptions[sysvar_data_point.state_path] = (
sysvar_data_point.event
)

def remove_sysvar_data_point(self, name: str) -> None:
"""Remove a sysvar data_point."""
if (sysvar_dp := self.get_sysvar_data_point(name=name)) is not None:
sysvar_dp.fire_device_removed_callback()
del self._sysvar_data_points[name]
if sysvar_dp.state_path in self._sysvar_data_point_event_subscriptions:
del self._sysvar_data_point_event_subscriptions[sysvar_dp.state_path]

def add_program_button(self, program_button: ProgramDpButton) -> None:
"""Add new program button."""
Expand Down Expand Up @@ -1324,6 +1333,10 @@ def get_data_point_path(self) -> tuple[str, ...]:
"""Return the registered state path."""
return tuple(self._data_point_path_event_subscriptions)

def get_sysvar_data_point_path(self) -> tuple[str, ...]:
"""Return the registered sysvar state path."""
return tuple(self._sysvar_data_point_event_subscriptions)

def get_un_ignore_candidates(self, include_master: bool = False) -> list[str]:
"""Return the candidates for un_ignore."""
candidates = sorted(
Expand Down
2 changes: 1 addition & 1 deletion hahomematic/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1514,7 +1514,7 @@ async def get_all_system_variables(
try:
if hg_variables := await self._proxy.getAllSystemVariables():
for name, value in hg_variables.items():
variables.append(SystemVariableData(name=name, value=value))
variables.append(SystemVariableData(vid=name, name=name, value=value))
except BaseHomematicException as ex:
raise ClientException(
f"GET_ALL_SYSTEM_VARIABLES failed: {reduce_args(args=ex.args)}"
Expand Down
1 change: 1 addition & 0 deletions hahomematic/client/json_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ async def get_all_system_variables(
min_value = parse_sys_var(data_type=data_type, raw_value=raw_min_value)
variables.append(
SystemVariableData(
vid=var_id,
name=name,
data_type=data_type,
unit=unit,
Expand Down
3 changes: 2 additions & 1 deletion hahomematic/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import re
from typing import Any, Final, Required, TypedDict

VERSION: Final = "2024.11.8"
VERSION: Final = "2024.11.9"

DEFAULT_CONNECTION_CHECKER_INTERVAL: Final = 15 # check if connection is available via rpc ping
DEFAULT_CUSTOM_ID: Final = "custom_id"
Expand Down Expand Up @@ -605,6 +605,7 @@ class ProgramData(HubData):
class SystemVariableData(HubData):
"""Dataclass for system variables."""

vid: str
value: SYSVAR_TYPE
data_type: SysvarType | None = None
extended_sysvar: bool = False
Expand Down
54 changes: 42 additions & 12 deletions hahomematic/model/hub/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from hahomematic.decorators import get_service_calls, service
from hahomematic.model.decorators import state_property
from hahomematic.model.hub.data_point import GenericHubDataPoint
from hahomematic.model.support import PathData, ProgramPathData


class ProgramDpButton(GenericHubDataPoint):
Expand All @@ -22,22 +23,47 @@ def __init__(
data: ProgramData,
) -> None:
"""Initialize the data_point."""
self._pid: Final = data.pid
super().__init__(
central=central,
address=PROGRAM_ADDRESS,
data=data,
)
self.pid: Final = data.pid
self.ccu_program_name: Final = data.name
self.is_active: bool = data.is_active
self.is_internal: bool = data.is_internal
self.last_execute_time: str = data.last_execute_time
self._ccu_program_name: Final = data.name
self._is_active: bool = data.is_active
self._is_internal: bool = data.is_internal
self._last_execute_time: str = data.last_execute_time
self._service_methods = get_service_calls(obj=self)

@state_property
def available(self) -> bool:
"""Return the availability of the device."""
return self.is_active
return self._is_active

@state_property
def ccu_program_name(self) -> str:
"""Return the ccu program name."""
return self._ccu_program_name

@state_property
def is_active(self) -> bool:
"""Return the program is active."""
return self._is_active

@state_property
def is_internal(self) -> bool:
"""Return the program is internal."""
return self._is_internal

@state_property
def last_execute_time(self) -> str:
"""Return the last execute time."""
return self._last_execute_time

@state_property
def pid(self) -> str:
"""Return the program id."""
return self._pid

def get_name(self, data: HubData) -> str:
"""Return the name of the program button data_point."""
Expand All @@ -48,14 +74,14 @@ def get_name(self, data: HubData) -> str:
def update_data(self, data: ProgramData) -> None:
"""Set variable value on CCU/Homegear."""
do_update: bool = False
if self.is_active != data.is_active:
self.is_active = data.is_active
if self._is_active != data.is_active:
self._is_active = data.is_active
do_update = True
if self.is_internal != data.is_internal:
self.is_internal = data.is_internal
if self._is_internal != data.is_internal:
self._is_internal = data.is_internal
do_update = True
if self.last_execute_time != data.last_execute_time:
self.last_execute_time = data.last_execute_time
if self._last_execute_time != data.last_execute_time:
self._last_execute_time = data.last_execute_time
do_update = True
if do_update:
self.fire_data_point_updated_callback()
Expand All @@ -64,3 +90,7 @@ def update_data(self, data: ProgramData) -> None:
async def press(self) -> None:
"""Handle the button press."""
await self.central.execute_program(pid=self.pid)

def _get_path_data(self) -> PathData:
"""Return the path data of the data_point."""
return ProgramPathData(pid=self.pid)
44 changes: 27 additions & 17 deletions hahomematic/model/hub/data_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,11 @@
from slugify import slugify

from hahomematic import central as hmcu
from hahomematic.const import SYSVAR_ADDRESS, SYSVAR_TYPE, HubData, SystemVariableData
from hahomematic.const import SYSVAR_ADDRESS, SYSVAR_TYPE, HubData, SystemVariableData, SysvarType
from hahomematic.decorators import get_service_calls, service
from hahomematic.model.data_point import CallbackDataPoint
from hahomematic.model.decorators import config_property, state_property
from hahomematic.model.support import (
PathData,
PayloadMixin,
ProgramPathData,
SysvarPathData,
generate_unique_id,
)
from hahomematic.model.support import PathData, PayloadMixin, SysvarPathData, generate_unique_id
from hahomematic.support import parse_sys_var


Expand Down Expand Up @@ -56,10 +50,6 @@ def name(self) -> str | None:
"""Return the name of the data_point."""
return self._name

def _get_path_data(self) -> PathData:
"""Return the path data of the data_point."""
return ProgramPathData(name=self._name)


class GenericSysvarDataPoint(GenericHubDataPoint):
"""Class for a HomeMatic system variable."""
Expand All @@ -72,9 +62,10 @@ def __init__(
data: SystemVariableData,
) -> None:
"""Initialize the data_point."""
self._vid: Final = data.vid
self.ccu_var_name: Final = data.name
super().__init__(central=central, address=SYSVAR_ADDRESS, data=data)
self.data_type: Final = data.data_type
self._data_type = data.data_type
self._values: Final[tuple[str, ...] | None] = tuple(data.values) if data.values else None
self._max: Final = data.max_value
self._min: Final = data.min_value
Expand All @@ -92,6 +83,21 @@ def available(self) -> bool:
"""Return the availability of the device."""
return self.central.available

@property
def data_type(self) -> SysvarType | None:
"""Return the availability of the device."""
return self._data_type

@data_type.setter
def data_type(self, data_type: SysvarType) -> None:
"""Write data_type."""
self._data_type = data_type

@config_property
def vid(self) -> str:
"""Return sysvar id."""
return self._vid

@property
def previous_value(self) -> SYSVAR_TYPE:
"""Return the previous value."""
Expand Down Expand Up @@ -141,14 +147,18 @@ def is_extended(self) -> bool:

def _get_path_data(self) -> PathData:
"""Return the path data of the data_point."""
return SysvarPathData(name=self.ccu_var_name)
return SysvarPathData(vid=self._vid)

def get_name(self, data: HubData) -> str:
"""Return the name of the sysvar data_point."""
if data.name.lower().startswith(tuple({"v_", "sv_", "sv"})):
return data.name
return f"Sv_{data.name}"

async def event(self, value: Any) -> None:
"""Handle event for which this data_point has subscribed."""
self.write_value(value=value)

def write_value(self, value: Any) -> None:
"""Set variable value on CCU/Homegear."""
old_value = self._current_value
Expand Down Expand Up @@ -178,8 +188,8 @@ def _convert_value(self, old_value: Any, new_value: Any) -> Any:
if new_value is None:
return None
value = new_value
if self.data_type:
value = parse_sys_var(data_type=self.data_type, raw_value=new_value)
if self._data_type:
value = parse_sys_var(data_type=self._data_type, raw_value=new_value)
elif isinstance(old_value, bool):
value = bool(new_value)
elif isinstance(old_value, int):
Expand All @@ -195,6 +205,6 @@ async def send_variable(self, value: Any) -> None:
"""Set variable value on CCU/Homegear."""
if client := self.central.primary_client:
await client.set_system_variable(
name=self.ccu_var_name, value=parse_sys_var(self.data_type, value)
name=self.ccu_var_name, value=parse_sys_var(self._data_type, value)
)
self._write_temporary_value(value=value)
2 changes: 1 addition & 1 deletion hahomematic/model/hub/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class SysvarDpSensor(GenericSysvarDataPoint):
def value(self) -> Any | None:
"""Return the value."""
if (
self.data_type == SysvarType.LIST
self._data_type == SysvarType.LIST
and (value := get_value_from_value_list(value=self._value, value_list=self.values))
is not None
):
Expand Down
12 changes: 6 additions & 6 deletions hahomematic/model/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,10 +303,10 @@ def state_path(self) -> str:
class ProgramPathData(PathData):
"""The program path data."""

def __init__(self, name: str):
def __init__(self, pid: str):
"""Init the path data."""
self._set_path: Final = f"{PROGRAM_SET_PATH_ROOT}/{name}"
self._state_path: Final = f"{PROGRAM_STATE_PATH_ROOT}/{name}"
self._set_path: Final = f"{PROGRAM_SET_PATH_ROOT}/{pid}"
self._state_path: Final = f"{PROGRAM_STATE_PATH_ROOT}/{pid}"

@property
def set_path(self) -> str:
Expand All @@ -322,10 +322,10 @@ def state_path(self) -> str:
class SysvarPathData(PathData):
"""The sysvar path data."""

def __init__(self, name: str):
def __init__(self, vid: str):
"""Init the path data."""
self._set_path: Final = f"{SYSVAR_SET_PATH_ROOT}/{name}"
self._state_path: Final = f"{SYSVAR_STATE_PATH_ROOT}/{name}"
self._set_path: Final = f"{SYSVAR_SET_PATH_ROOT}/{vid}"
self._state_path: Final = f"{SYSVAR_STATE_PATH_ROOT}/{vid}"

@property
def set_path(self) -> str:
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
aiohttp>=3.11.5
aiohttp>=3.11.7
orjson>=3.10.11
python-slugify>=8.0.4
voluptuous>=0.15.2
6 changes: 3 additions & 3 deletions requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ mypy-dev==1.14.0a2
pip==24.3.1
pre-commit==4.0.1
pur==7.3.2
pydantic==2.9.2
pydantic==2.10.1
pydevccu==0.1.8
pylint-per-file-ignores==1.3.2
pylint-strict-informational==0.1
pylint==3.3.1
pytest-aiohttp==1.0.5
pytest-asyncio==0.24.0
pytest-cov==6.0.0
pytest-rerunfailures==14.0
pytest-rerunfailures==15.0
pytest-socket==0.7.0
pytest-timeout==2.3.1
pytest==8.3.3
types-python-slugify==8.0.2.20240310
uv==0.5.2
uv==0.5.4
2 changes: 1 addition & 1 deletion requirements_test_pre_commit.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
bandit==1.7.10
codespell==2.3.0
ruff==0.7.4
ruff==0.8.0
yamllint==1.35.1
Loading

0 comments on commit 649625e

Please sign in to comment.