Skip to content

Commit 52b9515

Browse files
authored
Merge pull request #135 from plugwise/add_measures
Improve/provide original details
2 parents d876418 + 30dfcde commit 52b9515

File tree

8 files changed

+180
-93
lines changed

8 files changed

+180
-93
lines changed

.github/workflows/merge.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
name: Latest release
55

66
env:
7-
CACHE_VERSION: 5
7+
CACHE_VERSION: 6
88
DEFAULT_PYTHON: 3.9
99

1010
# Only run on merges

.github/workflows/verify.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
name: Latest commit
55

66
env:
7-
CACHE_VERSION: 5
7+
CACHE_VERSION: 6
88
DEFAULT_PYTHON: "3.9"
99
PRE_COMMIT_HOME: ~/.cache/pre-commit
1010

CHANGELOG.md

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

3-
- ongoing
3+
# v0.16.4 - Adding measurements
4+
- Expose mac-addresses for network and zigbee devices
5+
- Expose min/max thermostat (and heater) values and resolution (step in HA)
46
- Changed mac-addresses in userdata/fixtures to be obfuscated but unique
57

8+
# v0.16.3 - Typing
9+
- Code quality improvements
10+
611
# v0.16.2 - Generic and Stretch
712
- As per Core deprecation of python 3.8, removed CI/CD testing and bumped pypi to 3.9 and production
813
- Add support for Stretch with fw 2.7.18

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

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

plugwise/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,10 @@
432432
"electricity_consumed": {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT},
433433
"electricity_produced": {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT},
434434
"relay": {ATTR_UNIT_OF_MEASUREMENT: None},
435+
# Added measurements from actuator_functionalities/thermostat_functionality
436+
"lower_bound": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
437+
"upper_bound": {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
438+
"resolution": {ATTR_UNIT_OF_MEASUREMENT: None},
435439
}
436440

