Skip to content

Commit d14bc2a

Browse files
authored
Merge pull request #171 from plugwise/fix_set_schedule
Update set_schedule_state()
2 parents 65c0fbb + 0d5c276 commit d14bc2a

File tree

17 files changed

+165
-11138
lines changed

17 files changed

+165
-11138
lines changed

CHANGELOG.md

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

3+
# v0.17.2 - Smile Adam: more bugfixes, improvementds
4+
- Bugfix: update set_schedule_state() to handle multi thermostat scenario's
5+
- Improve tracking of the last used schedule, needed due to the changes in set_schedule_state()
6+
- Improve invalid schedule handling
7+
- Update & add related testcases
8+
- Naming cleanup
9+
310
# v0.17.1 - Smile: bugfix for https://github.com/home-assistant/core/issues/68621
411

512
# v0.17.0 - Smile: add more outputs

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.17.1"
3+
__version__ = "0.17.2"
44

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

plugwise/helper.py

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -91,39 +91,33 @@ def check_model(name: str, v_name: str) -> str:
9191
return name
9292

9393

94-
def schemas_schedule_temp(schedules: dict[str, Any], name: str) -> float | None:
95-
"""Helper-function for schemas().
96-
Obtain the schedule temperature of the schema/schedule.
94+
def schedules_schedule_temp(schedules: dict[str, Any], name: str) -> float | None:
95+
"""Helper-function for schedules().
96+
Obtain the schedule temperature of the schedule/schedule.
9797
"""
9898
if name == "None":
9999
return # pragma: no cover
100100

101-
schema_list: list[list[int, dt.time, float]] | None = []
101+
schedule_list: list[list[int, dt.time, float]] | None = []
102102
for period, temp in schedules[name].items():
103103
tmp_list: list[int, dt.time, float] = []
104104
moment, dummy = period.split(",")
105105
moment = moment.replace("[", "").split(" ")
106106
day_nr = DAYS.get(moment[0], "None")
107107
start_time = dt.datetime.strptime(moment[1], "%H:%M").time()
108108
tmp_list.extend((day_nr, start_time, temp))
109-
schema_list.append(tmp_list)
110-
111-
length = len(schema_list)
112-
schema_list = sorted(schema_list)
113-
114-
# Schema with less than 2 items
115-
if length == 1:
116-
LOGGER.debug("Invalid schedule, only one entry, ignoring.")
117-
return
109+
schedule_list.append(tmp_list)
118110

111+
length = len(schedule_list)
112+
schedule_list = sorted(schedule_list)
119113
for i in range(length):
120114
j = (i + 1) % (length - 1)
121115
now = dt.datetime.now().time()
122116
today = dt.datetime.now().weekday()
123-
if today in [schema_list[i][0], schema_list[j][0]] and in_between(
124-
now, schema_list[i][1], schema_list[j][1]
117+
if today in [schedule_list[i][0], schedule_list[j][0]] and in_between(
118+
now, schedule_list[i][1], schedule_list[j][1]
125119
):
126-
return schema_list[i][2]
120+
return schedule_list[i][2]
127121

128122

129123
def types_finder(data: etree) -> set[str]:
@@ -771,31 +765,31 @@ def _rule_ids_by_name(self, name: str, loc_id: str) -> dict[str]:
771765
"""Helper-function for _presets().
772766
Obtain the rule_id from the given name and and provide the location_id, when present.
773767
"""
774-
schema_ids: dict[str] = {}
768+
schedule_ids: dict[str] = {}
775769
locator = f'./contexts/context/zone/location[@id="{loc_id}"]'
776770
for rule in self._domain_objects.findall(f'./rule[name="{name}"]'):
777771
if rule.find(locator) is not None:
778-
schema_ids[rule.attrib["id"]] = loc_id
772+
schedule_ids[rule.attrib["id"]] = loc_id
779773
else:
780-
schema_ids[rule.attrib["id"]] = None
774+
schedule_ids[rule.attrib["id"]] = None
781775

782-
return schema_ids
776+
return schedule_ids
783777

784778
def _rule_ids_by_tag(self, tag: str, loc_id: str) -> dict[str]:
785-
"""Helper-function for _presets(), _schemas() and _last_active_schema().
779+
"""Helper-function for _presets(), _schedules() and _last_active_schedule().
786780
Obtain the rule_id from the given template_tag and provide the location_id, when present.
787781
"""
788-
schema_ids: dict[str] = {}
782+
schedule_ids: dict[str] = {}
789783
locator1 = f'./template[@tag="{tag}"]'
790784
locator2 = f'./contexts/context/zone/location[@id="{loc_id}"]'
791785
for rule in self._domain_objects.findall("./rule"):
792786
if rule.find(locator1) is not None:
793787
if rule.find(locator2) is not None:
794-
schema_ids[rule.attrib["id"]] = loc_id
788+
schedule_ids[rule.attrib["id"]] = loc_id
795789
else:
796-
schema_ids[rule.attrib["id"]] = None
790+
schedule_ids[rule.attrib["id"]] = None
797791

