Skip to content

Commit 5ba3e76

Browse files
authored
Merge pull request #110 from plugwise/cooling_function
Various updates and improvements
2 parents f4d0bbf + 979500b commit 5ba3e76

File tree

6 files changed

+215
-203
lines changed

6 files changed

+215
-203
lines changed

CHANGELOG.md

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

3-
## In progress
3+
# v0.15.6 - Smile - Various fixes and improvements
4+
- Adam: collect `control_state` from master thermostats, allows showing the thermostat state as on the Plugwise App
5+
- Adam: collect `allowed_modes` and look for `cooling`, indicating cooling capability being available
6+
- Optimize code: use `_all_appliances()` once instead of 3 times, by updating/changing `single_master_thermostat()`,
7+
- Protect several more variables,
8+
- Change/improve how `illuminance` and `outdoor_temperature` are obtained,
9+
- Use walrus operator where applicable,
10+
- Various small code improvements,
11+
- Add and adapt testcode
412
- Add testing for python 3.10, improve dependencies (github workflow)
513
- Bump aiohttp to 3.8.1, remove fixed dependencies
614

15+
# v0.15.5 - Skipping, not released
16+
717
## v0.15.4 - Smile - Bugfix: handle removed thermostats
818
- Recognize when a thermostat has been removed from a zone and don't show it in Core
919
- Rename Group Switch to Switchgroup, remove vendor name

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.15.4"
3+
__version__ = "0.15.6"
44

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

plugwise/constants.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
PRESSURE_BAR = "bar"
2020
SIGNAL_STRENGTH_DECIBELS_MILLIWATT = "dBm"
2121
TEMP_CELSIUS = "°C"
22+
TEMP_KELVIN = "°K"
2223
TIME_MILLISECONDS = "ms"
24+
UNIT = "unit"
25+
UNIT_LUMEN = "lm"
2326
VOLUME_CUBIC_METERS = "m³"
2427
VOLUME_CUBIC_METERS_PER_HOUR = "m³/h"
2528

@@ -388,10 +391,6 @@
388391
ATTR_TYPE: "gas",
389392
ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
390393
},
391-
"outdoor_temperature": {
392-
ATTR_TYPE: "temperature",
393-
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
394-
},
395394
}
396395