437441
HEATER_CENTRAL_MEASUREMENTS = {

plugwise/helper.py

Lines changed: 105 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,16 @@ def __init__(self):
315315
self._thermo_locs: dict[str, Any] = {}
316316

317317
self.cooling_active = False
318+
self.smile_fw_version: str | None = None
318319
self.gateway_id: str | None = None
319320
self.gw_data: dict[str, Any] = {}
320321
self.gw_devices: dict[str, Any] = {}
322+
self.smile_hw_version: str | None = None
323+
self.smile_mac_address: str | None = None
321324
self.smile_name: str | None = None
322325
self.smile_type: str | None = None
323326
self.smile_version: list[str] = []
327+
self.smile_zigbee_mac_address: str | None = None
324328

325329
def _locations_legacy(self) -> None:
326330
"""Helper-function for _all_locations().
@@ -414,18 +418,32 @@ def _get_module_data(
414418
Collect requested info from MODULES.
415419
"""
416420
appl_search = appliance.find(locator)
421+
model_data = {
422+
"contents": False,
423+
"vendor_name": None,
424+
"vendor_model": None,
425+
"hardware_version": None,
426+
"firmware_version": None,
427+
"zigbee_mac_address": None,
428+
}
417429
if appl_search is not None:
418430
link_id = appl_search.attrib["id"]
419431
locator = f".//{mod_type}[@id='{link_id}']...."
420432
module = self._modules.find(locator)
421433
if module is not None:
422-
v_name = module.find("vendor_name").text
423-
v_model = module.find("vendor_model").text
424-
hw_version = module.find("hardware_version").text
425-
fw_version = module.find("firmware_version").text
426-
427-
return [v_name, v_model, hw_version, fw_version]
428-
return [None, None, None, None]
434+
model_data["contents"] = True
435+
model_data["vendor_name"] = module.find("vendor_name").text
436+
model_data["vendor_model"] = module.find("vendor_model").text
437+
model_data["hardware_version"] = module.find("hardware_version").text
438+
model_data["firmware_version"] = module.find("firmware_version").text
439+
# Adam
440+
if found := module.find(".//protocols/zig_bee_node"):
441+
model_data["zigbee_mac_address"] = found.find("mac_address").text
442+
# Stretches
443+
if found := module.find(".//protocols/network_router"):
444+
model_data["zigbee_mac_address"] = found.find("mac_address").text
445+
446+
return model_data
429447

430448
def _energy_device_info_finder(self, appliance: etree, appl: Munch) -> Munch:
431449
"""Helper-function for _appliance_info_finder().
@@ -435,33 +453,47 @@ def _energy_device_info_finder(self, appliance: etree, appl: Munch) -> Munch:
435453
locator = ".//services/electricity_point_meter"
436454
mod_type = "electricity_point_meter"
437455
module_data = self._get_module_data(appliance, locator, mod_type)
438-
appl.v_name = module_data[0]
456+
if not module_data["contents"]:
457+
return None
458+
459+
appl.v_name = module_data["vendor_name"]
439460
if appl.model != "Switchgroup":
440461
appl.model = None
441-
if module_data[2] is not None:
442-
hw_version = module_data[2].replace("-", "")
462+
appl.hw = module_data["hardware_version"]
463+
if appl.hw:
464+
hw_version = module_data["hardware_version"].replace("-", "")
443465
appl.model = version_to_model(hw_version)
444-
appl.fw = module_data[3]
466+
appl.fw = module_data["firmware_version"]
467+
appl.zigbee_mac = module_data["zigbee_mac_address"]
445468
return appl
446469

447470
if self.smile_type != "stretch" and "plug" in appl.types:
448471
locator = ".//logs/point_log/electricity_point_meter"
449472
mod_type = "electricity_point_meter"
450473
module_data = self._get_module_data(appliance, locator, mod_type)
451-
appl.v_name = module_data[0]
452-
appl.model = version_to_model(module_data[1])
453-
appl.fw = module_data[3]
474+
appl.v_name = module_data["vendor_name"]
475+
appl.model = version_to_model(module_data["vendor_model"])
476+
appl.hw = module_data["hardware_version"]
477+
appl.fw = module_data["firmware_version"]
478+
appl.zigbee_mac = module_data["zigbee_mac_address"]
454479
return appl
455480

456481
def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
457482
"""Collect device info (Smile/Stretch, Thermostats, OpenTherm/On-Off): firmware, model and vendor name."""
458483
# Find gateway and heater_central devices
459484
if appl.pwclass == "gateway":
460485
self.gateway_id = appliance.attrib["id"]
461-
appl.fw = self.smile_version[0]
486+
appl.fw = self.smile_fw_version
487+
appl.mac = self.smile_mac_address
462488
appl.model = appl.name = self.smile_name
463489
appl.v_name = "Plugwise B.V."
464490

491+
# Adam: check for ZigBee mac address
492+
if self.smile_name == "Adam" and (
493+
found := self._domain_objects.find(".//protocols/zig_bee_coordinator")
494+
):
495+
appl.zigbee_mac = found.find("mac_address").text
496+
465497
# Adam: check for cooling capability and active heating/cooling operation-mode
466498
mode_list: list[str] = []
467499
locator = "./actuator_functionalities/regulation_mode_control_functionality"
@@ -480,9 +512,10 @@ def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
480512
locator = ".//logs/point_log[type='thermostat']/thermostat"
481513
mod_type = "thermostat"
482514
module_data = self._get_module_data(appliance, locator, mod_type)
483-
appl.v_name = module_data[0]
484-
appl.model = check_model(module_data[1], appl.v_name)
485-
appl.fw = module_data[3]
515+
appl.v_name = module_data["vendor_name"]
516+
appl.model = check_model(module_data["vendor_model"], appl.v_name)
517+
appl.hw = module_data["hardware_version"]
518+
appl.fw = module_data["firmware_version"]
486519

487520
return appl
488521

@@ -506,10 +539,11 @@ def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
506539
locator2 = ".//services/boiler_state"
507540
mod_type = "boiler_state"
508541
module_data = self._get_module_data(appliance, locator1, mod_type)
509-
if module_data == [None, None, None, None]:
542+
if not module_data["contents"]:
510543
module_data = self._get_module_data(appliance, locator2, mod_type)
511-
appl.v_name = module_data[0]
512-
appl.model = check_model(module_data[1], appl.v_name)
544+
appl.v_name = module_data["vendor_name"]
545+
appl.hw = module_data["hardware_version"]
546+
appl.model = check_model(module_data["vendor_model"], appl.v_name)
513547
if appl.model is None:
514548
appl.model = (
515549
"Generic heater/cooler"
@@ -519,7 +553,9 @@ def _appliance_info_finder(self, appliance: etree, appl: Munch) -> Munch:
519553
return appl
520554

521555
# Handle stretches
522-
self._energy_device_info_finder(appliance, appl)
556+
appl = self._energy_device_info_finder(appliance, appl)
557+
if not appl:
558+
return None
523559

524560
# Cornercase just return existing dict-object
525561
return appl # pragma: no cover
@@ -560,7 +596,9 @@ def _all_appliances(self) -> None:
560596
if self._smile_legacy:
561597
self._appl_data[self._home_location] = {
562598
"class": "gateway",
563-
"fw": self.smile_version[0],
599+
"fw": self.smile_fw_version,
600+
"hw": self.smile_hw_version,
601+
"mac_address": self.smile_mac_address,
564602
"location": self._home_location,
565603
"vendor": "Plugwise B.V.",
566604
}
@@ -580,7 +618,11 @@ def _all_appliances(self) -> None:
580618

581619
if self.smile_type == "stretch":
582620
self._appl_data[self._home_location].update(
583-
{"model": "Stretch", "name": "Stretch"}
621+
{
622+
"model": "Stretch",
623+
"name": "Stretch",
624+
"zigbee_mac_address": self.smile_zigbee_mac_address,
625+
}
584626
)
585627

586628
# The presence of either indicates a local active device, e.g. heat-pump or gas-fired heater
@@ -609,25 +651,42 @@ def _all_appliances(self) -> None:
609651
appl.name = appliance.find("name").text
610652
appl.model = appl.pwclass.replace("_", " ").title()
611653
appl.fw = None
654+
appl.hw = None
655+
appl.mac = None
656+
appl.zigbee_mac = None
612657
appl.v_name = None
613658

614659
# Determine types for this appliance
615660
appl = self._appliance_types_finder(appliance, appl)
616661

617662
# Determine class for this appliance
618663
appl = self._appliance_info_finder(appliance, appl)
619-
# Skip on heater_central when no active device present
664+
# Skip on heater_central when no active device present or on orphaned stretch devices
620665
if not appl:
621666
continue
622667

668+
if appl.pwclass == "gateway":
669+
appl.fw = self.smile_fw_version
670+
appl.hw = self.smile_hw_version
671+
623672
self._appl_data[appl.dev_id] = {
624673
"class": appl.pwclass,
625674
"fw": appl.fw,
675+
"hw": appl.hw,
626676
"location": appl.location,
677+
"mac_address": appl.mac,
627678
"model": appl.model,
628679
"name": appl.name,
629680
"vendor": appl.v_name,
630681
}
682+
683+
if appl.zigbee_mac:
684+
self._appl_data[appl.dev_id].update(
685+
{
686+
"zigbee_mac_address": appl.zigbee_mac,
687+
}
688+
)
689+
631690
if (
632691
not self._smile_legacy
633692
and appl.pwclass == "thermostat"
@@ -777,6 +836,22 @@ def _appliance_measurements(
777836
measure = appliance.find(i_locator).text
778837
data[name] = format_measure(measure, ENERGY_WATT_HOUR)
779838

839+
t_locator = (
840+
f".//actuator_functionalities/thermostat_functionality/{measurement}"
841+
)
842+
t_functions = appliance.find(t_locator)
843+
if t_functions is not None and t_functions.text:
844+
# Thermostat actuator measurements
845+
846+
try:
847+
measurement = attrs[ATTR_NAME]
848+
except KeyError:
849+
pass
850+
851+
data[measurement] = format_measure(
852+
t_functions.text, attrs[ATTR_UNIT_OF_MEASUREMENT]
853+
)
854+
780855
return data
781856

782857
def _get_appliance_data(self, d_id: str) -> dict[str, Any]:
@@ -900,20 +975,20 @@ def _scan_thermostats(self, debug_text="missing text") -> None:
900975
if thermo_matching[appl_class] > high_prio:
901976
high_prio = thermo_matching[appl_class]
902977

903-
def _temperature_uri_legacy(self) -> str:
904-
"""Helper-function for _temperature_uri().
978+
def _thermostat_uri_legacy(self) -> str:
979+
"""Helper-function for _thermostat_uri().
905980
Determine the location-set_temperature uri - from APPLIANCES.
906981
"""
907982
locator = ".//appliance[type='thermostat']"
908983
appliance_id = self._appliances.find(locator).attrib["id"]
909984

910985
return f"{APPLIANCES};id={appliance_id}/thermostat"
911986

912-
def _temperature_uri(self, loc_id: str) -> str:
987+
def _thermostat_uri(self, loc_id: str) -> str:
913988
"""Helper-function for smile.py: set_temperature().
914989
Determine the location-set_temperature uri - from LOCATIONS."""
915990
if self._smile_legacy:
916-
return self._temperature_uri_legacy()
991+
return self._thermostat_uri_legacy()
917992

918993
locator = f'location[@id="{loc_id}"]/actuator_functionalities/thermostat_functionality'
919994
thermostat_functionality_id = self._locations.find(locator).attrib["id"]
@@ -1214,7 +1289,7 @@ def _get_lock_state(self, xml: str) -> dict[str, Any]:
12141289
data: dict[str, Any] = {}
12151290
actuator = "actuator_functionalities"
12161291
func_type = "relay_functionality"
1217-
if self.smile_type == "stretch" and self.smile_version[1].major == 2:
1292+
if self._stretch_v2:
12181293
actuator = "actuators"
12191294
func_type = "relay"
12201295
appl_class = xml.find("type").text

0 commit comments

Comments
 (0)