Skip to content

Commit ed4acdb

Browse files
authored
Merge pull request #236 from plugwise/toggle-switches
Toggles-related update
2 parents cd446ce + 7fc26ac commit ed4acdb

File tree

6 files changed

+71
-46
lines changed

6 files changed

+71
-46
lines changed

CHANGELOG.md

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

3+
# v0.25.8: Make collection of toggle-data future-proof
4+
35
# v0.25.7: Correct faulty logic in the v0.25.6 release
46

57
# v0.25.6: Revert py.typed, fix Core PR #81531

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.7"
3+
__version__ = "0.25.8a0"
44

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

plugwise/constants.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,6 @@
478478
HEATER_CENTRAL_MEASUREMENTS: Final[dict[str, DATA | UOM]] = {
479479
"boiler_temperature": DATA("water_temperature", TEMP_CELSIUS),
480480
"domestic_hot_water_mode": DATA("dhw_mode", NONE),
481-
"domestic_hot_water_comfort_mode": DATA("dhw_cm_switch", NONE),
482481
"domestic_hot_water_state": DATA("dhw_state", TEMP_CELSIUS),
483482
"domestic_hot_water_temperature": DATA("dhw_temperature", TEMP_CELSIUS),
484483
"elga_status_code": UOM(NONE),
@@ -498,7 +497,7 @@
498497
"compressor_state": UOM(NONE),
499498
"cooling_state": UOM(NONE),
500499
# Available with the Loria and Elga (newer Anna firmware) heatpumps
501-
"cooling_enabled": DATA("cooling_ena_switch", TEMP_CELSIUS),
500+
"cooling_enabled": UOM(NONE),
502501
# Next 2 keys are used to show the state of the gas-heater used next to the Elga heatpump - marcelveldt
503502
"slave_boiler_state": UOM(NONE),
504503
"flame_state": UOM(NONE), # Also present when there is a single gas-heater
@@ -511,6 +510,11 @@
511510
"outdoor_temperature": DATA("outdoor_air_temperature", TEMP_CELSIUS),
512511
}
513512

