Skip to content

Commit 47df167

Browse files
authored
Merge pull request #167 from plugwise/regulation_mode
Add regulation_mode(s) and max_boiler_temp to output
2 parents dc354e5 + e21a171 commit 47df167

File tree

6 files changed

+83
-7
lines changed

6 files changed

+83
-7
lines changed

CHANGELOG.md

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

3+
# v0.17.0 - Smile: add more outputs
4+
- Add regulation_mode and regulation_modes to gateway dict, add related set-function
5+
- Add max_boiler_temperature to heater_central dict, add related set-function
6+
- Improve typing hints
7+
38
# v0.16.9 - Smile: bugfix and improve
49
- Fix for https://github.com/plugwise/plugwise-beta/issues/250
510
- Rename heatpump outdoor_temperature sensor to outdoor_air_temperature sensor

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

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

plugwise/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,8 @@
441441
"lower_bound": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
442442
"upper_bound": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
443443
"resolution": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
444+
"regulation_mode": {ATTR_UNIT_OF_MEASUREMENT: None},
445+
"maximum_boiler_temperature": {ATTR_UNIT_OF_MEASUREMENT: None},
444446
}
445447

446448
# Heater Central related measurements

plugwise/helper.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ def __init__(self):
290290
"""Set the constructor for this class."""
291291
self._appl_data: dict[str, Any] = {}
292292
self._appliances: etree | None = None
293+
self._allowed_modes: list[str] = []
293294
self._anna_cooling_present: bool = False
294295
self._cooling_activation_outdoor_temp: float | None = None
295296
self._cooling_deactivation_threshold: float | None = None
@@ -500,6 +501,7 @@ def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
500501
for mode in search.find("allowed_modes"):
501502
mode_list.append(mode.text)
502503
self._cooling_present = "cooling" in mode_list
504+
self._allowed_modes = mode_list
503505

504506
return appl
505507