397396
# Excluded:
@@ -402,6 +401,7 @@
402401
"temperature": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
403402
# HA Core setpoint
404403
"thermostat": {ATTR_NAME: "setpoint", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
404+
"illuminance": {ATTR_UNIT_OF_MEASUREMENT: UNIT_LUMEN},
405405
"outdoor_temperature": {
406406
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS
407407
}, # Outdoor temp as reported on the Anna, in the App
@@ -511,9 +511,6 @@
511511
ATTR_ENABLED = "enabled_default"
512512
ATTR_ID = "id"
513513
ATTR_ICON = "icon"
514-
TEMP_KELVIN = "°K"
515-
UNIT = "unit"
516-
UNIT_LUMEN = "lm"
517514

518515
EXTRA_STATE_ATTRIBS = {}
519516

plugwise/helper.py

Lines changed: 39 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,12 @@ def update_device_state(data, d_dict):
8686
_heating_state = False
8787
state = "idle"
8888

89-
for _, item in enumerate(d_dict["binary_sensors"]):
90-
if item[ATTR_ID] == "dhw_state":
91-
if item[ATTR_STATE]:
92-
state = "dhw-heating"
93-
_dhw_state = True
89+
if "binary_sensors" in d_dict:
90+
for _, item in enumerate(d_dict["binary_sensors"]):
91+
if item[ATTR_ID] == "dhw_state":
92+
if item[ATTR_STATE]:
93+
state = "dhw-heating"
94+
_dhw_state = True
9495

9596
if "heating_state" in data:
9697
if data["heating_state"]:
@@ -168,10 +169,6 @@ def types_finder(data):
168169
locator = f".//logs/point_log[type='{measure}']"
169170
if data.find(locator) is not None:
170171
log = data.find(locator)
171-
172-
if measure == "outdoor_temperature":
173-
types.add(attrs[ATTR_TYPE])
174-
175172
p_locator = ".//electricity_point_meter"
176173
if log.find(p_locator) is not None:
177174
if log.find(p_locator).get("id"):
@@ -403,8 +400,8 @@ def _get_module_data(self, appliance, locator, mod_type):
403400
appl_search = appliance.find(locator)
404401
if appl_search is not None:
405402
link_id = appl_search.attrib["id"]
406-
module = self._modules.find(f".//{mod_type}[@id='{link_id}']....")
407-
if module is not None:
403+
locator = f".//{mod_type}[@id='{link_id}']...."
404+
if (module := self._modules.find(locator)) is not None:
408405
v_name = module.find("vendor_name").text
409406
v_model = module.find("vendor_model").text
410407
hw_version = module.find("hardware_version").text
@@ -447,6 +444,16 @@ def _appliance_info_finder(self, appliance, appl):
447444
appl.fw = self.smile_version[0]
448445
appl.model = appl.name = self.smile_name
449446
appl.v_name = "Plugwise B.V."
447+
448+
# Adam: check for cooling capability, assume heating capability is always present
449+
mode_list = []
450+
locator = "./actuator_functionalities/regulation_mode_control_functionality/allowed_modes"
451+
if appliance.find(locator) is not None:
452+
self._cooling_present = False
453+
for mode in appliance.find(locator):
454+
mode_list.append(mode.text)
455+
self._cooling_present = "cooling" in mode_list
456+
450457
return appl
451458

452459
if appl.pwclass in THERMOSTAT_CLASSES:
@@ -621,14 +628,15 @@ def _presets_legacy(self):
621628
def _presets(self, loc_id):
622629
"""Collect Presets for a Thermostat based on location_id."""
623630
presets = {}
624-
tag = "zone_setpoint_and_state_based_on_preset"
631+
tag_1 = "zone_setpoint_and_state_based_on_preset"
632+
tag_2 = "Thermostat presets"
625633

626634
if self._smile_legacy:
627635
return self._presets_legacy()
628636

629-
rule_ids = self._rule_ids_by_tag(tag, loc_id)
630-
if rule_ids is None:
631-
rule_ids = self._rule_ids_by_name("Thermostat presets", loc_id)
637+
if not (rule_ids := self._rule_ids_by_tag(tag_1, loc_id)):
638+
if not (rule_ids := self._rule_ids_by_name(tag_2, loc_id)):
639+
return presets
632640

633641
for rule_id in rule_ids:
634642
directives = self._domain_objects.find(f'rule[@id="{rule_id}"]/directives')
@@ -656,8 +664,7 @@ def _rule_ids_by_name(self, name, loc_id):
656664
if rule.find(locator) is not None:
657665
schema_ids[rule.attrib["id"]] = loc_id
658666

659-
if schema_ids != {}:
660-
return schema_ids
667+
return schema_ids
661668

662669
def _rule_ids_by_tag(self, tag, loc_id):
663670
"""Helper-function for _presets(), _schemas() and _last_active_schema().
@@ -671,13 +678,11 @@ def _rule_ids_by_tag(self, tag, loc_id):
671678
if rule.find(locator2) is not None:
672679
schema_ids[rule.attrib["id"]] = loc_id
673680

674-
if schema_ids != {}:
675-
return schema_ids
681+
return schema_ids
676682

677683
def _appliance_measurements(self, appliance, data, measurements):
678684
"""Helper-function for _get_appliance_data() - collect appliance measurement data."""
679685
for measurement, attrs in measurements:
680-
681686
p_locator = f'.//logs/point_log[type="{measurement}"]/period/measurement'
682687
if appliance.find(p_locator) is not None:
683688
if self._smile_legacy:
@@ -720,12 +725,7 @@ def _get_appliance_data(self, d_id):
720725
if self._smile_legacy and self.smile_type == "power":
721726
return data
722727

723-
search = self._appliances
724-
if self._smile_legacy and self.smile_type != "stretch":
725-
search = self._domain_objects
726-
727-
appliances = search.findall(f'.//appliance[@id="{d_id}"]')
728-
728+
appliances = self._appliances.findall(f'.//appliance[@id="{d_id}"]')
729729
for appliance in appliances:
730730
measurements = DEVICE_MEASUREMENTS.items()
731731
if self._active_device_present:
@@ -735,7 +735,6 @@ def _get_appliance_data(self, d_id):
735735
}.items()
736736

737737
data = self._appliance_measurements(appliance, data, measurements)
738-
739738
data.update(self._get_lock_state(appliance))
740739

741740
# Fix for Adam + Anna: heating_state also present under Anna, remove
@@ -918,8 +917,7 @@ def _power_data_peak_value(self, loc):
918917
loc.found = False
919918
return loc
920919

921-
peak = loc.peak_select.split("_")[1]
922-
if peak == "offpeak":
920+
if (peak := loc.peak_select.split("_")[1]) == "offpeak":
923921
peak = "off_peak"
924922
log_found = loc.log_type.split("_")[0]
925923
loc.key_string = f"{loc.measurement}_{peak}_{log_found}"
@@ -941,22 +939,17 @@ def _power_data_from_location(self, loc_id):
941939
direct_data = {}
942940
loc = Munch()
943941

944-
search = self._domain_objects
945-
t_string = "tariff"
946-
if self.smile_type == "power":
947-
# P1: use data from LOCATIONS
948-
search = self._locations
949-
if self._smile_legacy:
950-
t_string = "tariff_indicator"
951-
952-
loc.logs = search.find(f'.//location[@id="{loc_id}"]/logs')
953-
954-
if loc.logs is None:
942+
if self.smile_type != "power":
955943
return
956944

945+
search = self._locations
957946
log_list = ["point_log", "cumulative_log", "interval_log"]
958947
peak_list = ["nl_peak", "nl_offpeak"]
948+
t_string = "tariff"
949+
if self._smile_legacy:
950+
t_string = "tariff_indicator"
959951

952+
loc.logs = search.find(f'.//location[@id="{loc_id}"]/logs')
960953
# meter_string = ".//{}[type='{}']/"
961954
for loc.measurement, loc.attrs in HOME_MEASUREMENTS.items():
962955
for loc.log_type in log_list:
@@ -965,19 +958,16 @@ def _power_data_from_location(self, loc_id):
965958
f'.//{loc.log_type}[type="{loc.measurement}"]/period/'
966959
f'measurement[@{t_string}="{loc.peak_select}"]'
967960
)
968-
969961
loc = self._power_data_peak_value(loc)
970962
if not loc.found:
971963
continue
972964

973965
direct_data = power_data_energy_diff(
974966
loc.measurement, loc.net_string, loc.f_val, direct_data
975967
)
976-
977968
direct_data[loc.key_string] = loc.f_val
978969

979-
if direct_data != {}:
980-
return direct_data
970+
return direct_data
981971

982972
def _preset(self, loc_id):
983973
"""Helper-function for smile.py: device_data_climate().
@@ -1007,8 +997,7 @@ def _schemas_legacy(self):
1007997
selected = None
1008998

1009999
for schema in self._domain_objects.findall(".//rule"):
1010-
rule_name = schema.find("name").text
1011-
if rule_name:
1000+
if rule_name := schema.find("name").text:
10121001
if "preset" not in rule_name:
10131002
name = rule_name
10141003

@@ -1040,9 +1029,7 @@ def _schemas(self, loc_id):
10401029

10411030
# Current schemas
10421031
tag = "zone_preset_based_on_time_and_presence_with_override"
1043-
rule_ids = self._rule_ids_by_tag(tag, loc_id)
1044-
1045-
if rule_ids is None:
1032+
if not (rule_ids := self._rule_ids_by_tag(tag, loc_id)):
10461033
return available, selected, schedule_temperature
10471034

10481035
for rule_id, dummy in rule_ids.items():
@@ -1082,8 +1069,7 @@ def _last_active_schema(self, loc_id):
10821069

10831070
tag = "zone_preset_based_on_time_and_presence_with_override"
10841071

1085-
rule_ids = self._rule_ids_by_tag(tag, loc_id)
1086-
if rule_ids is None:
1072+
if not (rule_ids := self._rule_ids_by_tag(tag, loc_id)):
10871073
return
10881074

10891075
for rule_id, dummy in rule_ids.items():
@@ -1099,14 +1085,13 @@ def _last_active_schema(self, loc_id):
10991085

11001086
return last_modified
11011087

1102-
def _object_value(self, obj_type, obj_id, measurement):
1088+
def _object_value(self, obj_id, measurement):
11031089
"""Helper-function for smile.py: _get_device_data() and _device_data_anna().
11041090
Obtain the value/state for the given object.
11051091
"""
11061092
search = self._domain_objects
1107-
11081093
locator = (
1109-
f'.//{obj_type}[@id="{obj_id}"]/logs/point_log'
1094+
f'.//location[@id="{obj_id}"]/logs/point_log'
11101095
f'[type="{measurement}"]/period/measurement'
11111096
)
11121097
if search.find(locator) is not None:

0 commit comments

Comments
 (0)