Skip to content

Commit 00516ae

Browse files
authored
Merge pull request #204 from plugwise/Improve
Improvements, breaking out dicts with setpoint, lower_bound, etc.
2 parents bab0373 + 035a458 commit 00516ae

File tree

8 files changed

+473
-275
lines changed

8 files changed

+473
-275
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.21.0: Smile: improve output, fix cooling-bug
4+
- Add `domestic_hot_water_setpoint` to the output. Will become an additional Number in Plugwise(-beta).
5+
- Create separate dicts for `domestic_hot_water_setpoint`, `maximum_boiler_temperature`, and `thermostat` in the output.
6+
- Change `set_max_boiler_temperature()` to `set_number_setpoint()` and make it more general so that more than one Number setpoint can be changed.
7+
- Fix a cooling-related bug (Anna + Elga).
8+
- Improve `set_temperature()`function.
9+
- Update the testcode accordingly.
10+
311
# v0.20.1: Smile: fix/improve cooling support (Elga/Loria/Thermastage) based on input from Plugwise
412

513
# v0.20.0: Adam: add support for the Aqara Plug

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.20.1"
3+
__version__ = "0.21.0"
44

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

plugwise/constants.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,18 @@
363363

364364
### Smile constants ###
365365

366+
ACTUATOR_CLASSES: Final[list[str]] = [
367+
"heater_central",
368+
"thermostat",
369+
"thermostatic_radiator_valve",
370+
"zone_thermometer",
371+
"zone_thermostat",
372+
]
373+
ACTIVE_ACTUATORS: Final[list[str]] = [
374+
"domestic_hot_water_setpoint",
375+
"maximum_boiler_temperature",
376+
"thermostat",
377+
]
366378
ATTR_ENABLED: Final = "enabled_default"
367379
ATTR_ID: Final = "id"
368380
ATTR_ICON: Final = "icon"
@@ -381,18 +393,21 @@
381393
DEFAULT_PORT: Final = 80
382394
NONE: Final = "None"
383395
FAKE_LOC: Final = "0000aaaa0000aaaa0000aaaa0000aa00"
384-
MAX_SETPOINT: Final = 40
385-
MIN_SETPOINT: Final = 0
396+
LIMITS: Final[list[str]] = ["setpoint", "lower_bound", "upper_bound", "resolution"]
397+
MAX_SETPOINT: Final[float] = 40.0
398+
MIN_SETPOINT: Final[float] = 0.0
386399
SEVERITIES: Final[list[str]] = ["other", "info", "warning", "error"]
387400
SPECIAL_FORMAT: Final[list[str]] = [ENERGY_KILO_WATT_HOUR, VOLUME_CUBIC_METERS]
388401
SWITCH_GROUP_TYPES: Final[list[str]] = ["switching", "report"]
389402
ZONE_THERMOSTATS: Final[list[str]] = [
390403
"thermostat",
404+
"thermostatic_radiator_valve",
391405
"zone_thermometer",
392406
"zone_thermostat",
393407
]
394408
THERMOSTAT_CLASSES: Final[list[str]] = [
395409
"thermostat",
410+
"thermo_sensor",
396411
"zone_thermometer",
397412
"zone_thermostat",
398413
"thermostatic_radiator_valve",
@@ -450,10 +465,6 @@
450465
"electricity_consumed": {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT},
451466
"electricity_produced": {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT},
452467
"relay": {ATTR_UNIT_OF_MEASUREMENT: NONE},
453-
# Added measurements from actuator_functionalities/thermostat_functionality
454-
"lower_bound": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
455-
"upper_bound": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
456-
"resolution": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
457468
"regulation_mode": {ATTR_UNIT_OF_MEASUREMENT: NONE},
458469
}
459470

@@ -731,6 +742,17 @@ class ThermoLoc(TypedDict, total=False):
731742
slaves: set[str]
732743

733744

