Skip to content

Commit 0da50f9

Browse files
authored
Merge pull request #244 from plugwise/pw_0_29_10
Various bugfixes 2
2 parents 900e3a9 + a86ac78 commit 0da50f9

File tree

13 files changed

+1038
-228
lines changed

13 files changed

+1038
-228
lines changed

CHANGELOG.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
# Changelog
22

3-
# v0.26.9: Hide cooling-related switch, binary_sensors when there is no cooling present
3+
# v0.25.10: Thermostats: more improvements
4+
- Anna + Elga: hide cooling_enable switch, (hardware-)switch is on Elga, not in Plugwise App
5+
- Adam: improve collecting regulation_mode-related data. Fix for https://github.com/plugwise/python-plugwise/issues/240
6+
- Anna: remove device availability, fix for https://github.com/home-assistant/core/issues/81716
7+
- Anna + OnOff device: fix incorrect heating-state, fix for https://github.com/home-assistant/core/issues/81839
8+
- Improve handling of xml-data missing, raise exception with warning. Solution for https://github.com/home-assistant/core/issues/81672
9+
- Improve handling of empty schedule, fix for https://github.com/plugwise/python-plugwise/issues/241
10+
11+
# v0.25.9: Adam: hide cooling-related switch, binary_sensors when there is no cooling present
412
- This fixes the unexpected appearance of new entities after the Adam 3.7.1 firmware-update
5-
- Properly handle an empty schedule, should fix #313
13+
- Properly handle an empty schedule, should fix https://github.com/plugwise/plugwise-beta/issues/313
614

715
# v0.25.8: Make collection of toggle-data future-proof
816

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.25.9"
3+
__version__ = "0.25.10"
44

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

plugwise/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,6 @@
471471
"electricity_consumed": UOM(POWER_WATT),
472472
"electricity_produced": UOM(POWER_WATT),
473473
"relay": UOM(NONE),
474-
"regulation_mode": UOM(NONE),
475474
}
476475

477476
# Heater Central related measurements

plugwise/helper.py

Lines changed: 167 additions & 117 deletions
Large diffs are not rendered by default.

plugwise/smile.py

Lines changed: 54 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from __future__ import annotations
55

66
import aiohttp
7-
from dateutil.parser import parse
87
from defusedxml import ElementTree as etree
98

109
# Dict as class
@@ -46,7 +45,7 @@
4645
ResponseError,
4746
UnsupportedDeviceError,
4847
)
49-
from .helper import SmileComm, SmileHelper, update_helper
48+
from .helper import SmileComm, SmileHelper, _find, _findall, update_helper
5049

5150

