Skip to content

Commit

Permalink
Use regex to identify schedule profiles (#1769)
Browse files Browse the repository at this point in the history
* Use regex to identify schedule profiles

* Update const.py
  • Loading branch information
SukramJ authored Oct 13, 2024
1 parent 2266cfd commit 5588b9c
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 21 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.10.7 (2024-10-12)

- Improve profile validation
- Use regex to identify schedule profiles

# Version 2024.10.6 (2024-10-11)

Expand Down
6 changes: 6 additions & 0 deletions hahomematic/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
DEVICE_ADDRESS_PATTERN: Final = re.compile(r"^[0-9a-zA-Z-]{5,20}$")
ALLOWED_HOSTNAME_PATTERN: Final = re.compile(r"(?!-)[a-z0-9-]{1,63}(?<!-)$", re.IGNORECASE)
HTMLTAG_PATTERN: Final = re.compile(r"<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
SCHEDULER_PROFILE_PATTERN = re.compile(
r"^P[1-6]_(ENDTIME|TEMPERATURE)_(MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FRIDAY|SATURDAY|SUNDAY)_([1-9]|1[0-3])$"
)
SCHEDULER_TIME_PATTERN = re.compile(r"^(([0-1]{0,1}[0-9])|(2[0-4])):[0-5][0-9]")

HUB_PATH: Final = "hub"
BLOCK_LOG_TIMEOUT = 60
Expand Down Expand Up @@ -267,6 +271,7 @@ class Parameter(StrEnum):
OPERATING_VOLTAGE = "OPERATING_VOLTAGE"
OPTICAL_ALARM_ACTIVE = "OPTICAL_ALARM_ACTIVE"
OPTICAL_ALARM_SELECTION = "OPTICAL_ALARM_SELECTION"
OPTIMUM_START_STOP = "OPTIMUM_START_STOP"
PARTY_MODE = "PARTY_MODE"
PONG = "PONG"
POWER = "POWER"
Expand Down Expand Up @@ -306,6 +311,7 @@ class Parameter(StrEnum):
TEMPERATURE = "TEMPERATURE"
TEMPERATURE_MAXIMUM = "TEMPERATURE_MAXIMUM"
TEMPERATURE_MINIMUM = "TEMPERATURE_MINIMUM"
TEMPERATURE_OFFSET = "TEMPERATURE_OFFSET"
UN_REACH = "UNREACH"
UPDATE_PENDING = "UPDATE_PENDING"
VALVE_STATE = "VALVE_STATE"
Expand Down
56 changes: 36 additions & 20 deletions hahomematic/platforms/custom/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
import logging
from typing import Any, Final, cast

from hahomematic.const import HmPlatform, ParamsetKey
from hahomematic.const import (
SCHEDULER_PROFILE_PATTERN,
SCHEDULER_TIME_PATTERN,
HmPlatform,
ParamsetKey,
)
from hahomematic.exceptions import ClientException, HaHomematicException, ValidationException
from hahomematic.platforms import device as hmd
from hahomematic.platforms.custom import definition as hmed
Expand All @@ -36,6 +41,8 @@
# HA constants
_CLOSED_LEVEL: Final = 0.0
_DEFAULT_TEMPERATURE_STEP: Final = 0.5
_MAX_SCHEDULER_TIME: Final = "24:00"
_MIN_SCHEDULER_TIME: Final = "00:00"
_OFF_TEMPERATURE: Final = 4.5
_PARTY_DATE_FORMAT: Final = "%Y_%m_%d %H:%M"
_PARTY_INIT_DATE: Final = "2000_01_01 00:00"
Expand Down Expand Up @@ -341,21 +348,21 @@ async def _get_schedule(
f"Schedule is not supported by device {self._device.name}"
) from cex

for line, slot_value in raw_schedule.items():
if not line.startswith("P"):
for slot_name, slot_value in raw_schedule.items():
if SCHEDULER_PROFILE_PATTERN.match(slot_name) is None:
continue
line_split = line.split("_")
if len(line_split) != 4:
slot_name_tuple = slot_name.split("_")
if len(slot_name_tuple) != 4:
continue
p, et, w, no = line_split
_profile = ScheduleProfile(p)
profile_name, slot_type, slot_weekday, slot_no = slot_name_tuple
_profile = ScheduleProfile(profile_name)
if profile and profile != _profile:
continue
_slot_type = ScheduleSlotType(et)
_weekday = ScheduleWeekday(w)
_slot_type = ScheduleSlotType(slot_type)
_weekday = ScheduleWeekday(slot_weekday)
if weekday and weekday != _weekday:
continue
_slot_no = int(no)
_slot_no = int(slot_no)

_add_to_schedule_data(
schedule_data=schedule_data,
Expand Down Expand Up @@ -465,7 +472,7 @@ def _validate_and_convert_simple_to_profile_weekday(
sorted_simple_weekday_list = _sort_simple_weekday_list(
simple_weekday_list=simple_weekday_list
)
previous_endtime = "00:00"
previous_endtime = _MIN_SCHEDULER_TIME
slot_no = 1
for slot in sorted_simple_weekday_list:
if (starttime := slot.get(ScheduleSlotType.STARTTIME)) is None:
Expand Down Expand Up @@ -915,16 +922,24 @@ def _profiles(self) -> Mapping[HmPresetMode, int]:
return profiles


def _convert_minutes_to_time_str(minutes: int) -> str:
def _convert_minutes_to_time_str(minutes: Any) -> str:
"""Convert minutes to a time string."""
try:
return f"{minutes//60:0=2}:{minutes%60:0=2}"
except Exception as ex:
raise ValidationException(ex) from ex
if not isinstance(minutes, int):
return _MAX_SCHEDULER_TIME
time_str = f"{minutes//60:0=2}:{minutes%60:0=2}"
if SCHEDULER_TIME_PATTERN.match(time_str) is None:
raise ValidationException(
f"Time {time_str} is not valid. Format must be hh:mm with min: {_MIN_SCHEDULER_TIME} and max: {_MAX_SCHEDULER_TIME}"
)
return time_str


def _convert_time_str_to_minutes(time_str: str) -> int:
"""Convert minutes to a time string."""
if SCHEDULER_TIME_PATTERN.match(time_str) is None:
raise ValidationException(
f"Time {time_str} is not valid. Format must be hh:mm with min: {_MIN_SCHEDULER_TIME} and max: {_MAX_SCHEDULER_TIME}"
)
try:
h, m = time_str.split(":")
return (int(h) * 60) + int(m)
Expand All @@ -950,7 +965,7 @@ def _fillup_weekday_data(base_temperature: float, weekday_data: WEEKDAY_DICT) ->
for slot_no in SCHEDULE_SLOT_IN_RANGE:
if slot_no not in weekday_data:
weekday_data[slot_no] = {
ScheduleSlotType.ENDTIME: "24:00",
ScheduleSlotType.ENDTIME: _MAX_SCHEDULER_TIME,
ScheduleSlotType.TEMPERATURE: base_temperature,
}

Expand All @@ -964,12 +979,13 @@ def _get_raw_paramset(schedule_data: _SCHEDULE_DICT) -> _RAW_SCHEDULE_DICT:
for weekday, weekday_data in profile_data.items():
for slot_no, slot in weekday_data.items():
for slot_type, slot_value in slot.items():
raw_profile_name = f"{str(profile)}_{str(slot_type)}_{str(weekday)}_{slot_no}"
if SCHEDULER_PROFILE_PATTERN.match(raw_profile_name) is None:
raise ValidationException(f"Not a valid profile name: {raw_profile_name}")
raw_value: float | int = cast(float | int, slot_value)
if slot_type == ScheduleSlotType.ENDTIME and isinstance(slot_value, str):
raw_value = _convert_time_str_to_minutes(slot_value)
raw_paramset[f"{str(profile)}_{str(slot_type)}_{str(weekday)}_{slot_no}"] = (
raw_value
)
raw_paramset[raw_profile_name] = raw_value
return raw_paramset


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools~=69.2.0", "wheel~=0.43.0"]
requires = ["setuptools==75.1.0"]
build-backend = "setuptools.build_meta"

[project]
Expand Down
22 changes: 22 additions & 0 deletions tests/test_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from hahomematic.client import Client
from hahomematic.const import (
INIT_DATETIME,
SCHEDULER_PROFILE_PATTERN,
SCHEDULER_TIME_PATTERN,
VIRTUAL_REMOTE_ADDRESSES,
EntityUsage,
ParameterType,
Expand Down Expand Up @@ -603,3 +605,23 @@ def test_is_channel_address() -> None:
assert is_channel_address("ABcdEFghIJ1234567890:123") is True
assert is_channel_address("12345678901234567890:123") is True
assert is_channel_address("123456789012345678901:123") is False


def test_scheduler_profile_pattern() -> None:
"""Test the SCHEDULER_PROFILE_PATTERN."""
assert SCHEDULER_PROFILE_PATTERN.match("P1_TEMPERATURE_THURSDAY_13")
assert SCHEDULER_PROFILE_PATTERN.match("P1_ENDTIME_THURSDAY_13")
assert SCHEDULER_PROFILE_PATTERN.match("P1_ENDTIME_THURSDAY_3")
assert SCHEDULER_PROFILE_PATTERN.match("Px_ENDTIME_THURSDAY_13") is None
assert SCHEDULER_PROFILE_PATTERN.match("P3_ENDTIME_THURSDAY_19") is None


def test_scheduler_time_pattern() -> None:
"""Test the SCHEDULER_TIME_PATTERN."""
assert SCHEDULER_TIME_PATTERN.match("00:00")
assert SCHEDULER_TIME_PATTERN.match("01:15")
assert SCHEDULER_TIME_PATTERN.match("23:59")
assert SCHEDULER_TIME_PATTERN.match("24:00")
assert SCHEDULER_TIME_PATTERN.match("5:00")
assert SCHEDULER_TIME_PATTERN.match("25:00") is None
assert SCHEDULER_TIME_PATTERN.match("F:00") is None

0 comments on commit 5588b9c

Please sign in to comment.