Skip to content

Commit 950635e

Browse files
authored
Merge pull request #190 from plugwise/find_last_used
Schedule-related bug-fixes and clean-up
2 parents 35f4f68 + cc85426 commit 950635e

File tree

8 files changed

+109
-222
lines changed

8 files changed

+109
-222
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
# v0.18.4: Smile: schedule-related bug-fixes and clean-up
4+
- Update `_last_used_schedule()`: provide the collected schedules as input in order to find the last-modified valid schedule.
5+
- `_rule_ids_by_x()`: replace None by NONE, allowing for simpler typing.
6+
- Remove `schedule_temperature` from output: for Adam the schedule temperature cannot be collected when a schedule is not active.
7+
- Simplify `_schedules()`, don't collect the schedule-details as no longer required.
8+
- Improve solution for plugwise-beta issue #276
9+
- Move HA Core input-checks into the backend library (into set_schedule_state() and set_preset())
10+
311
# v0.18.3: Smile: move solution for https://github.com/plugwise/plugwise-beta/issues/276 into backend
412

513
# v0.18.2: Smile: fix for https://github.com/plugwise/python-plugwise/issues/187

plugwise/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Plugwise module."""
22

3-
__version__ = "0.18.3"
3+
__version__ = "0.18.4"
44

55
from plugwise.smile import Smile
66
from plugwise.stick import Stick

plugwise/constants.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@
380380
NONE: Final = "None"
381381
FAKE_LOC: Final = "0000aaaa0000aaaa0000aaaa0000aa00"
382382
SEVERITIES: Final[list[str]] = ["other", "info", "warning", "error"]
383+
SPECIAL_FORMAT: Final[list[str]] = [ENERGY_KILO_WATT_HOUR, VOLUME_CUBIC_METERS]
383384
SWITCH_GROUP_TYPES: Final[list[str]] = ["switching", "report"]
384385
THERMOSTAT_CLASSES: Final[list[str]] = [
385386
"thermostat",
@@ -427,8 +428,6 @@
427428
"thermostat": {ATTR_NAME: "setpoint", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
428429
# Specific for an Anna
429430
"illuminance": {ATTR_UNIT_OF_MEASUREMENT: UNIT_LUMEN},
430-
# Schedule temperature - only present for a legacy Anna or an Anna v3
431-
"schedule_temperature": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
432431
# Specific for an Anna with heatpump extension installed
433432
"cooling_activation_outdoor_temperature": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
434433
"cooling_deactivation_threshold": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
@@ -738,7 +737,6 @@ class DeviceDataPoints(
738737
available_schedules: list[str]
739738
selected_schedule: str
740739
last_used: str | None
741-
schedule_temperature: float | None
742740

743741
mode: str
744742

plugwise/helper.py

Lines changed: 39 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
ATTR_NAME,
2222
ATTR_UNIT_OF_MEASUREMENT,
2323
BINARY_SENSORS,
24-
DAYS,
2524
DEVICE_MEASUREMENTS,
2625
ENERGY_KILO_WATT_HOUR,
2726
ENERGY_WATT_HOUR,
@@ -53,12 +52,7 @@
5352
PlugwiseException,
5453
ResponseError,
5554
)
56-
from .util import (
57-
escape_illegal_xml_characters,
58-
format_measure,
59-
in_between,
60-
version_to_model,
61-
)
55+
from .util import escape_illegal_xml_characters, format_measure, version_to_model
6256

6357

6458
def update_helper(
@@ -93,40 +87,6 @@ def check_model(name: str | None, vendor_name: str | None) -> str | None:
9387
return name
9488

9589

96-
def schedules_schedule_temp(
97-
schedules: dict[str, dict[str, float]], name: str
98-
) -> float | None:
99-
"""Helper-function for schedules().
100-
Obtain the schedule temperature of the schedule.
101-
"""
102-
if name == NONE:
103-
return None # pragma: no cover
104-
105-
schedule_list: list[tuple[int, dt.time, float]] = []
106-
for period, temp in schedules[name].items():
107-
moment, dummy = period.split(",")
108-
moment_cleaned = moment.replace("[", "").split(" ")
109-
day_nr = DAYS[moment_cleaned[0]]
110-
start_time = dt.datetime.strptime(moment_cleaned[1], "%H:%M").time()
111-
tmp_list: tuple[int, dt.time, float] = (day_nr, start_time, temp)
112-
schedule_list.append(tmp_list)
113-
114-
length = len(schedule_list)
115-
schedule_list = sorted(schedule_list)
116-
for i in range(length):
117-
j = (i + 1) % (length - 1)
118-
now = dt.datetime.now().time()
119-
today = dt.datetime.now().weekday()
120-
day_0 = schedule_list[i][0]
121-
day_1 = schedule_list[j][0]
122-
time_0 = schedule_list[i][1]
123-
time_1 = schedule_list[j][1]
124-
if today in [day_0, day_1] and in_between(now, time_0, time_1):
125-
return schedule_list[i][2]
126-
127-
return None
128-
129-
13090
def power_data_local_format(
13191
attrs: dict[str, str], key_string: str, val: str
13292
) -> float | int | bool:
@@ -688,16 +648,16 @@ def _control_state(self, loc_id: str) -> str | bool:
688648

689649
def _presets_legacy(self) -> dict[str, list[float]]:
690650
"""Helper-function for presets() - collect Presets for a legacy Anna."""
691-
preset_dictionary: dict[str, list[float]] = {}
651+
presets: dict[str, list[float]] = {}
692652
for directive in self._domain_objects.findall("rule/directives/when/then"):
693653
if directive is not None and "icon" in directive.keys():
694654
# Ensure list of heating_setpoint, cooling_setpoint
695-
preset_dictionary[directive.attrib["icon"]] = [
655+
presets[directive.attrib["icon"]] = [
696656
float(directive.attrib["temperature"]),
697657
0,
698658
]
699659

700-
return preset_dictionary
660+
return presets
701661

702662
def _presets(self, loc_id: str) -> dict[str, list[float]]:
703663
"""Collect Presets for a Thermostat based on location_id."""
@@ -731,35 +691,39 @@ def _presets(self, loc_id: str) -> dict[str, list[float]]:
731691
float(preset.get("cooling_setpoint")),
732692
]
733693

694+
# Adam does not show vacation preset anymore, issue #185
695+
if self.smile_name == "Adam":
696+
presets.pop("vacation")
697+
734698
return presets
735699

736-
def _rule_ids_by_name(self, name: str, loc_id: str) -> dict[str, str | None]:
700+
def _rule_ids_by_name(self, name: str, loc_id: str) -> dict[str, str]:
737701
"""Helper-function for _presets().
738702
Obtain the rule_id from the given name and and provide the location_id, when present.
739703
"""
740-
schedule_ids: dict[str, str | None] = {}
704+
schedule_ids: dict[str, str] = {}
741705
locator = f'./contexts/context/zone/location[@id="{loc_id}"]'
742706
for rule in self._domain_objects.findall(f'./rule[name="{name}"]'):
743707
if rule.find(locator) is not None:
744708
schedule_ids[rule.attrib["id"]] = loc_id
745709
else:
746-
schedule_ids[rule.attrib["id"]] = None
710+
schedule_ids[rule.attrib["id"]] = NONE
747711

748712
return schedule_ids
749713

750-
def _rule_ids_by_tag(self, tag: str, loc_id: str) -> dict[str, str | None]:
714+
def _rule_ids_by_tag(self, tag: str, loc_id: str) -> dict[str, str]:
751715
"""Helper-function for _presets(), _schedules() and _last_active_schedule().
752716
Obtain the rule_id from the given template_tag and provide the location_id, when present.
753717
"""
754-
schedule_ids: dict[str, str | None] = {}
718+
schedule_ids: dict[str, str] = {}
755719
locator1 = f'./template[@tag="{tag}"]'
756720
locator2 = f'./contexts/context/zone/location[@id="{loc_id}"]'
757721
for rule in self._domain_objects.findall("./rule"):
758722
if rule.find(locator1) is not None:
759723
if rule.find(locator2) is not None:
760724
schedule_ids[rule.attrib["id"]] = loc_id
761725
else:
762-
schedule_ids[rule.attrib["id"]] = None
726+
schedule_ids[rule.attrib["id"]] = NONE
763727

764728
return schedule_ids
765729

@@ -1072,8 +1036,8 @@ def _preset(self, loc_id: str) -> str | None:
10721036
return str(active_rule.attrib["icon"])
10731037

10741038
def _schedules_legacy(
1075-
self, avail: list[str], sched_temp: float | None, sel: str
1076-
) -> tuple[list[str], str, float | None, None]:
1039+
self, avail: list[str], sel: str
1040+
) -> tuple[list[str], str, None]:
10771041
"""Helper-function for _schedules().
10781042
Collect available schedules/schedules for the legacy thermostat.
10791043
"""
@@ -1096,25 +1060,21 @@ def _schedules_legacy(
10961060
if active:
10971061
sel = name
10981062

1099-
return avail, sel, sched_temp, None
1063+
return avail, sel, None
11001064

1101-
def _schedules(
1102-
self, location: str
1103-
) -> tuple[list[str], str, float | None, str | None]:
1065+
def _schedules(self, location: str) -> tuple[list[str], str, str | None]:
11041066
"""Helper-function for smile.py: _device_data_climate().
11051067
Obtain the available schedules/schedules. Adam: a schedule can be connected to more than one location.
11061068
NEW: when a location_id is present then the schedule is active. Valid for both Adam and non-legacy Anna.
11071069
"""
11081070
available: list[str] = [NONE]
11091071
last_used: str | None = None
1110-
rule_ids: dict[str, str | None] = {}
1111-
schedule_temperature: float | None = None
1072+
rule_ids: dict[str, str] = {}
11121073
selected = NONE
1113-
tmp_last_used: str | None = None
11141074

11151075
# Legacy Anna schedule, only one schedule allowed
11161076
if self._smile_legacy:
1117-
return self._schedules_legacy(available, schedule_temperature, selected)
1077+
return self._schedules_legacy(available, selected)
11181078

11191079
# Adam schedules, one schedule can be linked to various locations
11201080
# self._last_active contains the locations and the active schedule name per location, or None
@@ -1123,68 +1083,24 @@ def _schedules(
11231083

11241084
tag = "zone_preset_based_on_time_and_presence_with_override"
11251085
if not (rule_ids := self._rule_ids_by_tag(tag, location)):
1126-
return available, selected, schedule_temperature, None
1086+
return available, selected, None
11271087

1128-
schedules: dict[str, dict[str, float]] = {}
1088+
schedules: list[str] = []
11291089
for rule_id, loc_id in rule_ids.items():
11301090
name = self._domain_objects.find(f'./rule[@id="{rule_id}"]/name').text
1131-
schedule: dict[str, float] = {}
1132-
temp: dict[str, float] = {}
1133-
locator = f'./rule[@id="{rule_id}"]/directives'
1134-
directives = self._domain_objects.find(locator)
1135-
count = 0
1136-
for directive in directives:
1137-
entry = directive.find("then").attrib
1138-
keys, dummy = zip(*entry.items())
1139-
if str(keys[0]) == "preset":
1140-
if loc_id is None: # set to 0 when the schedule is not active
1141-
temp[directive.attrib["time"]] = float(0)
1142-
else:
1143-
temp[directive.attrib["time"]] = float(
1144-
self._presets(loc_id)[entry["preset"]][0]
1145-
)
1146-
if self.cooling_active:
1147-
temp[directive.attrib["time"]] = float(
1148-
self._presets(loc_id)[entry["preset"]][1]
1149-
)
1150-
else:
1151-
if "heating_setpoint" in entry:
1152-
temp[directive.attrib["time"]] = float(
1153-
entry["heating_setpoint"]
1154-
)
1155-
if self.cooling_active:
1156-
temp[directive.attrib["time"]] = float(
1157-
entry["cooling_setpoint"]
1158-
)
1159-
else:
1160-
temp[directive.attrib["time"]] = float(entry["setpoint"])
1161-
count += 1
1162-
1163-
if count > 1:
1164-
schedule = temp
1165-
else:
1166-
# Schedule with less than 2 items
1167-
LOGGER.debug("Invalid schedule, only one entry, ignoring.")
1168-
1169-
if schedule:
1170-
available.append(name)
1171-
if location == loc_id:
1172-
selected = name
1173-
self._last_active[location] = selected
1174-
schedules[name] = schedule
1091+
available.append(name)
1092+
if location == loc_id:
1093+
selected = name
1094+
self._last_active[location] = selected
1095+
schedules.append(name)
11751096

11761097
if schedules:
11771098
available.remove(NONE)
1178-
tmp_last_used = self._last_used_schedule(location, rule_ids)
1179-
if tmp_last_used in schedules:
1180-
last_used = tmp_last_used
1181-
schedule_temperature = schedules_schedule_temp(schedules, last_used)
1099+
last_used = self._last_used_schedule(location, schedules)
11821100

1183-
return available, selected, schedule_temperature, last_used
1101+
return available, selected, last_used
11841102

1185-
def _last_used_schedule(
1186-
self, loc_id: str, rule_ids: dict[str, str | None]
1187-
) -> str | None:
1103+
def _last_used_schedule(self, loc_id: str, schedules: list[str]) -> str | None:
11881104
"""Helper-function for smile.py: _device_data_climate().
11891105
Determine the last-used schedule based on the location or the modified date.
11901106
"""
@@ -1194,24 +1110,21 @@ def _last_used_schedule(
11941110
return last_used
11951111

11961112
# Alternatively, find last_used by finding the most recent modified_date
1197-
if not rule_ids:
1198-
return None # pragma: no cover
1113+
last_used = None
1114+
if not schedules:
1115+
return last_used # pragma: no cover
11991116

12001117
epoch = dt.datetime(1970, 1, 1, tzinfo=pytz.utc)
1201-
schedules: dict[str, float] = {}
1118+
schedules_dates: dict[str, float] = {}
12021119

1203-
for rule_id in rule_ids:
1204-
schedule_name = self._domain_objects.find(
1205-
f'./rule[@id="{rule_id}"]/name'
1206-
).text
1207-
schedule_date = self._domain_objects.find(
1208-
f'./rule[@id="{rule_id}"]/modified_date'
1209-
).text
1120+
for name in schedules:
1121+
result = self._domain_objects.find(f'./rule[name="{name}"]')
1122+
schedule_date = result.find("modified_date").text
12101123
schedule_time = parse(schedule_date)
1211-
schedules[schedule_name] = (schedule_time - epoch).total_seconds()
1124+
schedules_dates[name] = (schedule_time - epoch).total_seconds()
12121125

12131126
if schedules:
1214-
last_used = sorted(schedules.items(), key=lambda kv: kv[1])[-1][0]
1127+
last_used = sorted(schedules_dates.items(), key=lambda kv: kv[1])[-1][0]
12151128

12161129
return last_used
12171130

0 commit comments

Comments
 (0)