5251
class SmileData(SmileHelper):
@@ -130,11 +129,11 @@ def get_all_devices(self) -> None:
130129
# Start by determining the system capabilities:
131130
# Find the connected heating/cooling device (heater_central), e.g. heat-pump or gas-fired heater
132131
if self.smile_type == "thermostat":
133-
onoff_boiler: etree = self._domain_objects.find(
134-
"./module/protocols/onoff_boiler"
132+
onoff_boiler: etree = _find(
133+
self._domain_objects, "./module/protocols/onoff_boiler"
135134
)
136-
open_therm_boiler: etree = self._domain_objects.find(
137-
"./module/protocols/open_therm_boiler"
135+
open_therm_boiler: etree = _find(
136+
self._domain_objects, "./module/protocols/open_therm_boiler"
138137
)
139138
self._on_off_device = onoff_boiler is not None
140139
self._opentherm_device = open_therm_boiler is not None
@@ -144,11 +143,12 @@ def get_all_devices(self) -> None:
144143
locator_2 = "./gateway/features/elga_support"
145144
search = self._domain_objects
146145
self._cooling_present = False
147-
if search.find(locator_1) is not None:
146+
if _find(search, locator_1) is not None:
148147
self._cooling_present = True
149148
# Alternative method for the Anna with Elga
150-
elif search.find(locator_2) is not None:
149+
elif _find(search, locator_2) is not None:
151150
self._cooling_present = True
151+
self._elga = True
152152

153153
# Gather all the device and initial data
154154
self._scan_thermostats()
@@ -267,23 +267,6 @@ def _check_availability(
267267
if "P1 does not seem to be connected to a smart meter" in msg:
268268
device_data["available"] = False
269269

270-
# Anna thermostat
271-
if "modified" in device_data:
272-
time_now: str | None = None
273-
if (
274-
time_now := self._domain_objects.find("./gateway/time").text
275-
) is not None:
276-
interval = (
277-
parse(time_now) - parse(device_data["modified"])
278-
).total_seconds()
279-
if interval > 0:
280-
if details["dev_class"] == "thermostat":
281-
device_data["available"] = False
282-
if interval < 90:
283-
device_data["available"] = True
284-
285-
device_data.pop("modified")
286-
287270
def _get_device_data(self, dev_id: str) -> DeviceData:
288271
"""Helper-function for _all_device_data() and async_update().
289272
Provide device-data, based on Location ID (= dev_id), from APPLIANCES.
@@ -368,12 +351,12 @@ def __init__(
368351
async def connect(self) -> bool:
369352
"""Connect to Plugwise device and determine its name, type and version."""
370353
result = await self._request(DOMAIN_OBJECTS)
371-
vendor_names: list[etree] = result.findall("./module/vendor_name")
372-
vendor_models: list[etree] = result.findall("./module/vendor_model")
354+
vendor_names: list[etree] = _findall(result, "./module/vendor_name")
355+
vendor_models: list[etree] = _findall(result, "./module/vendor_model")
373356
# Work-around for Stretch fv 2.7.18
374357
if not vendor_names:
375358
result = await self._request(MODULES)
376-
vendor_names = result.findall("./module/vendor_name")
359+
vendor_names = _findall(result, "./module/vendor_name")
377360

378361
names: list[str] = []
379362
models: list[str] = []
@@ -382,7 +365,7 @@ async def connect(self) -> bool:
382365
for model in vendor_models:
383366
models.append(model.text)
384367

385-
dsmrmain = result.find("./module/protocols/dsmrmain")
368+
dsmrmain = _find(result, "./module/protocols/dsmrmain")
386369
if "Plugwise" not in names and dsmrmain is None: # pragma: no cover
387370
LOGGER.error(
388371
"Connected but expected text not returned, we got %s. Please create \
@@ -409,40 +392,42 @@ async def connect(self) -> bool:
409392
async def _smile_detect_legacy(self, result: etree, dsmrmain: etree) -> str:
410393
"""Helper-function for _smile_detect()."""
411394
# Stretch: find the MAC of the zigbee master_controller (= Stick)
412-
if network := result.find("./module/protocols/master_controller"):
413-
self.smile_zigbee_mac_address = network.find("mac_address").text
395+
if network := _find(result, "./module/protocols/master_controller"):
396+
self.smile_zigbee_mac_address = _find(network, "mac_address").text
414397
# Find the active MAC in case there is an orphaned Stick
415-
if zb_networks := result.findall("./network"):
398+
if zb_networks := _findall(result, "./network"):
416399
for zb_network in zb_networks:
417-
if zb_network.find("./nodes/network_router"):
418-
network = zb_network.find("./master_controller")
419-
self.smile_zigbee_mac_address = network.find("mac_address").text
400+
if _find(zb_network, "./nodes/network_router"):
401+
network = _find(zb_network, "./master_controller")
402+
self.smile_zigbee_mac_address = _find(network, "mac_address").text
420403

421404
# Assume legacy
422405
self._smile_legacy = True
423406
# Try if it is a legacy Anna, assuming appliance thermostat,
424407
# fake insert version assuming Anna, couldn't find another way to identify as legacy Anna
425408
self.smile_fw_version = "1.8.0"
426409
model = "smile_thermo"
427-
if result.find('./appliance[type="thermostat"]') is None:
410+
if _find(result, './appliance[type="thermostat"]') is None:
428411
# It's a P1 legacy:
429412
if dsmrmain is not None:
430413
self._status = await self._request(STATUS)
431-
self.smile_fw_version = self._status.find("./system/version").text
432-
model = self._status.find("./system/product").text
433-
self.smile_hostname = self._status.find("./network/hostname").text
434-
self.smile_mac_address = self._status.find("./network/mac_address").text
414+
self.smile_fw_version = _find(self._status, "./system/version").text
415+
model = _find(self._status, "./system/product").text
416+
self.smile_hostname = _find(self._status, "./network/hostname").text
417+
self.smile_mac_address = _find(
418+
self._status, "./network/mac_address"
419+
).text
435420

436421
# Or a legacy Stretch:
437422
elif network is not None:
438423
self._system = await self._request(SYSTEM)
439-
self.smile_fw_version = self._system.find("./gateway/firmware").text
440-
model = self._system.find("./gateway/product").text
441-
self.smile_hostname = self._system.find("./gateway/hostname").text
424+
self.smile_fw_version = _find(self._system, "./gateway/firmware").text
425+
model = _find(self._system, "./gateway/product").text
426+
self.smile_hostname = _find(self._system, "./gateway/hostname").text
442427
# If wlan0 contains data it's active, so eth0 should be checked last
443428
for network in ("wlan0", "eth0"):
444429
locator = f"./{network}/mac"
445-
if (net_locator := self._system.find(locator)) is not None:
430+
if (net_locator := _find(self._system, locator)) is not None:
446431
self.smile_mac_address = net_locator.text
447432

448433
else: # pragma: no cover
@@ -460,12 +445,12 @@ async def _smile_detect(self, result: etree, dsmrmain: etree) -> None:
460445
Detect which type of Smile is connected.
461446
"""
462447
model: str | None = None
463-
if (gateway := result.find("./gateway")) is not None:
464-
model = gateway.find("vendor_model").text
465-
self.smile_fw_version = gateway.find("firmware_version").text
466-
self.smile_hw_version = gateway.find("hardware_version").text
467-
self.smile_hostname = gateway.find("hostname").text
468-
self.smile_mac_address = gateway.find("mac_address").text
448+
if (gateway := _find(result, "./gateway")) is not None:
449+
model = _find(gateway, "vendor_model").text
450+
self.smile_fw_version = _find(gateway, "firmware_version").text
451+
self.smile_hw_version = _find(gateway, "hardware_version").text
452+
self.smile_hostname = _find(gateway, "hostname").text
453+
self.smile_mac_address = _find(gateway, "mac_address").text
469454
else:
470455
model = await self._smile_detect_legacy(result, dsmrmain)
471456

@@ -520,11 +505,11 @@ async def _update_domain_objects(self) -> None:
520505

521506
# If Plugwise notifications present:
522507
self._notifications = {}
523-
for notification in self._domain_objects.findall("./notification"):
508+
for notification in _findall(self._domain_objects, "./notification"):
524509
try:
525510
msg_id = notification.attrib["id"]
526-
msg_type = notification.find("type").text
527-
msg = notification.find("message").text
511+
msg_type = _find(notification, "type").text
512+
msg = _find(notification, "message").text
528513
self._notifications.update({msg_id: {msg_type: msg}})
529514
LOGGER.debug("Plugwise notifications: %s", self._notifications)
530515
except AttributeError: # pragma: no cover
@@ -580,8 +565,8 @@ async def _set_schedule_state_legacy(
580565
) -> None:
581566
"""Helper-function for set_schedule_state()."""
582567
schedule_rule_id: str | None = None
583-
for rule in self._domain_objects.findall("rule"):
584-
if rule.find("name").text == name:
568+
for rule in _findall(self._domain_objects, "rule"):
569+
if _find(rule, "name").text == name:
585570
schedule_rule_id = rule.attrib["id"]
586571

587572
if schedule_rule_id is None:
@@ -595,7 +580,7 @@ async def _set_schedule_state_legacy(
595580
return
596581

597582
locator = f'.//*[@id="{schedule_rule_id}"]/template'
598-
for rule in self._domain_objects.findall(locator):
583+
for rule in _findall(self._domain_objects, locator):
599584
template_id = rule.attrib["id"]
600585

601586
uri = f"{RULES};id={schedule_rule_id}"
@@ -643,13 +628,13 @@ async def set_schedule_state(
643628
)
644629
if self.smile_name != "Adam":
645630
locator = f'.//*[@id="{schedule_rule_id}"]/template'
646-
template_id = self._domain_objects.find(locator).attrib["id"]
631+
template_id = _find(self._domain_objects, locator).attrib["id"]
647632
template = f'<template id="{template_id}" />'
648633

649634
locator = f'.//*[@id="{schedule_rule_id}"]/contexts'
650-
contexts = self._domain_objects.find(locator)
635+
contexts = _find(self._domain_objects, locator)
651636
locator = f'.//*[@id="{loc_id}"].../...'
652-
if (subject := contexts.find(locator)) is None:
637+
if (subject := _find(contexts, locator)) is None:
653638
subject = f'<context><zone><location id="{loc_id}" /></zone></context>'
654639
subject = etree.fromstring(subject)
655640

@@ -672,7 +657,7 @@ async def set_schedule_state(
672657
async def _set_preset_legacy(self, preset: str) -> None:
673658
"""Set the given Preset on the relevant Thermostat - from DOMAIN_OBJECTS."""
674659
locator = f'rule/directives/when/then[@icon="{preset}"].../.../...'
675-
rule = self._domain_objects.find(locator)
660+
rule = _find(self._domain_objects, locator)
676661
data = f'<rules><rule id="{rule.attrib["id"]}"><active>true</active></rule></rules>'
677662

678663
await self._request(RULES, method="put", data=data)
@@ -688,9 +673,9 @@ async def set_preset(self, loc_id: str, preset: str) -> None:
688673
await self._set_preset_legacy(preset)
689674
return
690675

691-
current_location = self._locations.find(f'location[@id="{loc_id}"]')
692-
location_name = current_location.find("name").text
693-
location_type = current_location.find("type").text
676+
current_location = _find(self._locations, f'location[@id="{loc_id}"]')
677+
location_name = _find(current_location, "name").text
678+
location_type = _find(current_location, "type").text
694679

695680
uri = f"{LOCATIONS};id={loc_id}"
696681
data = (
@@ -732,9 +717,9 @@ async def set_number_setpoint(self, key: str, temperature: float) -> None:
732717
temp = str(temperature)
733718
thermostat_id: str | None = None
734719
locator = f'appliance[@id="{self._heater_id}"]/actuator_functionalities/thermostat_functionality'
735-
if th_func_list := self._appliances.findall(locator):
720+
if th_func_list := _findall(self._appliances, locator):
736721
for th_func in th_func_list:
737-
if th_func.find("type").text == key:
722+
if _find(th_func, "type").text == key:
738723
thermostat_id = th_func.attrib["id"]
739724

740725
if thermostat_id is None:
@@ -752,7 +737,7 @@ async def _set_groupswitch_member_state(
752737
"""
753738
for member in members:
754739
locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}'
755-
switch_id = self._appliances.find(locator).attrib["id"]
740+
switch_id = _find(self._appliances, locator).attrib["id"]
756741
uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}"
757742
if self._stretch_v2:
758743
uri = f"{APPLIANCES};id={member}/{switch.device}"
@@ -791,9 +776,9 @@ async def set_switch_state(
791776
return await self._set_groupswitch_member_state(members, state, switch)
792777

793778
locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}'
794-
found: list[etree] = self._appliances.findall(locator)
779+
found: list[etree] = _findall(self._appliances, locator)
795780
for item in found:
796-
if (sw_type := item.find("type")) is not None:
781+
if (sw_type := _find(item, "type")) is not None:
797782
if sw_type.text == switch.act_type:
798783
switch_id = item.attrib["id"]
799784
else:
@@ -810,7 +795,7 @@ async def set_switch_state(
810795
f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock'
811796
)
812797
# Don't bother switching a relay when the corresponding lock-state is true
813-
if self._appliances.find(locator).text == "true":
798+
if _find(self._appliances, locator).text == "true":
814799
raise PlugwiseError("Plugwise: the locked Relay was not switched.")
815800

816801
await self._request(uri, method="put", data=data)

0 commit comments

Comments
 (0)