|
| 1 | +"""Use of this source code is governed by the MIT license found in the LICENSE file. |
| 2 | +
|
| 3 | +Plugwise Smile protocol helpers. |
| 4 | +""" |
| 5 | +from __future__ import annotations |
| 6 | + |
| 7 | +from plugwise.constants import ModelData |
| 8 | +from plugwise.util import ( |
| 9 | + check_heater_central, |
| 10 | + check_model, |
| 11 | + get_vendor_name, |
| 12 | + return_valid, |
| 13 | +) |
| 14 | + |
| 15 | +from defusedxml import ElementTree as etree |
| 16 | +from munch import Munch |
| 17 | + |
| 18 | + |
| 19 | +class SmileCommon: |
| 20 | + """The SmileCommon class.""" |
| 21 | + |
| 22 | + def __init__(self) -> None: |
| 23 | + """Init.""" |
| 24 | + self._appliances: etree |
| 25 | + self._domain_objects: etree |
| 26 | + self._cooling_present: bool |
| 27 | + self._heater_id: str |
| 28 | + self._on_off_device: bool |
| 29 | + self._opentherm_device: bool |
| 30 | + self.smile_name: str |
| 31 | + |
| 32 | + def smile(self, name: str) -> bool: |
| 33 | + """Helper-function checking the smile-name.""" |
| 34 | + return self.smile_name == name |
| 35 | + |
| 36 | + def _appl_thermostat_info(self, appl: Munch, xml_1: etree, xml_2: etree = None) -> Munch: |
| 37 | + """Helper-function for _appliance_info_finder().""" |
| 38 | + locator = "./logs/point_log[type='thermostat']/thermostat" |
| 39 | + mod_type = "thermostat" |
| 40 | + xml_2 = return_valid(xml_2, self._domain_objects) |
| 41 | + module_data = self._get_module_data(xml_1, locator, mod_type, xml_2) |
| 42 | + appl.vendor_name = module_data["vendor_name"] |
| 43 | + appl.model = check_model(module_data["vendor_model"], appl.vendor_name) |
| 44 | + appl.hardware = module_data["hardware_version"] |
| 45 | + appl.firmware = module_data["firmware_version"] |
| 46 | + appl.zigbee_mac = module_data["zigbee_mac_address"] |
| 47 | + |
| 48 | + return appl |
| 49 | + |
| 50 | + def _appl_heater_central_info( |
| 51 | + self, |
| 52 | + appl: Munch, |
| 53 | + xml_1: etree, |
| 54 | + xml_2: etree = None, |
| 55 | + xml_3: etree = None, |
| 56 | + ) -> Munch: |
| 57 | + """Helper-function for _appliance_info_finder().""" |
| 58 | + # Remove heater_central when no active device present |
| 59 | + if not self._opentherm_device and not self._on_off_device: |
| 60 | + return None |
| 61 | + |
| 62 | + # Find the valid heater_central |
| 63 | + # xml_2 self._appliances for legacy, self._domain_objects for actual |
| 64 | + xml_2 = return_valid(xml_2, self._domain_objects) |
| 65 | + self._heater_id = check_heater_central(xml_2) |
| 66 | + |
| 67 | + # Info for On-Off device |
| 68 | + if self._on_off_device: |
| 69 | + appl.name = "OnOff" # pragma: no cover |
| 70 | + appl.vendor_name = None # pragma: no cover |
| 71 | + appl.model = "Unknown" # pragma: no cover |
| 72 | + return appl # pragma: no cover |
| 73 | + |
| 74 | + # Info for OpenTherm device |
| 75 | + appl.name = "OpenTherm" |
| 76 | + locator_1 = "./logs/point_log[type='flame_state']/boiler_state" |
| 77 | + locator_2 = "./services/boiler_state" |
| 78 | + mod_type = "boiler_state" |
| 79 | + # xml_1: appliance |
| 80 | + # xml_3: self._modules for legacy, self._domain_objects for actual |
| 81 | + xml_3 = return_valid(xml_3, self._domain_objects) |
| 82 | + module_data = self._get_module_data(xml_1, locator_1, mod_type, xml_3) |
| 83 | + if not module_data["contents"]: |
| 84 | + module_data = self._get_module_data(xml_1, locator_2, mod_type, xml_3) |
| 85 | + appl.vendor_name = module_data["vendor_name"] |
| 86 | + appl.hardware = module_data["hardware_version"] |
| 87 | + appl.model = module_data["vendor_model"] |
| 88 | + if appl.model is None: |
| 89 | + appl.model = ( |
| 90 | + "Generic heater/cooler" |
| 91 | + if self._cooling_present |
| 92 | + else "Generic heater" |
| 93 | + ) |
| 94 | + |
| 95 | + return appl |
| 96 | + |
| 97 | + def _get_module_data( |
| 98 | + self, |
| 99 | + xml_1: etree, |
| 100 | + locator: str, |
| 101 | + mod_type: str, |
| 102 | + xml_2: etree = None, |
| 103 | + legacy: bool = False, |
| 104 | + ) -> ModelData: |
| 105 | + """Helper-function for _energy_device_info_finder() and _appliance_info_finder(). |
| 106 | +
|
| 107 | + Collect requested info from MODULES. |
| 108 | + """ |
| 109 | + model_data: ModelData = { |
| 110 | + "contents": False, |
| 111 | + "firmware_version": None, |
| 112 | + "hardware_version": None, |
| 113 | + "reachable": None, |
| 114 | + "vendor_name": None, |
| 115 | + "vendor_model": None, |
| 116 | + "zigbee_mac_address": None, |
| 117 | + } |
| 118 | + # xml_1: appliance |
| 119 | + if (appl_search := xml_1.find(locator)) is not None: |
| 120 | + link_id = appl_search.attrib["id"] |
| 121 | + loc = f".//services/{mod_type}[@id='{link_id}']...." |
| 122 | + if legacy: |
| 123 | + loc = f".//{mod_type}[@id='{link_id}']...." |
| 124 | + # Not possible to walrus for some reason... |
| 125 | + # xml_2: self._modules for legacy, self._domain_objects for actual |
| 126 | + search = return_valid(xml_2, self._domain_objects) |
| 127 | + module = search.find(loc) |
| 128 | + if module is not None: # pylint: disable=consider-using-assignment-expr |
| 129 | + model_data["contents"] = True |
| 130 | + get_vendor_name(module, model_data) |
| 131 | + model_data["vendor_model"] = module.find("vendor_model").text |
| 132 | + model_data["hardware_version"] = module.find("hardware_version").text |
| 133 | + model_data["firmware_version"] = module.find("firmware_version").text |
| 134 | + self._get_zigbee_data(module, model_data, legacy) |
| 135 | + |
| 136 | + return model_data |
| 137 | + |
| 138 | + def _get_zigbee_data(self, module: etree, model_data: ModelData, legacy: bool) -> None: |
| 139 | + """Helper-function for _get_model_data().""" |
| 140 | + if legacy: |
| 141 | + # Stretches |
| 142 | + if (router := module.find("./protocols/network_router")) is not None: |
| 143 | + model_data["zigbee_mac_address"] = router.find("mac_address").text |
| 144 | + # Also look for the Circle+/Stealth M+ |
| 145 | + if (coord := module.find("./protocols/network_coordinator")) is not None: |
| 146 | + model_data["zigbee_mac_address"] = coord.find("mac_address").text |
| 147 | + # Adam |
| 148 | + elif (zb_node := module.find("./protocols/zig_bee_node")) is not None: |
| 149 | + model_data["zigbee_mac_address"] = zb_node.find("mac_address").text |
| 150 | + model_data["reachable"] = zb_node.find("reachable").text == "true" |
| 151 | + |
0 commit comments