Skip to content

Commit ff138aa

Browse files
authored
Merge pull request #459 from plugwise/driessens_2
Add detection&removal of orphaned heater_central - 2nd try
2 parents de852c3 + bf17672 commit ff138aa

File tree

13 files changed

+2855
-29
lines changed

13 files changed

+2855
-29
lines changed

CHANGELOG.md

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

3+
## v0.35.3
4+
5+
- Working solution for [Core Issue #104433](https://github.com/home-assistant/core/issues/104433)
6+
37
## v0.35.2
48

59
- Add detection & removal of orphaned heater_central.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
{
2+
"devices": {
3+
"5c118b1842e943c0a5b6ef88a60bb17a": {
4+
"binary_sensors": {
5+
"plugwise_notification": false
6+
},
7+
"dev_class": "gateway",
8+
"firmware": "4.4.1",
9+
"hardware": "AME Smile 2.0 board",
10+
"location": "82c15f65c9bf44c592d69e16139355e3",
11+
"mac_address": "D40FB2011556",
12+
"model": "Gateway",
13+
"name": "Smile Anna",
14+
"sensors": {
15+
"outdoor_temperature": 6.81
16+
},
17+
"vendor": "Plugwise"
18+
},
19+
"9fb768d699e44c7fb5cc50309dc4e7d4": {
20+
"active_preset": "home",
21+
"available_schedules": [
22+
"Verwarmen@9-23u",
23+
"VAKANTIE (winter)",
24+
"VERWARMEN",
25+
"KOELEN",
26+
"off"
27+
],
28+
"dev_class": "thermostat",
29+
"firmware": "2018-02-08T11:15:53+01:00",
30+
"hardware": "6539-1301-5002",
31+
"location": "fa70e08550c94de3a34feb27ecf31421",
32+
"mode": "auto",
33+
"model": "ThermoTouch",
34+
"name": "Anna",
35+
"preset_modes": ["no_frost", "asleep", "vacation", "away", "home"],
36+
"select_schedule": "Verwarmen@9-23u",
37+
"sensors": {
38+
"illuminance": 5.5,
39+
"setpoint_high": 30.0,
40+
"setpoint_low": 20.0,
41+
"temperature": 21.2
42+
},
43+
"temperature_offset": {
44+
"lower_bound": -2.0,
45+
"resolution": 0.1,
46+
"setpoint": 0.0,
47+
"upper_bound": 2.0
48+
},
49+
"thermostat": {
50+
"lower_bound": 4.0,
51+
"resolution": 0.1,
52+
"setpoint_high": 30.0,
53+
"setpoint_low": 20.0,
54+
"upper_bound": 30.0
55+
},
56+
"vendor": "Plugwise"
57+
},
58+
"a449cbc334ae4a5bb7f89064984b2906": {
59+
"available": true,
60+
"binary_sensors": {
61+
"cooling_state": false,
62+
"dhw_state": false,
63+
"flame_state": false,
64+
"heating_state": false
65+
},
66+
"dev_class": "heater_central",
67+
"dhw_modes": ["comfort", "eco", "off", "boost", "auto"],
68+
"location": "82c15f65c9bf44c592d69e16139355e3",
69+
"max_dhw_temperature": {
70+
"lower_bound": 35.0,
71+
"resolution": 0.01,
72+
"setpoint": 53.0,
73+
"upper_bound": 60.0
74+
},
75+
"maximum_boiler_temperature": {
76+
"lower_bound": 25.0,
77+
"resolution": 0.01,
78+
"setpoint": 45.0,
79+
"upper_bound": 45.0
80+
},
81+
"model": "173",
82+
"name": "OpenTherm",
83+
"select_dhw_mode": "auto",
84+
"sensors": {
85+
"dhw_temperature": 49.5,
86+
"intended_boiler_temperature": 0.0,
87+
"modulation_level": 0.0,
88+
"outdoor_air_temperature": 7.63,
89+
"return_temperature": 23.0,
90+
"water_temperature": 23.3
91+
},
92+
"switches": {
93+
"cooling_ena_switch": false,
94+
"dhw_cm_switch": true
95+
},
96+
"vendor": "Atlantic"
97+
}
98+
},
99+
"gateway": {
100+
"cooling_present": true,
101+
"gateway_id": "5c118b1842e943c0a5b6ef88a60bb17a",
102+
"heater_id": "a449cbc334ae4a5bb7f89064984b2906",
103+
"item_count": 63,
104+
"notifications": {},
105+
"smile_name": "Smile Anna"
106+
}
107+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[
2+
"5c118b1842e943c0a5b6ef88a60bb17a",
3+
"a449cbc334ae4a5bb7f89064984b2906",
4+
"9fb768d699e44c7fb5cc50309dc4e7d4"
5+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

plugwise/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,6 @@ class DeviceData(TypedDict, total=False):
488488
"""The Device Data class, covering the collected and ordered output-data per device."""
489489

490490
# Appliance base data
491-
has_actuators: bool
492491
dev_class: str
493492
firmware: str | None
494493
hardware: str

plugwise/helper.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,9 @@ def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
456456
if not self._opentherm_device and not self._on_off_device:
457457
return None
458458

459-
self._heater_id = appliance.attrib["id"]
459+
# Find the valid heater_central
460+
self._heater_id = self._check_heater_central()
461+
460462
# Info for On-Off device
461463
if self._on_off_device:
462464
appl.name = "OnOff"
@@ -498,6 +500,34 @@ def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
498500

499501
return appl
500502

503+
def _check_heater_central(self) -> str:
504+
"""Find the valid heater_central, helper-function for _appliance_info_finder().
505+
506+
Solution for Core Issue #104433,
507+
for a system that has two heater_central appliances.
508+
"""
509+
locator = "./appliance[type='heater_central']"
510+
hc_count = 0
511+
hc_list: list[dict[str, bool]] = []
512+
for heater_central in self._appliances.findall(locator):
513+
hc_count += 1
514+
hc_id: str = heater_central.attrib["id"]
515+
has_actuators: bool = (
516+
heater_central.find("actuator_functionalities/") is not None
517+
)
518+
hc_list.append({hc_id: has_actuators})
519+
520+
heater_central_id = list(hc_list[0].keys())[0]
521+
if hc_count > 1:
522+
for item in hc_list:
523+
for key, value in item.items():
524+
if value:
525+
heater_central_id = key
526+
# Stop when a valid id is found
527+
break
528+
529+
return heater_central_id
530+
501531
def _p1_smartmeter_info_finder(self, appl: Munch) -> None:
502532
"""Collect P1 DSMR Smartmeter info."""
503533
loc_id = next(iter(self._loc_data.keys()))
@@ -570,21 +600,15 @@ def _all_appliances(self) -> None:
570600
# Legacy P1 has no more devices
571601
return
572602

573-
hc_count = 0
574603
for appliance in self._appliances.findall("./appliance"):
575604
appl = Munch()
576605
appl.pwclass = appliance.find("type").text
577-
# Count amount of heater_central's
578-
if appl.pwclass == "heater_central":
579-
hc_count += 1
580-
# Mark heater_central and thermostat that don't have actuator_functionalities,
581-
# could be an orphaned device (Core #81712, #104433)
582-
appl.has_actuators = True
606+
# Skip thermostats that have this key, should be an orphaned device (Core #81712)
583607
if (
584-
appl.pwclass in ["heater_central", "thermostat"]
608+
appl.pwclass == "thermostat"
585609
and appliance.find("actuator_functionalities/") is None
586610
):
587-
appl.has_actuators = False
611+
continue
588612

589613
appl.location = None
590614
if (appl_loc := appliance.find("location")) is not None:
@@ -610,6 +634,10 @@ def _all_appliances(self) -> None:
610634
if not (appl := self._appliance_info_finder(appliance, appl)):
611635
continue
612636

637+
# Skip orphaned heater_central (Core Issue #104433)
638+
if appl.pwclass == "heater_central" and appl.dev_id != self._heater_id:
639+
continue
640+
613641
# P1: for gateway and smartmeter switch device_id - part 1
614642
# This is done to avoid breakage in HA Core
615643
if appl.pwclass == "gateway" and self.smile_type == "power":
@@ -628,7 +656,6 @@ def _all_appliances(self) -> None:
628656
for key, value in {
629657
"firmware": appl.firmware,
630658
"hardware": appl.hardware,
631-
"has_actuators": appl.has_actuators,
632659
"location": appl.location,
633660
"mac_address": appl.mac,
634661
"model": appl.model,
@@ -641,22 +668,6 @@ def _all_appliances(self) -> None:
641668
self.gw_devices[appl.dev_id][appl_key] = value
642669
self._count += 1
643670

644-
# Remove thermostat with empty actuator_functionalities (Core #81712), remove heater_central
645-
# with empty actuator_functionalities but only when there are more than one (Core #104433).
646-
for dev_id, device in dict(self.gw_devices).items():
647-
if device["dev_class"] == "thermostat" or (
648-
device["dev_class"] == "heater_central" and hc_count > 1
649-
):
650-
if not self.gw_devices[dev_id]["has_actuators"]:
651-
self._count -= len(self.gw_devices[dev_id])
652-
self.gw_devices.pop(dev_id)
653-
else:
654-
self.gw_devices[dev_id].pop("has_actuators")
655-
self._count -= 1
656-
elif "has_actuators" in self.gw_devices[dev_id]:
657-
self.gw_devices[dev_id].pop("has_actuators")
658-
self._count -= 1
659-
660671
# For non-legacy P1 collect the connected SmartMeter info
661672
if self.smile_type == "power":
662673
self._p1_smartmeter_info_finder(appl)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "plugwise"
7-
version = "0.35.2"
7+
version = "0.35.3"
88
license = {file = "LICENSE"}
99
description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3."
1010
readme = "README.md"

tests/test_smile.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,115 @@ async def test_connect_anna_loria_cooling_active(self):
48184818
await smile.close_connection()
48194819
await self.disconnect(server, client)
48204820

4821+
@pytest.mark.asyncio
4822+
async def test_connect_anna_loria_driessens(self):
4823+
"""Test an Anna with a Loria in heating mode - state idle."""
4824+
testdata = {
4825+
"5c118b1842e943c0a5b6ef88a60bb17a": {
4826+
"binary_sensors": {"plugwise_notification": False},
4827+
"dev_class": "gateway",
4828+
"firmware": "4.4.1",
4829+
"hardware": "AME Smile 2.0 board",
4830+
"location": "82c15f65c9bf44c592d69e16139355e3",
4831+
"mac_address": "D40FB2011556",
4832+
"model": "Gateway",
4833+
"name": "Smile Anna",
4834+
"sensors": {"outdoor_temperature": 6.81},
4835+
"vendor": "Plugwise",
4836+
},
4837+
"9fb768d699e44c7fb5cc50309dc4e7d4": {
4838+
"active_preset": "home",
4839+
"available_schedules": [
4840+
"Verwarmen@9-23u",
4841+
"VAKANTIE (winter)",
4842+
"VERWARMEN",
4843+
"KOELEN",
4844+
"off",
4845+
],
4846+
"dev_class": "thermostat",
4847+
"firmware": "2018-02-08T11:15:53+01:00",
4848+
"hardware": "6539-1301-5002",
4849+
"location": "fa70e08550c94de3a34feb27ecf31421",
4850+
"mode": "auto",
4851+
"model": "ThermoTouch",
4852+
"name": "Anna",
4853+
"preset_modes": ["no_frost", "asleep", "vacation", "away", "home"],
4854+
"select_schedule": "Verwarmen@9-23u",
4855+
"sensors": {
4856+
"illuminance": 5.5,
4857+
"setpoint_high": 30.0,
4858+
"setpoint_low": 20.0,
4859+
"temperature": 21.2,
4860+
},
4861+
"temperature_offset": {
4862+
"lower_bound": -2.0,
4863+
"resolution": 0.1,
4864+
"setpoint": 0.0,
4865+
"upper_bound": 2.0,
4866+
},
4867+
"thermostat": {
4868+
"lower_bound": 4.0,
4869+
"resolution": 0.1,
4870+
"setpoint_high": 30.0,
4871+
"setpoint_low": 20.0,
4872+
"upper_bound": 30.0,
4873+
},
4874+
"vendor": "Plugwise",
4875+
},
4876+
"a449cbc334ae4a5bb7f89064984b2906": {
4877+
"available": True,
4878+
"binary_sensors": {
4879+
"cooling_state": False,
4880+
"dhw_state": False,
4881+
"flame_state": False,
4882+
"heating_state": False,
4883+
},
4884+
"dev_class": "heater_central",
4885+
"dhw_modes": ["comfort", "eco", "off", "boost", "auto"],
4886+
"location": "82c15f65c9bf44c592d69e16139355e3",
4887+
"max_dhw_temperature": {
4888+
"lower_bound": 35.0,
4889+
"resolution": 0.01,
4890+
"setpoint": 53.0,
4891+
"upper_bound": 60.0,
4892+
},
4893+
"maximum_boiler_temperature": {
4894+
"lower_bound": 25.0,
4895+
"resolution": 0.01,
4896+
"setpoint": 45.0,
4897+
"upper_bound": 45.0,
4898+
},
4899+
"model": "173",
4900+
"name": "OpenTherm",
4901+
"select_dhw_mode": "auto",
4902+
"sensors": {
4903+
"dhw_temperature": 49.5,
4904+
"intended_boiler_temperature": 0.0,
4905+
"modulation_level": 0.0,
4906+
"outdoor_air_temperature": 7.63,
4907+
"return_temperature": 23.0,
4908+
"water_temperature": 23.3,
4909+
},
4910+
"switches": {"cooling_ena_switch": False, "dhw_cm_switch": True},
4911+
"vendor": "Atlantic",
4912+
},
4913+
}
4914+
self.smile_setup = "anna_loria_driessens"
4915+
server, smile, client = await self.connect_wrapper()
4916+
assert smile.smile_hostname == "smile000000"
4917+
4918+
_LOGGER.info("Basics:")
4919+
_LOGGER.info(" # Assert type = thermostat")
4920+
assert smile.smile_type == "thermostat"
4921+
4922+
await self.device_test(smile, "2022-05-16 00:00:01", testdata)
4923+
assert smile.device_items == 63
4924+
assert smile._cooling_present
4925+
assert not smile._cooling_enabled
4926+
4927+
await smile.close_connection()
4928+
await self.disconnect(server, client)
4929+
48214930
@pytest.mark.asyncio
48224931
async def test_connect_stretch_v31(self):
48234932
"""Test a legacy Stretch with firmware 3.1 setup."""

userdata/anna_loria_driessens/@dries-driessens

Whitespace-only changes.

0 commit comments

Comments
 (0)