798-
return schema_ids
792+
return schedule_ids
799793

800794
def _appliance_measurements(
801795
self, appliance: etree, data: dict[str, Any], measurements: dict[str, Any]
@@ -1127,18 +1121,18 @@ def _preset(self, loc_id: str) -> str | None:
11271121
return
11281122
return active_rule.attrib["icon"]
11291123

1130-
def _schemas_legacy(
1124+
def _schedules_legacy(
11311125
self, avail: list[str], sched_temp: str, sel: str
11321126
) -> tuple[str, ...]:
1133-
"""Helper-function for _schemas().
1134-
Collect available schemas/schedules for the legacy thermostat.
1127+
"""Helper-function for _schedules().
1128+
Collect available schedules/schedules for the legacy thermostat.
11351129
"""
11361130
name: str | None = None
1137-
schemas: dict[str] = {}
1131+
schedules: dict[str] = {}
11381132

11391133
search = self._domain_objects
1140-
for schema in search.findall("./rule"):
1141-
if rule_name := schema.find("name").text:
1134+
for schedule in search.findall("./rule"):
1135+
if rule_name := schedule.find("name").text:
11421136
if "preset" not in rule_name:
11431137
name = rule_name
11441138

@@ -1149,27 +1143,28 @@ def _schemas_legacy(
11491143
active = result.text == "on"
11501144

11511145
if name is not None:
1152-
schemas[name] = active
1146+
schedules[name] = active
11531147
avail = [name]
11541148
if active:
11551149
sel = name
11561150

11571151
return avail, sel, sched_temp, None
11581152

1159-
def _schemas(self, location: str) -> tuple[str, ...]:
1153+
def _schedules(self, location: str) -> tuple[str, ...]:
11601154
"""Helper-function for smile.py: _device_data_climate().
1161-
Obtain the available schemas/schedules. Adam: a schedule can be connected to more than one location.
1155+
Obtain the available schedules/schedules. Adam: a schedule can be connected to more than one location.
11621156
NEW: when a location_id is present then the schedule is active. Valid for both Adam and non-legacy Anna.
11631157
"""
11641158
available: list[str] = ["None"]
11651159
last_used: str | None = None
11661160
rule_ids: dict[str] = {}
11671161
schedule_temperature: str | None = None
11681162
selected = "None"
1163+
tmp_last_used: str | None = None
11691164

11701165
# Legacy Anna schedule, only one schedule allowed
11711166
if self._smile_legacy:
1172-
return self._schemas_legacy(available, schedule_temperature, selected)
1167+
return self._schedules_legacy(available, schedule_temperature, selected)
11731168

11741169
# Adam schedules, one schedule can be linked to various locations
11751170
# self._last_active contains the locations and the active schedule name per location, or None
@@ -1184,17 +1179,26 @@ def _schemas(self, location: str) -> tuple[str, ...]:
11841179
for rule_id, loc_id in rule_ids.items():
11851180
name = self._domain_objects.find(f'./rule[@id="{rule_id}"]/name').text
11861181
schedule: dict[str, float] = {}
1182+
temp: dict[str, float] = {}
11871183
locator = f'./rule[@id="{rule_id}"]/directives'
11881184
directives = self._domain_objects.find(locator)
1185+
count = 0
11891186
for directive in directives:
11901187
entry = directive.find("then").attrib
11911188
keys, dummy = zip(*entry.items())
11921189
if str(keys[0]) == "preset":
1193-
schedule[directive.attrib["time"]] = float(
1190+
temp[directive.attrib["time"]] = float(
11941191
self._presets(loc_id)[entry["preset"]][0]
11951192
)
11961193
else:
1197-
schedule[directive.attrib["time"]] = float(entry.get("setpoint"))
1194+
temp[directive.attrib["time"]] = float(entry.get("setpoint"))
1195+
count += 1
1196+
1197+
if count > 1:
1198+
schedule = temp
1199+
else:
1200+
# Schedule with less than 2 items
1201+
LOGGER.debug("Invalid schedule, only one entry, ignoring.")
11981202

11991203
if schedule:
12001204
available.append(name)
@@ -1205,8 +1209,10 @@ def _schemas(self, location: str) -> tuple[str, ...]:
12051209

12061210
if schedules:
12071211
available.remove("None")
1208-
last_used = self._last_used_schedule(location, rule_ids)
1209-
schedule_temperature = schemas_schedule_temp(schedules, last_used)
1212+
tmp_last_used = self._last_used_schedule(location, rule_ids)
1213+
if tmp_last_used in schedules:
1214+
last_used = tmp_last_used
1215+
schedule_temperature = schedules_schedule_temp(schedules, last_used)
12101216

12111217
return available, selected, schedule_temperature, last_used
12121218

@@ -1224,20 +1230,20 @@ def _last_used_schedule(self, loc_id: str, rule_ids: dict[str]) -> str | None:
12241230
return # pragma: no cover
12251231

12261232
epoch = dt.datetime(1970, 1, 1, tzinfo=pytz.utc)
1227-
schemas: dict[str] | None = {}
1233+
schedules: dict[str] | None = {}
12281234

12291235
for rule_id in rule_ids:
1230-
schema_name = self._domain_objects.find(
1236+
schedule_name = self._domain_objects.find(
12311237
f'./rule[@id="{rule_id}"]/name'
12321238
).text
1233-
schema_date = self._domain_objects.find(
1239+
schedule_date = self._domain_objects.find(
12341240
f'./rule[@id="{rule_id}"]/modified_date'
12351241
).text
1236-
schema_time = parse(schema_date)
1237-
schemas[schema_name] = (schema_time - epoch).total_seconds()
1242+
schedule_time = parse(schedule_date)
1243+
schedules[schedule_name] = (schedule_time - epoch).total_seconds()
12381244

1239-
if schemas:
1240-
last_used = sorted(schemas.items(), key=lambda kv: kv[1])[-1][0]
1245+
if schedules:
1246+
last_used = sorted(schedules.items(), key=lambda kv: kv[1])[-1][0]
12411247

12421248
return last_used
12431249

plugwise/smile.py

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,13 @@ def _device_data_climate(
162162
device_data["active_preset"] = self._preset(loc_id)
163163

164164
# Schedule
165-
avail_schemas, sel_schema, sched_setpoint, last_active = self._schemas(loc_id)
166-
device_data["available_schedules"] = avail_schemas
167-
device_data["selected_schedule"] = sel_schema
165+
avail_schedules, sel_schedule, sched_setpoint, last_active = self._schedules(
166+
loc_id
167+
)
168+
device_data["available_schedules"] = avail_schedules
169+
device_data["selected_schedule"] = sel_schedule
168170
if self._smile_legacy:
169-
device_data["last_used"] = "".join(map(str, avail_schemas))
171+
device_data["last_used"] = "".join(map(str, avail_schedules))
170172
else:
171173
device_data["last_used"] = last_active
172174
device_data["schedule_temperature"] = sched_setpoint
@@ -177,7 +179,7 @@ def _device_data_climate(
177179

178180
# Operation mode: auto, heat, cool
179181
device_data["mode"] = "auto"
180-
if sel_schema == "None":
182+
if sel_schedule == "None":
181183
device_data["mode"] = "heat"
182184
if self._heater_id is not None and self.cooling_active:
183185
device_data["mode"] = "cool"
@@ -474,25 +476,25 @@ async def async_update(self) -> dict[str, Any]:
474476

475477
async def _set_schedule_state_legacy(self, name: str, status: str) -> bool:
476478
"""Helper-function for set_schedule_state()."""
477-
schema_rule_id: str | None = None
479+
schedule_rule_id: str | None = None
478480
for rule in self._domain_objects.findall("rule"):
479481
if rule.find("name").text == name:
480-
schema_rule_id = rule.attrib["id"]
482+
schedule_rule_id = rule.attrib["id"]
481483

482-
if schema_rule_id is None:
484+
if schedule_rule_id is None:
483485
return False
484486

485487
state = "false"
486488
if status == "on":
487489
state = "true"
488-
locator = f'.//*[@id="{schema_rule_id}"]/template'
490+
locator = f'.//*[@id="{schedule_rule_id}"]/template'
489491
for rule in self._domain_objects.findall(locator):
490492
template_id = rule.attrib["id"]
491493

492-
uri = f"{RULES};id={schema_rule_id}"
494+
uri = f"{RULES};id={schedule_rule_id}"
493495
data = (
494496
"<rules><rule"
495-
f' id="{schema_rule_id}"><name><![CDATA[{name}]]></name><template'
497+
f' id="{schedule_rule_id}"><name><![CDATA[{name}]]></name><template'
496498
f' id="{template_id}" /><active>{state}</active></rule></rules>'
497499
)
498500

@@ -506,29 +508,41 @@ async def set_schedule_state(self, loc_id: str, name: str, state: str) -> bool:
506508
if self._smile_legacy:
507509
return await self._set_schedule_state_legacy(name, state)
508510

509-
schema_rule = self._rule_ids_by_name(name, loc_id)
510-
if not schema_rule or schema_rule is None:
511+
schedule_rule = self._rule_ids_by_name(name, loc_id)
512+
if not schedule_rule or schedule_rule is None:
511513
return False
512514

513-
schema_rule_id: str = next(iter(schema_rule))
514-
info = ""
515-
if state == "on":
516-
info = f'<context><zone><location id="{loc_id}" /></zone></context>'
515+
schedule_rule_id: str = next(iter(schedule_rule))
517516

518517
template = (
519518
'<template tag="zone_preset_based_on_time_and_presence_with_override" />'
520519
)
521520
if self.smile_name != "Adam":
522-
locator = f'.//*[@id="{schema_rule_id}"]/template'
521+
locator = f'.//*[@id="{schedule_rule_id}"]/template'
523522
template_id = self._domain_objects.find(locator).attrib["id"]
524523
template = f'<template id="{template_id}" />'
525524

526-
uri = f"{RULES};id={schema_rule_id}"
525+
locator = f'.//*[@id="{schedule_rule_id}"]/contexts'
526+
contexts = self._domain_objects.find(locator)
527+
locator = f'.//*[@id="{loc_id}"].../...'
528+
subject = contexts.find(locator)
529+
if subject is None:
530+
subject = f'<context><zone><location id="{loc_id}" /></zone></context>'
531+
subject = etree.fromstring(subject)
532+
533+
if state == "off":
534+
self._last_active[loc_id] = name
535+
contexts.remove(subject)
536+
if state == "on":
537+
contexts.append(subject)
538+
539+
contexts = etree.tostring(contexts, encoding="unicode").rstrip()
540+
541+
uri = f"{RULES};id={schedule_rule_id}"
527542
data = (
528-
f'<rules><rule id="{schema_rule_id}"><name><![CDATA[{name}]]></name>'
529-
f"{template}<contexts>{info}</contexts></rule></rules>"
543+
f'<rules><rule id="{schedule_rule_id}"><name><![CDATA[{name}]]></name>'
544+
f"{template}{contexts}</rule></rules>"
530545
)
531-
532546
await self._request(uri, method="put", data=data)
533547

534548
return True

0 commit comments

Comments
 (0)