513+
TOGGLES: Final[dict[str, str]] = {
514+
"cooling_enabled": "cooling_ena_switch",
515+
"domestic_hot_water_comfort_mode": "dhw_cm_switch",
516+
}
517+
514518
# Known types of Smiles and Stretches
515519
SMILE = namedtuple("SMILE", "smile_type smile_name")
516520
SMILES: Final[dict[str, SMILE]] = {
@@ -529,6 +533,7 @@
529533
# All available Binary Sensor, Sensor, and Switch Types
530534

531535
BINARY_SENSORS: Final[tuple[str, ...]] = (
536+
"cooling_enabled",
532537
"compressor_state",
533538
"cooling_state",
534539
"dhw_state",
@@ -630,6 +635,7 @@ class ModelData(TypedDict):
630635
class SmileBinarySensors(TypedDict, total=False):
631636
"""Smile Binary Sensors class."""
632637

638+
cooling_enabled: bool
633639
compressor_state: bool
634640
cooling_state: bool
635641
dhw_state: bool
@@ -752,7 +758,6 @@ class DeviceDataPoints(
752758
class DeviceData(ApplianceData, DeviceDataPoints, TypedDict, total=False):
753759
"""The Device Data class, covering the collected and ordere output-data per device."""
754760

755-
cooling_enabled: bool
756761
binary_sensors: SmileBinarySensors
757762
domestic_hot_water_setpoint: ActuatorData
758763
sensors: SmileSensors

plugwise/helper.py

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import asyncio
77
import datetime as dt
8+
from typing import Any, cast
89

910
# This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
1011
from aiohttp import BasicAuth, ClientError, ClientResponse, ClientSession, ClientTimeout
@@ -45,6 +46,7 @@
4546
SWITCHES,
4647
TEMP_CELSIUS,
4748
THERMOSTAT_CLASSES,
49+
TOGGLES,
4850
UOM,
4951
ApplianceData,
5052
DeviceData,
@@ -99,9 +101,8 @@ def check_model(name: str | None, vendor_name: str | None) -> str | None:
99101
return name
100102

101103

102-
def _get_actuator_functionalities(xml: etree) -> DeviceData:
104+
def _get_actuator_functionalities(xml: etree, data: dict[str, Any]) -> None:
103105
"""Helper-function for _get_appliance_data()."""
104-
data: DeviceData = {}
105106
for item in ACTIVE_ACTUATORS:
106107
temp_dict: dict[str, float] = {}
107108
for key in LIMITS:
@@ -113,9 +114,7 @@ def _get_actuator_functionalities(xml: etree) -> DeviceData:
113114
temp_dict.update({key: format_measure(function.text, TEMP_CELSIUS)})
114115

115116
if temp_dict:
116-
data[item] = temp_dict # type: ignore [literal-required]
117-
118-
return data
117+
data[item] = temp_dict
119118

120119

121120
def schedules_temps(
@@ -848,9 +847,9 @@ def _rule_ids_by_tag(self, tag: str, loc_id: str) -> dict[str, str]:
848847
def _appliance_measurements(
849848
self,
850849
appliance: etree,
851-
data: DeviceData,
850+
data: dict[str, Any],
852851
measurements: dict[str, DATA | UOM],
853-
) -> DeviceData:
852+
) -> None:
854853
"""Helper-function for _get_appliance_data() - collect appliance measurement data."""
855854
for measurement, attrs in measurements.items():
856855
p_locator = f'.//logs/point_log[type="{measurement}"]/period/measurement'
@@ -869,28 +868,28 @@ def _appliance_measurements(
869868
if new_name := getattr(attrs, ATTR_NAME, None):
870869
measurement = new_name
871870

872-
data[measurement] = appl_p_loc.text # type: ignore [literal-required]
871+
data[measurement] = appl_p_loc.text
873872
# measurements with states "on" or "off" that need to be passed directly
874873
if measurement not in ["dhw_mode", "regulation_mode"]:
875-
data[measurement] = format_measure(appl_p_loc.text, getattr(attrs, ATTR_UNIT_OF_MEASUREMENT)) # type: ignore [literal-required]
874+
data[measurement] = format_measure(
875+
appl_p_loc.text, getattr(attrs, ATTR_UNIT_OF_MEASUREMENT)
876+
)
876877

877878
# Anna: save cooling-related measurements for later use
878879
# Use the local outdoor temperature as reference for turning cooling on/off
879880
if measurement == "cooling_activation_outdoor_temperature":
880-
self._cooling_activation_outdoor_temp = data[measurement] # type: ignore [literal-required]
881+
self._cooling_activation_outdoor_temp = data[measurement]
881882
if measurement == "cooling_deactivation_threshold":
882-
self._cooling_deactivation_threshold = data[measurement] # type: ignore [literal-required]
883+
self._cooling_deactivation_threshold = data[measurement]
883884
if measurement == "outdoor_air_temperature":
884-
self._outdoor_temp = data[measurement] # type: ignore [literal-required]
885+
self._outdoor_temp = data[measurement]
885886

886887
i_locator = f'.//logs/interval_log[type="{measurement}"]/period/measurement'
887888
if (appl_i_loc := appliance.find(i_locator)) is not None:
888889
name = f"{measurement}_interval"
889-
data[name] = format_measure(appl_i_loc.text, ENERGY_WATT_HOUR) # type: ignore [literal-required]
890-
891-
return data
890+
data[name] = format_measure(appl_i_loc.text, ENERGY_WATT_HOUR)
892891

893-
def _wireless_availablity(self, appliance: etree, data: DeviceData) -> None:
892+
def _wireless_availablity(self, appliance: etree, data: dict[str, Any]) -> None:
894893
"""Helper-function for _get_appliance_data().
895894
Collect the availablity-status for wireless connected devices.
896895
"""
@@ -913,10 +912,10 @@ def _get_appliance_data(self, d_id: str) -> DeviceData:
913912
Collect the appliance-data based on device id.
914913
Determined from APPLIANCES, for legacy from DOMAIN_OBJECTS.
915914
"""
916-
data: DeviceData = {}
915+
data: dict[str, Any] = {}
917916
# P1 legacy has no APPLIANCES, also not present in DOMAIN_OBJECTS
918917
if self._smile_legacy and self.smile_type == "power":
919-
return data
918+
return cast(DeviceData, data)
920919

921920
measurements = DEVICE_MEASUREMENTS
922921
if d_id == self._heater_id:
@@ -926,11 +925,13 @@ def _get_appliance_data(self, d_id: str) -> DeviceData:
926925
appliance := self._appliances.find(f'./appliance[@id="{d_id}"]')
927926
) is not None:
928927

929-
data = self._appliance_measurements(appliance, data, measurements)
930-
data.update(self._get_lock_state(appliance))
931-
if (appl_type := appliance.find("type")) is not None:
932-
if appl_type.text in ACTUATOR_CLASSES:
933-
data.update(_get_actuator_functionalities(appliance))
928+
self._appliance_measurements(appliance, data, measurements)
929+
self._get_lock_state(appliance, data)
930+
for toggle, name in TOGGLES.items():
931+
self._get_toggle_state(appliance, toggle, name, data)
932+
933+
if appliance.find("type").text in ACTUATOR_CLASSES:
934+
_get_actuator_functionalities(appliance, data)
934935

935936
# Collect availability-status for wireless connected devices to Adam
936937
self._wireless_availablity(appliance, data)
@@ -960,26 +961,31 @@ def _get_appliance_data(self, d_id: str) -> DeviceData:
960961
if "temperature" in data:
961962
data.pop("heating_state", None)
962963

963-
if d_id == self._heater_id:
964-
# Adam
965-
if self.smile_name == "Smile Anna":
966-
# Use elga_status_code or cooling_enabled to set _cooling_enabled to True
967-
if self._cooling_present:
968-
# Elga:
969-
if "elga_status_code" in data:
970-
self._cooling_enabled = data["elga_status_code"] in [8, 9]
971-
self._cooling_active = data["elga_status_code"] == 8
972-
data.pop("elga_status_code", None)
973-
# Loria/Thermastate: look at cooling_state, not at cooling_enabled, not available on R32!
974-
elif "cooling_ena_switch" in data:
975-
self._cooling_enabled = data["cooling_ena_switch"]
976-
self._cooling_active = data["cooling_state"]
964+
if (
965+
d_id == self._heater_id
966+
and self.smile_name == "Smile Anna"
967+
and self._cooling_present
968+
):
969+
# Use elga_status_code or cooling_enabled to set _cooling_enabled to True
970+
if "elga_status_code" in data:
971+
self._cooling_enabled = data["elga_status_code"] in [8, 9]
972+
self._cooling_active = data["elga_status_code"] == 8
973+
data.pop("elga_status_code", None)
974+
# Loria/Thermastate: look at cooling_state, not at cooling_enabled, not available on R32!
975+
# Anna + Elga >= 4.3.7: the Elga cooling-enabled state is shown but there is no cooling-switch
976+
for item in ("cooling_ena_switch", "cooling_enabled"):
977+
if item in data:
978+
self._cooling_enabled = data[item]
979+
self._cooling_active = data["cooling_state"]
980+
981+
if all(item in data for item in ("cooling_ena_switch", "cooling_enabled")):
982+
data.pop("cooling_enabled")
977983

978984
# Don't show cooling_state when no cooling present
979985
if not self._cooling_present and "cooling_state" in data:
980986
data.pop("cooling_state")
981987

982-
return data
988+
return cast(DeviceData, data)
983989

984990
def _rank_thermostat(
985991
self,
@@ -1333,11 +1339,10 @@ def _object_value(self, obj_id: str, measurement: str) -> float | int | None:
13331339

13341340
return val
13351341

1336-
def _get_lock_state(self, xml: etree) -> DeviceData:
1342+
def _get_lock_state(self, xml: etree, data: dict[str, Any]) -> None:
13371343
"""Helper-function for _get_appliance_data().
13381344
Adam & Stretches: obtain the relay-switch lock state.
13391345
"""
1340-
data: DeviceData = {}
13411346
actuator = "actuator_functionalities"
13421347
func_type = "relay_functionality"
13431348
if self._stretch_v2:
@@ -1348,7 +1353,19 @@ def _get_lock_state(self, xml: etree) -> DeviceData:
13481353
if (found := xml.find(locator)) is not None:
13491354
data["lock"] = found.text == "true"
13501355

1351-
return data
1356+
def _get_toggle_state(
1357+
self, xml: etree, toggle: str, name: str, data: dict[str, Any]
1358+
) -> None:
1359+
"""Helper-function for _get_appliance_data().
1360+
Obtain the toggle state of 'toggle'.
1361+
"""
1362+
if xml.find("type").text == "heater_central":
1363+
locator = "./actuator_functionalities/toggle_functionality"
1364+
if found := xml.findall(locator):
1365+
for item in found:
1366+
if (toggle_type := item.find("type")) is not None:
1367+
if toggle_type.text == toggle:
1368+
data.update({name: item.find("state").text == "on"})
13521369

13531370
def _update_device_with_dicts(
13541371
self,

tests/test_smile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3950,6 +3950,7 @@ async def test_connect_anna_heatpump_cooling_fake_firmware(self):
39503950
# Heater central
39513951
"1cbf783bb11e4a7c8a6843dee3a86927": {
39523952
"binary_sensors": {
3953+
"cooling_enabled": True,
39533954
"cooling_state": True,
39543955
"dhw_state": False,
39553956
"heating_state": False,

userdata/anna_loria_cooling_active/core.appliances.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@
560560
<cooling_toggle id='bb06428d40a44079bebcce46e5132fa9'/>
561561
<updated_date>2022-09-29T16:33:27.169+02:00</updated_date>
562562
<type>cooling_enabled</type>
563-
<state>off</state>
563+
<state>on</state>
564564
</toggle_functionality>
565565
</actuator_functionalities>
566566
</appliance>

0 commit comments

Comments
 (0)