diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0c55cfa..a6d3133e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.5.2 + +- Bugfix for Adam: improve recognition of unknown zigbee devices. + ## v1.5.1 - Fix typing and rounding of P1 and thermostat sensors, energy-device-related code improvements. diff --git a/fixtures/adam_plus_anna_new/all_data.json b/fixtures/adam_plus_anna_new/all_data.json index a318023fc..4727e8967 100644 --- a/fixtures/adam_plus_anna_new/all_data.json +++ b/fixtures/adam_plus_anna_new/all_data.json @@ -25,6 +25,18 @@ "dhw_cm_switch": false } }, + "10016900610d4c7481df78c89606ef22": { + "available": true, + "dev_class": "valve_actuator_plug", + "location": "d9786723dbcf4f19b5c629a54629f9c7", + "model_id": "TS0011", + "name": "Aanvoer water afsluiter (nous lz3)", + "switches": { + "relay": false + }, + "vendor": "_TZ3000_abjodzas", + "zigbee_mac_address": "A4C13862AF9917B1" + }, "1772a4ea304041adb83f357b751341ff": { "available": true, "binary_sensors": { @@ -266,7 +278,7 @@ "cooling_present": false, "gateway_id": "da224107914542988a88561b4452b0f6", "heater_id": "056ee145a816487eaa69243c3280f8bf", - "item_count": 157, + "item_count": 165, "notifications": {}, "reboot": true, "smile_name": "Adam" diff --git a/fixtures/m_adam_cooling/all_data.json b/fixtures/m_adam_cooling/all_data.json index ffda247bb..90d9ab8d9 100644 --- a/fixtures/m_adam_cooling/all_data.json +++ b/fixtures/m_adam_cooling/all_data.json @@ -189,7 +189,7 @@ "cooling_present": true, "gateway_id": "da224107914542988a88561b4452b0f6", "heater_id": "056ee145a816487eaa69243c3280f8bf", - "item_count": 157, + "item_count": 89, "notifications": {}, "reboot": true, "smile_name": "Adam" diff --git a/fixtures/m_adam_heating/all_data.json b/fixtures/m_adam_heating/all_data.json index bc7fee930..7e6a5a103 100644 --- a/fixtures/m_adam_heating/all_data.json +++ b/fixtures/m_adam_heating/all_data.json @@ -193,7 +193,7 @@ "cooling_present": false, "gateway_id": "da224107914542988a88561b4452b0f6", "heater_id": "056ee145a816487eaa69243c3280f8bf", - "item_count": 157, + "item_count": 89, "notifications": {}, "reboot": true, "smile_name": "Adam" diff --git a/plugwise/common.py b/plugwise/common.py index b3a327bc3..ee6c22e99 100644 --- a/plugwise/common.py +++ b/plugwise/common.py @@ -73,13 +73,12 @@ def _appl_heater_central_info( appl.name = "OpenTherm" locator_1 = "./logs/point_log[type='flame_state']/boiler_state" locator_2 = "./services/boiler_state" - mod_type = "boiler_state" # xml_1: appliance # xml_3: self._modules for legacy, self._domain_objects for actual xml_3 = return_valid(xml_3, self._domain_objects) - module_data = self._get_module_data(xml_1, locator_1, mod_type, xml_3) + module_data = self._get_module_data(xml_1, locator_1, xml_3) if not module_data["contents"]: - module_data = self._get_module_data(xml_1, locator_2, mod_type, xml_3) + module_data = self._get_module_data(xml_1, locator_2, xml_3) appl.vendor_name = module_data["vendor_name"] appl.hardware = module_data["hardware_version"] appl.model_id = module_data["vendor_model"] if not legacy else None @@ -94,15 +93,15 @@ def _appl_heater_central_info( def _appl_thermostat_info(self, appl: Munch, xml_1: etree, xml_2: etree = None) -> Munch: """Helper-function for _appliance_info_finder().""" locator = "./logs/point_log[type='thermostat']/thermostat" - mod_type = "thermostat" xml_2 = return_valid(xml_2, self._domain_objects) - module_data = self._get_module_data(xml_1, locator, mod_type, xml_2) + module_data = self._get_module_data(xml_1, locator, xml_2) appl.vendor_name = module_data["vendor_name"] appl.model = module_data["vendor_model"] if appl.model != "ThermoTouch": # model_id for Anna not present as stand-alone device appl.model_id = appl.model appl.model = check_model(appl.model, appl.vendor_name) + appl.available = module_data["reachable"] appl.hardware = module_data["hardware_version"] appl.firmware = module_data["firmware_version"] appl.zigbee_mac = module_data["zigbee_mac_address"] @@ -192,6 +191,7 @@ def _create_gw_devices(self, appl: Munch) -> None: self.gw_devices[appl.dev_id] = {"dev_class": appl.pwclass} self._count += 1 for key, value in { + "available": appl.available, "firmware": appl.firmware, "hardware": appl.hardware, "location": appl.location, @@ -278,7 +278,6 @@ def _get_module_data( self, xml_1: etree, locator: str, - mod_type: str, xml_2: etree = None, legacy: bool = False, ) -> ModelData: @@ -295,12 +294,11 @@ def _get_module_data( "vendor_model": None, "zigbee_mac_address": None, } - # xml_1: appliance + if (appl_search := xml_1.find(locator)) is not None: + link_tag = appl_search.tag link_id = appl_search.attrib["id"] - loc = f".//services/{mod_type}[@id='{link_id}']...." - if legacy: - loc = f".//{mod_type}[@id='{link_id}']...." + loc = f".//services/{link_tag}[@id='{link_id}']...." # Not possible to walrus for some reason... # xml_2: self._modules for legacy, self._domain_objects for actual search = return_valid(xml_2, self._domain_objects) diff --git a/plugwise/constants.py b/plugwise/constants.py index da9cfe6e0..3830e5d45 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -81,6 +81,7 @@ MAX_SETPOINT: Final[float] = 30.0 MIN_SETPOINT: Final[float] = 4.0 +MODULE_LOCATOR: Final = "./logs/point_log/*[@id]" NONE: Final = "None" OFF: Final = "off" diff --git a/plugwise/helper.py b/plugwise/helper.py index 0c298471a..67a123887 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -24,6 +24,7 @@ LIMITS, LOCATIONS, LOGGER, + MODULE_LOCATOR, NONE, OFF, P1_MEASUREMENTS, @@ -299,6 +300,7 @@ def _all_appliances(self) -> None: if appl.pwclass in THERMOSTAT_CLASSES and appl.location is None: continue + appl.available = None appl.dev_id = appliance.attrib["id"] appl.name = appliance.find("name").text appl.model = None @@ -356,9 +358,8 @@ def _p1_smartmeter_info_finder(self, appl: Munch) -> None: """Collect P1 DSMR SmartMeter info.""" loc_id = next(iter(self.loc_data.keys())) location = self._domain_objects.find(f'./location[@id="{loc_id}"]') - locator = "./logs/point_log/electricity_point_meter" - mod_type = "electricity_point_meter" - module_data = self._get_module_data(location, locator, mod_type) + locator = MODULE_LOCATOR + module_data = self._get_module_data(location, locator) if not module_data["contents"]: LOGGER.error("No module data found for SmartMeter") # pragma: no cover return None # pragma: no cover @@ -396,13 +397,13 @@ def _appliance_info_finder(self, appl: Munch, appliance: etree) -> Munch: return appl case _ as s if s.endswith("_plug"): # Collect info from plug-types (Plug, Aqara Smart Plug) - locator = "./logs/interval_log/electricity_interval_meter" - mod_type = "electricity_interval_meter" - module_data = self._get_module_data(appliance, locator, mod_type) + locator = MODULE_LOCATOR + module_data = self._get_module_data(appliance, locator) # A plug without module-data is orphaned/ no present if not module_data["contents"]: return Munch() + appl.available = module_data["reachable"] appl.firmware = module_data["firmware_version"] appl.hardware = module_data["hardware_version"] appl.model_id = module_data["vendor_model"] @@ -515,9 +516,6 @@ def _get_measurement_data(self, dev_id: str) -> DeviceData: if appliance.find("type").text in ACTUATOR_CLASSES: self._get_actuator_functionalities(appliance, device, data) - # Collect availability-status for wireless connected devices to Adam - self._wireless_availability(appliance, data) - if dev_id == self.gateway_id and self.smile(ADAM): self._get_regulation_mode(appliance, data) self._get_gateway_mode(appliance, data) @@ -710,29 +708,6 @@ def _get_actuator_functionalities( act_item = cast(ActuatorType, item) data[act_item] = temp_dict - def _wireless_availability(self, appliance: etree, data: DeviceData) -> None: - """Helper-function for _get_measurement_data(). - - Collect the availability-status for wireless connected devices. - """ - if self.smile(ADAM): - # Try collecting for a Plug - locator = "./logs/interval_log/electricity_interval_meter" - mod_type = "electricity_interval_meter" - module_data = self._get_module_data(appliance, locator, mod_type) - if not module_data["contents"]: - # Try collecting for a wireless thermostat - locator = "./logs/point_log[type='thermostat']/thermostat" - mod_type = "thermostat" - module_data = self._get_module_data(appliance, locator, mod_type) - if not module_data["contents"]: - LOGGER.error("No module data found for Plug or wireless thermostat") # pragma: no cover - return None # pragma: no cover - - if module_data["reachable"] is not None: - data["available"] = module_data["reachable"] - self._count += 1 - def _get_regulation_mode(self, appliance: etree, data: DeviceData) -> None: """Helper-function for _get_measurement_data(). diff --git a/plugwise/legacy/helper.py b/plugwise/legacy/helper.py index a82b7005b..a9a62cb88 100644 --- a/plugwise/legacy/helper.py +++ b/plugwise/legacy/helper.py @@ -125,6 +125,7 @@ def _all_appliances(self) -> None: appl.pwclass = "heater_central_plug" appl.model = appl.pwclass.replace("_", " ").title() + appl.available = None appl.model_id = None appl.firmware = None appl.hardware = None @@ -230,9 +231,7 @@ def _energy_device_info_finder(self, appliance: etree, appl: Munch) -> Munch: """ if self.smile_type in ("power", "stretch"): locator = "./services/electricity_point_meter" - mod_type = "electricity_point_meter" - - module_data = self._get_module_data(appliance, locator, mod_type, self._modules, legacy=True) + module_data = self._get_module_data(appliance, locator, self._modules, legacy=True) appl.zigbee_mac = module_data["zigbee_mac_address"] # Filter appliance without zigbee_mac, it's an orphaned device if appl.zigbee_mac is None and self.smile_type != "power": @@ -253,6 +252,7 @@ def _energy_device_info_finder(self, appliance: etree, appl: Munch) -> Munch: def _p1_smartmeter_info_finder(self, appl: Munch) -> None: """Collect P1 DSMR Smartmeter info.""" loc_id = next(iter(self.loc_data.keys())) + appl.available = None appl.dev_id = loc_id appl.location = loc_id appl.mac = None diff --git a/plugwise/util.py b/plugwise/util.py index 1012deee7..5798feb28 100644 --- a/plugwise/util.py +++ b/plugwise/util.py @@ -115,7 +115,7 @@ def check_model(name: str | None, vendor_name: str | None) -> str | None: if name is not None and "lumi.plug" in name: return "Aqara Smart Plug" - return name # pragma: no cover + return None def common_match_cases( diff --git a/pyproject.toml b/pyproject.toml index c726b8ca9..ebc2b3cde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise" -version = "1.5.1" +version = "1.5.2" license = {file = "LICENSE"} description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3." readme = "README.md" diff --git a/scripts/manual_fixtures.py b/scripts/manual_fixtures.py index 7e236b451..1ec96d80d 100755 --- a/scripts/manual_fixtures.py +++ b/scripts/manual_fixtures.py @@ -65,11 +65,13 @@ def json_writer(manual_name: str, all_data: dict) -> None: m_adam_cooling = base.copy() -# Set cooling_present to true +# Set cooling_present to true, item_count to 89 m_adam_cooling["gateway"]["cooling_present"] = True +m_adam_cooling["gateway"]["item_count"] = 89 -# Remove device "67d73d0bd469422db25a618a5fb8eeb0" from anywhere +# Remove devices "67d73d0bd469422db25a618a5fb8eeb0" and "10016900610d4c7481df78c89606ef22" from anywhere m_adam_cooling["devices"].pop("67d73d0bd469422db25a618a5fb8eeb0") +m_adam_cooling["devices"].pop("10016900610d4c7481df78c89606ef22") # Correct setpoint for "ad4838d7d35c4d6ea796ee12ae5aedf8" m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["thermostat"][ diff --git a/tests/test_adam.py b/tests/test_adam.py index 4c2e812ff..2a2f95116 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -36,10 +36,11 @@ async def test_connect_adam_plus_anna_new(self): assert smile.gateway_id == "da224107914542988a88561b4452b0f6" assert smile._last_active["f2bf9048bef64cc5b6d5110154e33c81"] == "Weekschema" assert smile._last_active["f871b8c4d63549319221e294e4f88074"] == "Badkamer" - assert self.device_items == 157 + assert self.device_items == 165 assert self.device_list == [ "da224107914542988a88561b4452b0f6", "056ee145a816487eaa69243c3280f8bf", + "10016900610d4c7481df78c89606ef22", "67d73d0bd469422db25a618a5fb8eeb0", "e2f4322d57924fa090fbbc48b3a140dc", "29542b2b6a6a4169acecc15c72a599b8", diff --git a/userdata/adam_plus_anna_new/core.domain_objects.xml b/userdata/adam_plus_anna_new/core.domain_objects.xml index 71f3e293c..04e7c0be7 100644 --- a/userdata/adam_plus_anna_new/core.domain_objects.xml +++ b/userdata/adam_plus_anna_new/core.domain_objects.xml @@ -1,5 +1,74 @@ + + Aanvoer water afsluiter (nous lz3) + A device that communicates through the ZigBee protocol. + valve_actuator + 2024-11-11T20:31:49.734+01:00 + 2024-11-15T13:55:19.574+01:00 + + + + + + + + relay + + 2024-11-15T13:55:19.571+01:00 + 2024-11-15T13:55:19.571+01:00 + + + + off + + + + + + 2024-11-15T13:55:19.571+01:00 + false + off + + + + + + _TZ3000_abjodzas + TS0011 + + + + 2024-11-11T20:31:46.189+01:00 + 2024-11-11T20:31:49.706+01:00 + + + + + + + + + + + + A4C13862AF9917B1 + end_device + true + battery + + + + 221 + 1 + parent + + + 2024-11-15T14:07:23+01:00 + true + + + SmartPlug Floor 0 A device that communicates through the ZigBee protocol.