@@ -815,9 +817,12 @@ def _appliance_measurements(
815817
except KeyError:
816818
pass
817819

818-
data[measurement] = format_measure(
819-
appl_p_loc.text, attrs.get(ATTR_UNIT_OF_MEASUREMENT)
820-
)
820+
data[measurement] = appl_p_loc.text
821+
# measurements with states "on" or "off" that need to be passed directly
822+
if measurement not in ["regulation_mode"]:
823+
data[measurement] = format_measure(
824+
appl_p_loc.text, attrs.get(ATTR_UNIT_OF_MEASUREMENT)
825+
)
821826

822827
# Anna: save cooling-related measurements for later use
823828
# Use the local outdoor temperature as reference for turning cooling on/off

plugwise/smile.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,19 @@ def _get_device_data(self, dev_id: str) -> dict[str, Any]:
193193

194194
# Generic
195195
if details.get("class") == "gateway" or dev_id == self.gateway_id:
196-
# Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
197-
# The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
198196
if self.smile_type == "thermostat":
197+
# Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
198+
# The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
199199
outdoor_temperature = self._object_value(
200200
self._home_location, "outdoor_temperature"
201201
)
202202
if outdoor_temperature is not None:
203203
device_data["outdoor_temperature"] = outdoor_temperature
204204

205+
if self.smile_name == "Adam":
206+
# Show the allowed regulation modes
207+
device_data["regulation_modes"] = self._allowed_modes
208+
205209
# Get P1 data from LOCATIONS
206210
power_data = self._power_data_from_location(details.get("location"))
207211
if power_data is not None:
@@ -565,7 +569,6 @@ async def set_preset(self, loc_id: str, preset: str) -> bool:
565569

566570
async def set_temperature(self, loc_id: str, temperature: str) -> bool:
567571
"""Set the given Temperature on the relevant Thermostat."""
568-
temperature = str(temperature)
569572
uri = self._thermostat_uri(loc_id)
570573
data = (
571574
"<thermostat_functionality><setpoint>"
@@ -575,6 +578,19 @@ async def set_temperature(self, loc_id: str, temperature: str) -> bool:
575578
await self._request(uri, method="put", data=data)
576579
return True
577580

581+
async def set_max_boiler_temperature(self, temperature: str) -> bool:
582+
"""Set the max. Boiler Temperature on the Central heating boiler."""
583+
locator = f'appliance[@id="{self._heater_id}"]/actuator_functionalities/thermostat_functionality'
584+
th_func = self._appliances.find(locator)
585+
if th_func.find("type").text == "maximum_boiler_temperature":
586+
thermostat_id = th_func.attrib["id"]
587+
588+
uri = f"{APPLIANCES};id={self._heater_id}/thermostat;id={thermostat_id}"
589+
data = f"<thermostat_functionality><setpoint>{temperature}</setpoint></thermostat_functionality>"
590+
591+
await self._request(uri, method="put", data=data)
592+
return True
593+
578594
async def _set_groupswitch_member_state(
579595
self, members: list[str] | None, state: str, switch: Munch
580596
) -> bool:
@@ -636,6 +652,20 @@ async def set_switch_state(
636652
await self._request(uri, method="put", data=data)
637653
return True
638654

655+
async def set_regulation_mode(self, mode: str) -> bool:
656+
"""Set the heating regulation mode."""
657+
if mode not in self._allowed_modes:
658+
return False
659+
660+
uri = f"{APPLIANCES};type=gateway/regulation_mode_control"
661+
duration = ""
662+
if "bleeding" in mode:
663+
duration = "<duration>300</duration>"
664+
data = f"<regulation_mode_control_functionality>{duration}<mode>{mode}</mode></regulation_mode_control_functionality>"
665+
666+
await self._request(uri, method="put", data=data)
667+
return True
668+
639669
async def delete_notification(self) -> bool:
640670
"""Delete the active Plugwise Notification."""
641671
uri = NOTIFICATIONS

tests/test_smile.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,30 @@ async def tinker_thermostat(self, smile, loc_id, good_schemas=None, unhappy=Fals
564564
await self.tinker_thermostat_preset(smile, loc_id, unhappy)
565565
await self.tinker_thermostat_schema(smile, loc_id, good_schemas, unhappy)
566566

567+
@staticmethod
568+
async def tinker_regulation_mode(smile):
569+
"""Toggle regulation_mode to test functionality."""
570+
for mode in ["off", "heating", "bleeding_cold", "!bogus"]:
571+
assert_state = True
572+
warning = ""
573+
if mode[0] == "!":
574+
assert_state = False
575+
warning = " Negative test"
576+
mode = mode[1:]
577+
_LOGGER.info("%s", f"- Adjusting regulation mode to {mode}{warning}")
578+
mode_change = await smile.set_regulation_mode(mode)
579+
assert mode_change == assert_state
580+
_LOGGER.info(" + worked as intended")
581+
582+
@staticmethod
583+
async def tinker_max_boiler_temp(smile):
584+
"""Change max boiler temp setpoint to test functionality."""
585+
new_temp = 60.0
586+
_LOGGER.info("- Adjusting temperature to %s", new_temp)
587+
temp_change = await smile.set_max_boiler_temperature(new_temp)
588+
assert temp_change
589+
_LOGGER.info(" + worked as intended")
590+
567591
@pytest.mark.asyncio
568592
async def test_connect_legacy_anna(self):
569593
"""Test a legacy Anna device."""
@@ -603,6 +627,7 @@ async def test_connect_legacy_anna(self):
603627
"model": "4.21",
604628
"name": "OpenTherm",
605629
"vendor": "Bosch Thermotechniek B.V.",
630+
"maximum_boiler_temperature": 50.0,
606631
"binary_sensors": {"flame_state": True, "heating_state": True},
607632
"sensors": {
608633
"water_temperature": 23.6,
@@ -1447,6 +1472,9 @@ async def test_connect_adam_plus_anna_new(self):
14471472
"model": "Adam",
14481473
"name": "Adam",
14491474
"vendor": "Plugwise B.V.",
1475+
"zigbee_mac_address": "ABCD012345670101",
1476+
"regulation_mode": "heating",
1477+
"regulation_modes": ["heating", "off", "bleeding_cold", "bleeding_hot"],
14501478
"binary_sensors": {"plugwise_notification": False},
14511479
"sensors": {"outdoor_temperature": -1.25},
14521480
},
@@ -1458,6 +1486,7 @@ async def test_connect_adam_plus_anna_new(self):
14581486
"model": "Generic heater",
14591487
"name": "OpenTherm",
14601488
"vendor": None,
1489+
"maximum_boiler_temperature": 60.0,
14611490
"binary_sensors": {
14621491
"dhw_state": False,
14631492
"flame_state": False,
@@ -1516,6 +1545,11 @@ async def test_connect_adam_plus_anna_new(self):
15161545
smile, "2568cc4b9c1e401495d4741a5f89bee1"
15171546
)
15181547
assert not switch_change
1548+
1549+
await self.tinker_regulation_mode(smile)
1550+
1551+
await self.tinker_max_boiler_temp(smile)
1552+
15191553
await smile.close_connection()
15201554
await self.disconnect(server, client)
15211555

0 commit comments

Comments
 (0)