745+
class ActuatorData(TypedDict, total=False):
746+
"""Actuator data for thermostat types."""
747+
748+
lower_bound: float
749+
setpoint: float
750+
setpoint_high: float
751+
setpoint_low: float
752+
resolution: float
753+
upper_bound: float
754+
755+
734756
class DeviceDataPoints(
735757
SmileBinarySensors, SmileSensors, SmileSwitches, TypedDict, total=False
736758
):
@@ -740,14 +762,7 @@ class DeviceDataPoints(
740762
regulation_mode: str
741763
regulation_modes: list[str]
742764

743-
# Heater Central
744-
maximum_boiler_temperature: float
745-
746765
# Master Thermostats
747-
lower_bound: float
748-
upper_bound: float
749-
resolution: float
750-
751766
preset_modes: list[str] | None
752767
active_preset: str | None
753768

@@ -768,5 +783,7 @@ class DeviceData(ApplianceData, DeviceDataPoints, TypedDict, total=False):
768783
"""The Device Data class, covering the collected and ordere output-data per device."""
769784

770785
binary_sensors: SmileBinarySensors
786+
domestic_hot_water_setpoint: ActuatorData
771787
sensors: SmileSensors
772788
switches: SmileSwitches
789+
thermostat: ActuatorData

plugwise/helper.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from semver import VersionInfo
1818

1919
from .constants import (
20+
ACTIVE_ACTUATORS,
21+
ACTUATOR_CLASSES,
2022
APPLIANCES,
2123
ATTR_NAME,
2224
ATTR_UNIT_OF_MEASUREMENT,
@@ -28,6 +30,7 @@
2830
FAKE_LOC,
2931
HEATER_CENTRAL_MEASUREMENTS,
3032
HOME_MEASUREMENTS,
33+
LIMITS,
3134
LOCATIONS,
3235
LOGGER,
3336
NONE,
@@ -36,6 +39,7 @@
3639
SPECIAL_PLUG_TYPES,
3740
SWITCH_GROUP_TYPES,
3841
SWITCHES,
42+
TEMP_CELSIUS,
3943
THERMOSTAT_CLASSES,
4044
ApplianceData,
4145
DeviceData,
@@ -93,6 +97,25 @@ def check_model(name: str | None, vendor_name: str | None) -> str | None:
9397
return name
9498

9599

100+
def _get_actuator_functionalities(xml: etree) -> DeviceData:
101+
"""Helper-function for _get_appliance_data()."""
102+
data: DeviceData = {}
103+
for item in ACTIVE_ACTUATORS:
104+
temp_dict: dict[str, float] = {}
105+
for key in LIMITS:
106+
locator = f'.//actuator_functionalities/thermostat_functionality[type="{item}"]/{key}'
107+
if (function := xml.find(locator)) is not None:
108+
if function.text == "nil":
109+
break
110+
111+
temp_dict.update({key: format_measure(function.text, TEMP_CELSIUS)})
112+
113+
if temp_dict:
114+
data[item] = temp_dict # type: ignore [literal-required]
115+
116+
return data
117+
118+
96119
def schedules_temps(
97120
schedules: dict[str, dict[str, list[float]]], name: str
98121
) -> list[float] | None:
@@ -824,18 +847,6 @@ def _appliance_measurements(
824847
name = f"{measurement}_interval"
825848
data[name] = format_measure(appl_i_loc.text, ENERGY_WATT_HOUR) # type: ignore [literal-required]
826849

827-
# Thermostat actuator measurements
828-
t_locator = f'.//actuator_functionalities/thermostat_functionality[type="thermostat"]/{measurement}'
829-
if (t_function := appliance.find(t_locator)) is not None:
830-
if new_name := attrs.get(ATTR_NAME):
831-
measurement = new_name
832-
833-
# Avoid double processing
834-
if measurement == "setpoint":
835-
continue
836-
837-
data[measurement] = format_measure(t_function.text, attrs[ATTR_UNIT_OF_MEASUREMENT]) # type: ignore [literal-required]
838-
839850
return data
840851

841852
def _get_appliance_data(self, d_id: str) -> DeviceData:
@@ -857,6 +868,9 @@ def _get_appliance_data(self, d_id: str) -> DeviceData:
857868
) is not None:
858869
data = self._appliance_measurements(appliance, data, measurements)
859870
data.update(self._get_lock_state(appliance))
871+
if (appl_type := appliance.find("type")) is not None:
872+
if appl_type.text in ACTUATOR_CLASSES:
873+
data.update(_get_actuator_functionalities(appliance))
860874

861875
# Remove c_heating_state from the output
862876
if "c_heating_state" in data:

plugwise/smile.py

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
"""
44
from __future__ import annotations
55

6-
from typing import Any
7-
86
import aiohttp
97
from defusedxml import ElementTree as etree
108

@@ -31,8 +29,8 @@
3129
STATUS,
3230
SWITCH_GROUP_TYPES,
3331
SYSTEM,
34-
THERMOSTAT_CLASSES,
3532
ZONE_THERMOSTATS,
33+
ActuatorData,
3634
ApplianceData,
3735
DeviceData,
3836
GatewayData,
@@ -67,15 +65,31 @@ def update_for_cooling(self, devices: dict[str, DeviceData]) -> None:
6765
continue
6866

6967
if self.elga_cooling_enabled:
68+
# Replace setpoint with setpoint_high/_low
69+
thermostat = device["thermostat"]
7070
sensors = device["sensors"]
71-
sensors["setpoint_low"] = sensors["setpoint"]
72-
sensors["setpoint_high"] = MAX_SETPOINT
73-
if self._elga_cooling_active:
74-
sensors["setpoint_low"] = MIN_SETPOINT
75-
sensors["setpoint_high"] = sensors["setpoint"]
71+
max_setpoint = MAX_SETPOINT
72+
min_setpoint = MIN_SETPOINT
7673
if self._sched_setpoints is not None:
77-
sensors["setpoint_low"] = self._sched_setpoints[0]
78-
sensors["setpoint_high"] = self._sched_setpoints[1]
74+
max_setpoint = self._sched_setpoints[1]
75+
min_setpoint = self._sched_setpoints[0]
76+
77+
temp_dict: ActuatorData = {
78+
"setpoint_low": thermostat["setpoint"],
79+
"setpoint_high": max_setpoint,
80+
}
81+
if self._elga_cooling_active:
82+
temp_dict = {
83+
"setpoint_low": min_setpoint,
84+
"setpoint_high": thermostat["setpoint"],
85+
}
86+
if "setpoint" in sensors:
87+
sensors.pop("setpoint")
88+
sensors["setpoint_low"] = temp_dict["setpoint_low"]
89+
sensors["setpoint_high"] = temp_dict["setpoint_high"]
90+
thermostat.pop("setpoint")
91+
temp_dict.update(thermostat)
92+
device["thermostat"] = temp_dict
7993

8094
# For Adam + on/off cooling, modify heating_state and cooling_state
8195
# based on provided info by Plugwise
@@ -244,6 +258,9 @@ def _get_device_data(self, dev_id: str) -> DeviceData:
244258
"""
245259
details = self._appl_data[dev_id]
246260
device_data = self._get_appliance_data(dev_id)
261+
# Remove thermostat-dict for thermo_sensors
262+
if details["dev_class"] == "thermo_sensor":
263+
device_data.pop("thermostat")
247264

248265
# Generic
249266
if details["dev_class"] == "gateway" or dev_id == self.gateway_id:
@@ -270,7 +287,7 @@ def _get_device_data(self, dev_id: str) -> DeviceData:
270287
# Specific, not generic Adam data
271288
device_data = self._device_data_adam(details, device_data)
272289
# No need to obtain thermostat data when the device is not a thermostat
273-
if details["dev_class"] not in THERMOSTAT_CLASSES:
290+
if details["dev_class"] not in ZONE_THERMOSTATS:
274291
return device_data
275292

276293
# Thermostat data (presets, temperatures etc)
@@ -639,35 +656,46 @@ async def set_preset(self, loc_id: str, preset: str) -> None:
639656

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

642-
async def set_temperature(self, loc_id: str, temps: dict[str, Any]) -> None:
659+
async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None:
643660
"""Set the given Temperature on the relevant Thermostat."""
644-
if "setpoint" in temps:
645-
setpoint = temps["setpoint"]
646-
elif self._elga_cooling_active:
647-
setpoint = temps["setpoint_high"]
648-
else:
649-
setpoint = temps["setpoint_low"]
650-
651-
temp = str(setpoint)
661+
setpoint: float | None = None
662+
if "setpoint" in items:
663+
setpoint = items["setpoint"]
664+
if self.elga_cooling_enabled:
665+
if "setpoint_low" in items:
666+
setpoint = items["setpoint_low"]
667+
if self._elga_cooling_active:
668+
if "setpoint_high" in items:
669+
setpoint = items["setpoint_high"]
670+
671+
if setpoint is None:
672+
raise PlugwiseError(
673+
"Plugwise: failed setting temperature: no valid input provided"
674+
) # pragma: no cover
675+
temperature = str(setpoint)
652676
uri = self._thermostat_uri(loc_id)
653677
data = (
654678
"<thermostat_functionality><setpoint>"
655-
f"{temp}</setpoint></thermostat_functionality>"
679+
f"{temperature}</setpoint></thermostat_functionality>"
656680
)
657681

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

660-
async def set_max_boiler_temperature(self, temperature: float) -> None:
661-
"""Set the max. Boiler Temperature on the Central heating boiler."""
684+
async def set_number_setpoint(self, key: str, temperature: float) -> None:
685+
"""Set the max. Boiler or DHW setpoint on the Central Heating boiler."""
662686
temp = str(temperature)
687+
thermostat_id: str | None = None
663688
locator = f'appliance[@id="{self._heater_id}"]/actuator_functionalities/thermostat_functionality'
664-
th_func = self._appliances.find(locator)
665-
if th_func.find("type").text == "maximum_boiler_temperature":
666-
thermostat_id = th_func.attrib["id"]
689+
if th_func_list := self._appliances.findall(locator):
690+
for th_func in th_func_list:
691+
if th_func.find("type").text == key:
692+
thermostat_id = th_func.attrib["id"]
693+
694+
if thermostat_id is None:
695+
raise PlugwiseError(f"Plugwise: cannot change setpoint, {key} not found.")
667696

668697
uri = f"{APPLIANCES};id={self._heater_id}/thermostat;id={thermostat_id}"
669698
data = f"<thermostat_functionality><setpoint>{temp}</setpoint></thermostat_functionality>"
670-
671699
await self._request(uri, method="put", data=data)
672700

673701
async def _set_groupswitch_member_state(

0 commit comments

Comments
 (0)