Skip to content

Commit 5420246

Browse files
authored
Merge pull request #406 from plugwise/daily_full_update
Implement a daily full-update
2 parents e7d0fdc + 7cca0cc commit 5420246

File tree

11 files changed

+5402
-68
lines changed

11 files changed

+5402
-68
lines changed

CHANGELOG.md

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

3-
## Ongoing
3+
## v0.33.0 Bugfixes, implement daily full-update
44

5+
- New feature: implement a daily full-update (other part of solution for [HA Core issue #99372](https://github.com/home-assistant/core/issues/99372))
56
- Reorder device-dicts: gateway first, optionally heater_central second
67
- Improve handling of obsolete sensors (solution for [HA Core issue #100306](https://github.com/home-assistant/core/issues/100306)
78
- Improve handling of invalid actuator xml-data (partial solution for [HA Core issue #99372](https://github.com/home-assistant/core/issues/99372)

plugwise/__init__.py

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"""
55
from __future__ import annotations
66

7+
import datetime as dt
8+
79
import aiohttp
810
from defusedxml import ElementTree as etree
911

@@ -34,6 +36,7 @@
3436
ZONE_THERMOSTATS,
3537
ActuatorData,
3638
DeviceData,
39+
GatewayData,
3740
PlugwiseData,
3841
)
3942
from .exceptions import (
@@ -85,29 +88,46 @@ def update_for_cooling(self, device: DeviceData) -> DeviceData:
8588

8689
return device
8790

88-
def _all_device_data(self) -> None:
89-
"""Helper-function for get_all_devices().
91+
def _update_gw_devices(self) -> None:
92+
"""Helper-function for _all_device_data() and async_update().
9093
91-
Collect data for each device and add to self.gw_data and self.gw_devices.
94+
Collect data for each device and add to self.gw_devices.
9295
"""
9396
for device_id, device in self.gw_devices.items():
9497
data = self._get_device_data(device_id)
95-
device.update(data)
96-
# Add plugwise notification binary_sensor to the relevant gateway
97-
if device_id == self.gateway_id and (
98-
self._is_thermostat
99-
or (not self._smile_legacy and self.smile_type == "power")
98+
if (
99+
"binary_sensors" in device
100+
and "plugwise_notification" in device["binary_sensors"]
101+
) or (
102+
device_id == self.gateway_id
103+
and (
104+
self._is_thermostat
105+
or (self.smile_type == "power" and not self._smile_legacy)
106+
)
100107
):
101-
device["binary_sensors"]["plugwise_notification"] = False
108+
data["binary_sensors"]["plugwise_notification"] = bool(
109+
self._notifications
110+
)
111+
device.update(data)
102112

103113
# Update for cooling
104114
if device["dev_class"] in ZONE_THERMOSTATS:
105115
self.update_for_cooling(device)
106116

107117
remove_empty_platform_dicts(device)
108118

119+
def _all_device_data(self) -> None:
120+
"""Helper-function for get_all_devices().
121+
122+
Collect data for each device and add to self.gw_data and self.gw_devices.
123+
"""
124+
self._update_gw_devices()
109125
self.gw_data.update(
110-
{"smile_name": self.smile_name, "gateway_id": self.gateway_id}
126+
{
127+
"smile_name": self.smile_name,
128+
"gateway_id": self.gateway_id,
129+
"notifications": self._notifications,
130+
}
111131
)
112132
if self._is_thermostat:
113133
self.gw_data.update(
@@ -318,6 +338,7 @@ def __init__(
318338
SmileData.__init__(self)
319339

320340
self.smile_hostname: str | None = None
341+
self._previous_day_number: str = "0"
321342
self._target_smile: str | None = None
322343

323344
async def connect(self) -> bool:
@@ -473,15 +494,6 @@ async def _smile_detect(self, result: etree, dsmrmain: etree) -> None:
473494
if result.find(locator_2) is not None:
474495
self._elga = True
475496

476-
async def _full_update_device(self) -> None:
477-
"""Perform a first fetch of all XML data, needed for initialization."""
478-
await self._update_domain_objects()
479-
self._locations = await self._request(LOCATIONS)
480-
self._modules = await self._request(MODULES)
481-
# P1 legacy has no appliances
482-
if not (self.smile_type == "power" and self._smile_legacy):
483-
self._appliances = await self._request(APPLIANCES)
484-
485497
async def _update_domain_objects(self) -> None:
486498
"""Helper-function for smile.py: full_update_device() and async_update().
487499
@@ -504,39 +516,48 @@ async def _update_domain_objects(self) -> None:
504516
f"{self._endpoint}{DOMAIN_OBJECTS}",
505517
)
506518

507-
async def async_update(self) -> PlugwiseData:
508-
"""Perform an incremental update for updating the various device states."""
519+
async def _full_update_device(self) -> None:
520+
"""Perform a first fetch of all XML data, needed for initialization."""
509521
await self._update_domain_objects()
510-
match self._target_smile:
511-
case "smile_v2":
512-
self._modules = await self._request(MODULES)
513-
case "smile_v3" | "smile_v4":
514-
self._locations = await self._request(LOCATIONS)
515-
case "smile_open_therm_v2" | "smile_open_therm_v3":
516-
self._appliances = await self._request(APPLIANCES)
517-
self._modules = await self._request(MODULES)
518-
case self._target_smile if self._target_smile in REQUIRE_APPLIANCES:
519-
self._appliances = await self._request(APPLIANCES)
520-
521-
self.gw_data["notifications"] = self._notifications
522-
523-
for device_id, device in self.gw_devices.items():
524-
data = self._get_device_data(device_id)
525-
if (
526-
"binary_sensors" in device
527-
and "plugwise_notification" in device["binary_sensors"]
528-
):
529-
data["binary_sensors"]["plugwise_notification"] = bool(
530-
self._notifications
531-
)
532-
device.update(data)
533-
534-
# Update for cooling
535-
if device["dev_class"] in ZONE_THERMOSTATS:
536-
self.update_for_cooling(device)
537-
538-
remove_empty_platform_dicts(device)
522+
self._locations = await self._request(LOCATIONS)
523+
self._modules = await self._request(MODULES)
524+
# P1 legacy has no appliances
525+
if not (self.smile_type == "power" and self._smile_legacy):
526+
self._appliances = await self._request(APPLIANCES)
539527

528+
async def async_update(self) -> PlugwiseData:
529+
"""Perform an incremental update for updating the various device states."""
530+
# Perform a full update at day-change
531+
day_number = dt.datetime.now().strftime("%w")
532+
if (
533+
day_number # pylint: disable=consider-using-assignment-expr
534+
!= self._previous_day_number
535+
):
536+
LOGGER.debug(
537+
"Performing daily full-update, reload the Plugwise integration when a single entity becomes unavailable."
538+
)
539+
self.gw_data: GatewayData = {}
540+
self.gw_devices: dict[str, DeviceData] = {}
541+
await self._full_update_device()
542+
self.get_all_devices()
543+
# Otherwise perform an incremental update
544+
else:
545+
await self._update_domain_objects()
546+
match self._target_smile:
547+
case "smile_v2":
548+
self._modules = await self._request(MODULES)
549+
case "smile_v3" | "smile_v4":
550+
self._locations = await self._request(LOCATIONS)
551+
case "smile_open_therm_v2" | "smile_open_therm_v3":
552+
self._appliances = await self._request(APPLIANCES)
553+
self._modules = await self._request(MODULES)
554+
case self._target_smile if self._target_smile in REQUIRE_APPLIANCES:
555+
self._appliances = await self._request(APPLIANCES)
556+
557+
self._update_gw_devices()
558+
self.gw_data["notifications"] = self._notifications
559+
560+
self._previous_day_number = day_number
540561
return PlugwiseData(self.gw_data, self.gw_devices)
541562

542563
async def _set_schedule_state_legacy(

plugwise/helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1008,7 +1008,7 @@ def _get_regulation_mode(self, appliance: etree, data: DeviceData) -> None:
10081008
locator = "./actuator_functionalities/regulation_mode_control_functionality"
10091009
if (search := appliance.find(locator)) is not None:
10101010
data["select_regulation_mode"] = search.find("mode").text
1011-
self._cooling_enabled = search.find("mode").text == "cooling"
1011+
self._cooling_enabled = data["select_regulation_mode"] == "cooling"
10121012

10131013
def _cleanup_data(self, data: DeviceData) -> None:
10141014
"""Helper-function for _get_measurement_data().

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.32.3"
7+
version = "0.33.0"
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: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -380,15 +380,17 @@ async def device_test(
380380
):
381381
"""Perform basic device tests."""
382382
bsw_list = ["binary_sensors", "central", "climate", "sensors", "switches"]
383-
# Make sure to test with the day set to Sunday, needed for full testcoverage of schedules_temps()
383+
# Make sure to test thermostats with the day set to Monday, needed for full testcoverage of schedules_temps()
384+
# Otherwise set the day to Sunday.
384385
with freeze_time(test_time):
385386
if initialize:
386387
_LOGGER.info("Asserting testdata:")
387388
await smile._full_update_device()
388389
smile.get_all_devices()
390+
data = await smile.async_update()
389391
else:
390392
_LOGGER.info("Asserting updated testdata:")
391-
data = await smile.async_update()
393+
data = await smile.async_update()
392394

393395
if "heater_id" in data.gateway:
394396
self.cooling_present = data.gateway["cooling_present"]
@@ -3861,6 +3863,18 @@ async def test_connect_p1v3_full_option(self):
38613863
async def test_connect_anna_heatpump_heating(self):
38623864
"""Test an Anna with Elga, cooling-mode off, in heating mode."""
38633865
testdata = {
3866+
"015ae9ea3f964e668e490fa39da3870b": {
3867+
"dev_class": "gateway",
3868+
"firmware": "4.0.15",
3869+
"hardware": "AME Smile 2.0 board",
3870+
"location": "a57efe5f145f498c9be62a9b63626fbf",
3871+
"mac_address": "012345670001",
3872+
"model": "Gateway",
3873+
"name": "Smile Anna",
3874+
"vendor": "Plugwise",
3875+
"binary_sensors": {"plugwise_notification": False},
3876+
"sensors": {"outdoor_temperature": 20.2},
3877+
},
38643878
"1cbf783bb11e4a7c8a6843dee3a86927": {
38653879
"dev_class": "heater_central",
38663880
"location": "a57efe5f145f498c9be62a9b63626fbf",
@@ -3899,18 +3913,6 @@ async def test_connect_anna_heatpump_heating(self):
38993913
},
39003914
"switches": {"dhw_cm_switch": False},
39013915
},
3902-
"015ae9ea3f964e668e490fa39da3870b": {
3903-
"dev_class": "gateway",
3904-
"firmware": "4.0.15",
3905-
"hardware": "AME Smile 2.0 board",
3906-
"location": "a57efe5f145f498c9be62a9b63626fbf",
3907-
"mac_address": "012345670001",
3908-
"model": "Gateway",
3909-
"name": "Smile Anna",
3910-
"vendor": "Plugwise",
3911-
"binary_sensors": {"plugwise_notification": False},
3912-
"sensors": {"outdoor_temperature": 20.2},
3913-
},
39143916
"3cb70739631c4d17a86b8b12e8a5161b": {
39153917
"dev_class": "thermostat",
39163918
"firmware": "2018-02-08T11:15:53+01:00",
@@ -3919,6 +3921,12 @@ async def test_connect_anna_heatpump_heating(self):
39193921
"model": "ThermoTouch",
39203922
"name": "Anna",
39213923
"vendor": "Plugwise",
3924+
"temperature_offset": {
3925+
"lower_bound": -2.0,
3926+
"resolution": 0.1,
3927+
"upper_bound": 2.0,
3928+
"setpoint": -0.5,
3929+
},
39223930
"thermostat": {
39233931
"setpoint": 20.5,
39243932
"lower_bound": 4.0,
@@ -3940,6 +3948,41 @@ async def test_connect_anna_heatpump_heating(self):
39403948
},
39413949
},
39423950
}
3951+
testdata_updated = {
3952+
"1cbf783bb11e4a7c8a6843dee3a86927": {
3953+
"dev_class": "heater_central",
3954+
"location": "a57efe5f145f498c9be62a9b63626fbf",
3955+
"model": "Generic heater",
3956+
"name": "OpenTherm",
3957+
"vendor": "Techneco",
3958+
"maximum_boiler_temperature": {
3959+
"setpoint": 60.0,
3960+
"lower_bound": 0.0,
3961+
"upper_bound": 100.0,
3962+
"resolution": 1.0,
3963+
},
3964+
"available": True,
3965+
"binary_sensors": {
3966+
"dhw_state": False,
3967+
"heating_state": True,
3968+
"compressor_state": True,
3969+
"cooling_enabled": False,
3970+
"slave_boiler_state": False,
3971+
"flame_state": False,
3972+
},
3973+
"sensors": {
3974+
"water_temperature": 29.1,
3975+
"domestic_hot_water_setpoint": 60.0,
3976+
"dhw_temperature": 46.3,
3977+
"intended_boiler_temperature": 35.0,
3978+
"modulation_level": 52,
3979+
"return_temperature": 25.1,
3980+
"water_pressure": 1.57,
3981+
"outdoor_air_temperature": 3.0,
3982+
},
3983+
"switches": {"dhw_cm_switch": False},
3984+
},
3985+
}
39433986

39443987
self.smile_setup = "anna_heatpump_heating"
39453988
server, smile, client = await self.connect_wrapper()
@@ -3971,6 +4014,14 @@ async def test_connect_anna_heatpump_heating(self):
39714014
)
39724015
assert result
39734016

4017+
# Now change some data and change directory reading xml from
4018+
# emulating reading newer dataset after an update_interval,
4019+
# set testday to Monday to force an incremental update
4020+
self.smile_setup = "updated/anna_heatpump_heating"
4021+
await self.device_test(
4022+
smile, "2020-04-13 00:00:01", testdata_updated, initialize=False
4023+
)
4024+
assert self.device_items == 61
39744025
await smile.close_connection()
39754026
await self.disconnect(server, client)
39764027

userdata/anna_loria_heating_idle/core.domain_objects.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@
456456
<when time="[sa 07:30,sa 23:15)"><then preset="home"/></when>
457457
<when time="[sa 23:15,su 07:30)"><then preset="asleep"/></when>
458458
<when time="[su 07:30,su 23:15)"><then preset="home"/></when>
459-
<when time="[mo 06:00,mo 23:15)"><then preset="home"/></when>
459+
<when time="[mo 06:00,mo 23:15)"><then setpoint="17.5"/></when>
460460
<when time="[su 23:15,mo 06:00)"><then preset="asleep"/></when>
461461
<when time="[mo 23:15,tu 06:00)"><then preset="asleep"/></when>
462462
<when time="[tu 06:00,tu 23:15)"><then preset="home"/></when>

0 commit comments

Comments
 (0)