From 371898370824ec6bc0e48f4cc7af490c1e706ce7 Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Mon, 23 Mar 2026 19:50:41 +0000 Subject: [PATCH 01/21] Introduce device type requirement test step in IDM-10.5 --- data_model/1.6/device_types/AirPurifier.xml | 14 ++++ .../1.6/device_types/AmbientContextSensor.xml | 82 ------------------- .../1.6/device_types/BatteryStorage.xml | 22 ++++- data_model/1.6/device_types/BridgedNode.xml | 5 ++ data_model/1.6/device_types/Camera.xml | 5 ++ data_model/1.6/device_types/Closure.xml | 11 +++ data_model/1.6/device_types/Cooktop.xml | 8 ++ data_model/1.6/device_types/EVSE.xml | 28 ++++--- .../1.6/device_types/ElectricalMeter.xml | 8 ++ data_model/1.6/device_types/ExtractorHood.xml | 5 ++ data_model/1.6/device_types/Fan.xml | 5 ++ .../1.6/device_types/FloodlightCamera.xml | 14 ++++ data_model/1.6/device_types/HeatPump.xml | 23 ++++-- data_model/1.6/device_types/Intercom.xml | 8 ++ .../1.6/device_types/IrrigationSystem.xml | 8 ++ .../1.6/device_types/MeterReferencePoint.xml | 18 ++++ data_model/1.6/device_types/MicrowaveOven.xml | 5 ++ data_model/1.6/device_types/Oven.xml | 17 +++- data_model/1.6/device_types/Refrigerator.xml | 11 ++- .../1.6/device_types/RoomAirConditioner.xml | 8 ++ .../1.6/device_types/RootNodeDeviceType.xml | 10 +++ data_model/1.6/device_types/SmokeCOAlarm.xml | 8 ++ .../1.6/device_types/SnapshotCamera.xml | 5 ++ data_model/1.6/device_types/SolarPower.xml | 19 ++++- .../1.6/device_types/ThreadBorderRouter.xml | 5 ++ data_model/1.6/device_types/VideoDoorbell.xml | 14 ++++ data_model/1.6/device_types/WaterHeater.xml | 27 ++---- .../1.6/device_types/device_type_ids.json | 1 - data_model/1.6/scraper_version | 2 +- data_model/1.6/spec_sha | 2 +- docs/ids_and_codes/spec_device_types.md | 1 - src/python_testing/TC_DeviceConformance.py | 1 + .../data_model_xmls.gni | 1 - .../matter/testing/spec_parsing.py | 59 +++++++++++++ .../test_testing/DeviceConformanceTests.py | 40 ++++++++- 35 files changed, 364 insertions(+), 136 deletions(-) delete mode 100644 data_model/1.6/device_types/AmbientContextSensor.xml diff --git a/data_model/1.6/device_types/AirPurifier.xml b/data_model/1.6/device_types/AirPurifier.xml index 9791d46d7f7c8e..e0fb9810a76d64 100644 --- a/data_model/1.6/device_types/AirPurifier.xml +++ b/data_model/1.6/device_types/AirPurifier.xml @@ -94,4 +94,18 @@ Davis, CA 95616, USA + + + + + + + + + + + + + + diff --git a/data_model/1.6/device_types/AmbientContextSensor.xml b/data_model/1.6/device_types/AmbientContextSensor.xml deleted file mode 100644 index e14a19b8f52835..00000000000000 --- a/data_model/1.6/device_types/AmbientContextSensor.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/data_model/1.6/device_types/BatteryStorage.xml b/data_model/1.6/device_types/BatteryStorage.xml index 0bcb110f5f8a59..9175ba4bb69dcd 100644 --- a/data_model/1.6/device_types/BatteryStorage.xml +++ b/data_model/1.6/device_types/BatteryStorage.xml @@ -69,7 +69,14 @@ Davis, CA 95616, USA + + + + + + + @@ -85,10 +92,17 @@ Davis, CA 95616, USA - - - + + + + + + + - + + + + diff --git a/data_model/1.6/device_types/BridgedNode.xml b/data_model/1.6/device_types/BridgedNode.xml index c1c1770ca9e0e4..37b8ca211bb1e5 100644 --- a/data_model/1.6/device_types/BridgedNode.xml +++ b/data_model/1.6/device_types/BridgedNode.xml @@ -98,4 +98,9 @@ Davis, CA 95616, USA + + + + + diff --git a/data_model/1.6/device_types/Camera.xml b/data_model/1.6/device_types/Camera.xml index efed03dc6bf281..422e061329b18a 100644 --- a/data_model/1.6/device_types/Camera.xml +++ b/data_model/1.6/device_types/Camera.xml @@ -132,4 +132,9 @@ Davis, CA 95616, USA + + + + + diff --git a/data_model/1.6/device_types/Closure.xml b/data_model/1.6/device_types/Closure.xml index 89b88a69295842..71febda55e2521 100644 --- a/data_model/1.6/device_types/Closure.xml +++ b/data_model/1.6/device_types/Closure.xml @@ -76,4 +76,15 @@ Davis, CA 95616, USA + + + + + + + + + + + diff --git a/data_model/1.6/device_types/Cooktop.xml b/data_model/1.6/device_types/Cooktop.xml index 09f014328d67f2..9e02a055973f11 100644 --- a/data_model/1.6/device_types/Cooktop.xml +++ b/data_model/1.6/device_types/Cooktop.xml @@ -75,4 +75,12 @@ Davis, CA 95616, USA + + + + + + + + diff --git a/data_model/1.6/device_types/EVSE.xml b/data_model/1.6/device_types/EVSE.xml index 4d440608f7667f..352c8a824a688f 100644 --- a/data_model/1.6/device_types/EVSE.xml +++ b/data_model/1.6/device_types/EVSE.xml @@ -78,7 +78,17 @@ Davis, CA 95616, USA + + + + + + + + + + @@ -88,19 +98,11 @@ Davis, CA 95616, USA - - - - - - + - - - - - - - + + + + diff --git a/data_model/1.6/device_types/ElectricalMeter.xml b/data_model/1.6/device_types/ElectricalMeter.xml index ee31dc4c9f8307..a0220e021c0bb9 100644 --- a/data_model/1.6/device_types/ElectricalMeter.xml +++ b/data_model/1.6/device_types/ElectricalMeter.xml @@ -76,4 +76,12 @@ Davis, CA 95616, USA + + + + + + + + diff --git a/data_model/1.6/device_types/ExtractorHood.xml b/data_model/1.6/device_types/ExtractorHood.xml index c55bc9abd16cfc..6f694a115b75c8 100644 --- a/data_model/1.6/device_types/ExtractorHood.xml +++ b/data_model/1.6/device_types/ExtractorHood.xml @@ -87,4 +87,9 @@ Davis, CA 95616, USA + + + + + diff --git a/data_model/1.6/device_types/Fan.xml b/data_model/1.6/device_types/Fan.xml index d8c832b0f43822..21ba7f60f5a81f 100644 --- a/data_model/1.6/device_types/Fan.xml +++ b/data_model/1.6/device_types/Fan.xml @@ -90,4 +90,9 @@ Davis, CA 95616, USA + + + + + diff --git a/data_model/1.6/device_types/FloodlightCamera.xml b/data_model/1.6/device_types/FloodlightCamera.xml index feb4927739df1c..5f667f75f9ef36 100644 --- a/data_model/1.6/device_types/FloodlightCamera.xml +++ b/data_model/1.6/device_types/FloodlightCamera.xml @@ -62,4 +62,18 @@ Davis, CA 95616, USA + + + + + + + + + + + + + + diff --git a/data_model/1.6/device_types/HeatPump.xml b/data_model/1.6/device_types/HeatPump.xml index 778d66f2e4a299..6d7774b25eb0f6 100644 --- a/data_model/1.6/device_types/HeatPump.xml +++ b/data_model/1.6/device_types/HeatPump.xml @@ -71,7 +71,14 @@ Davis, CA 95616, USA + + + + + + + @@ -81,20 +88,22 @@ Davis, CA 95616, USA + + + + + + + - - - - - - + - + diff --git a/data_model/1.6/device_types/Intercom.xml b/data_model/1.6/device_types/Intercom.xml index 85411d329d871c..657ab802e4899e 100644 --- a/data_model/1.6/device_types/Intercom.xml +++ b/data_model/1.6/device_types/Intercom.xml @@ -119,4 +119,12 @@ Davis, CA 95616, USA + + + + + + + + diff --git a/data_model/1.6/device_types/IrrigationSystem.xml b/data_model/1.6/device_types/IrrigationSystem.xml index f7de739e013bc6..efe3ba0e2bd951 100644 --- a/data_model/1.6/device_types/IrrigationSystem.xml +++ b/data_model/1.6/device_types/IrrigationSystem.xml @@ -76,4 +76,12 @@ Davis, CA 95616, USA + + + + + + + + diff --git a/data_model/1.6/device_types/MeterReferencePoint.xml b/data_model/1.6/device_types/MeterReferencePoint.xml index 6cce132b79f13f..a17b4e77de4d08 100644 --- a/data_model/1.6/device_types/MeterReferencePoint.xml +++ b/data_model/1.6/device_types/MeterReferencePoint.xml @@ -77,4 +77,22 @@ Davis, CA 95616, USA + + + + + + + + + + + + + + + + + + diff --git a/data_model/1.6/device_types/MicrowaveOven.xml b/data_model/1.6/device_types/MicrowaveOven.xml index 1ce8b1e0ac985a..15700f5366b4c6 100644 --- a/data_model/1.6/device_types/MicrowaveOven.xml +++ b/data_model/1.6/device_types/MicrowaveOven.xml @@ -98,4 +98,9 @@ Davis, CA 95616, USA + + + + + diff --git a/data_model/1.6/device_types/Oven.xml b/data_model/1.6/device_types/Oven.xml index 193ce51d7795b8..bdfbcdccd10068 100644 --- a/data_model/1.6/device_types/Oven.xml +++ b/data_model/1.6/device_types/Oven.xml @@ -57,10 +57,11 @@ Davis, CA 95616, USA :xrefstyle: basic --> - + + @@ -75,4 +76,18 @@ Davis, CA 95616, USA + + + + + + + + + + + + + + diff --git a/data_model/1.6/device_types/Refrigerator.xml b/data_model/1.6/device_types/Refrigerator.xml index 6d22572b3fcef4..c651a16e1679e2 100644 --- a/data_model/1.6/device_types/Refrigerator.xml +++ b/data_model/1.6/device_types/Refrigerator.xml @@ -57,10 +57,11 @@ Davis, CA 95616, USA :xrefstyle: basic --> - + + @@ -91,4 +92,12 @@ Davis, CA 95616, USA + + + + + + + + diff --git a/data_model/1.6/device_types/RoomAirConditioner.xml b/data_model/1.6/device_types/RoomAirConditioner.xml index f2e5d58985a28d..d99c8634ad856a 100644 --- a/data_model/1.6/device_types/RoomAirConditioner.xml +++ b/data_model/1.6/device_types/RoomAirConditioner.xml @@ -120,4 +120,12 @@ Davis, CA 95616, USA + + + + + + + + diff --git a/data_model/1.6/device_types/RootNodeDeviceType.xml b/data_model/1.6/device_types/RootNodeDeviceType.xml index 6278a752291dbc..28edf193e79954 100644 --- a/data_model/1.6/device_types/RootNodeDeviceType.xml +++ b/data_model/1.6/device_types/RootNodeDeviceType.xml @@ -357,4 +357,14 @@ Davis, CA 95616, USA + + + + + + + + + + diff --git a/data_model/1.6/device_types/SmokeCOAlarm.xml b/data_model/1.6/device_types/SmokeCOAlarm.xml index f4dc779ff59709..9dd5030159053d 100644 --- a/data_model/1.6/device_types/SmokeCOAlarm.xml +++ b/data_model/1.6/device_types/SmokeCOAlarm.xml @@ -93,4 +93,12 @@ Davis, CA 95616, USA + + + + + + + + diff --git a/data_model/1.6/device_types/SnapshotCamera.xml b/data_model/1.6/device_types/SnapshotCamera.xml index f39717fcff010f..e84bcd873816ff 100644 --- a/data_model/1.6/device_types/SnapshotCamera.xml +++ b/data_model/1.6/device_types/SnapshotCamera.xml @@ -105,4 +105,9 @@ Davis, CA 95616, USA + + + + + diff --git a/data_model/1.6/device_types/SolarPower.xml b/data_model/1.6/device_types/SolarPower.xml index d0875d1f86470e..5b3bb2f5367f9d 100644 --- a/data_model/1.6/device_types/SolarPower.xml +++ b/data_model/1.6/device_types/SolarPower.xml @@ -68,7 +68,14 @@ Davis, CA 95616, USA + + + + + + + @@ -81,10 +88,14 @@ Davis, CA 95616, USA - - - + + + + + + + - + diff --git a/data_model/1.6/device_types/ThreadBorderRouter.xml b/data_model/1.6/device_types/ThreadBorderRouter.xml index ea6086e268f5a5..db0d439b5f5b98 100644 --- a/data_model/1.6/device_types/ThreadBorderRouter.xml +++ b/data_model/1.6/device_types/ThreadBorderRouter.xml @@ -81,4 +81,9 @@ Davis, CA 95616, USA + + + + + diff --git a/data_model/1.6/device_types/VideoDoorbell.xml b/data_model/1.6/device_types/VideoDoorbell.xml index 37e4693966b435..6660f387bdcd30 100644 --- a/data_model/1.6/device_types/VideoDoorbell.xml +++ b/data_model/1.6/device_types/VideoDoorbell.xml @@ -62,4 +62,18 @@ Davis, CA 95616, USA + + + + + + + + + + + + + + diff --git a/data_model/1.6/device_types/WaterHeater.xml b/data_model/1.6/device_types/WaterHeater.xml index 5f04901b9e3d96..cea2109748407b 100644 --- a/data_model/1.6/device_types/WaterHeater.xml +++ b/data_model/1.6/device_types/WaterHeater.xml @@ -82,7 +82,11 @@ Davis, CA 95616, USA + + + + @@ -92,24 +96,11 @@ Davis, CA 95616, USA - - - + - - - - - - - - - - - - - - - + + + + diff --git a/data_model/1.6/device_types/device_type_ids.json b/data_model/1.6/device_types/device_type_ids.json index 6e60e7e42a13f7..d47eef2eb82bcc 100644 --- a/data_model/1.6/device_types/device_type_ids.json +++ b/data_model/1.6/device_types/device_type_ids.json @@ -66,7 +66,6 @@ "326": "Chime", "327": "Camera Controller", "328": "Doorbell", - "336": "Ambient Context Sensor", "514": "Window Covering", "515": "Window Covering Controller", "560": "Closure", diff --git a/data_model/1.6/scraper_version b/data_model/1.6/scraper_version index fce501e8625947..09d506b6b65696 100644 --- a/data_model/1.6/scraper_version +++ b/data_model/1.6/scraper_version @@ -1 +1 @@ -alchemy version: v1.6.11-0.20260317222315-453cc52bdb3c +alchemy version: v1.6.11-0.20260312201802-2c07bc48856b+dirty diff --git a/data_model/1.6/spec_sha b/data_model/1.6/spec_sha index 00b720e4c03d72..045edfa9c5003e 100644 --- a/data_model/1.6/spec_sha +++ b/data_model/1.6/spec_sha @@ -1 +1 @@ -636b190328176dc88f2cd894c7a5ba00f1935c99 +6982bddd1100b62fdd504bbfcb61bb7ed51a6418 diff --git a/docs/ids_and_codes/spec_device_types.md b/docs/ids_and_codes/spec_device_types.md index 3d122a5cc3ccd8..b4ec56a3a9c00e 100644 --- a/docs/ids_and_codes/spec_device_types.md +++ b/docs/ids_and_codes/spec_device_types.md @@ -78,7 +78,6 @@ The following markers are used in this document (matches the ID master list): |326 |0x0146 |Chime | | | | | | | |C |C |C | |327 |0x0147 |Camera Controller | | | | | | | |C |C |C | |328 |0x0148 |Doorbell | | | | | | | |C |C |C | -|336 |0x0150 |Ambient Context Sensor | | | | | | | | | |C | |514 |0x0202 |Window Covering |C |C |C |C |C |C |C |C |C |C | |515 |0x0203 |Window Covering Controller |C |C |C |C |C |C |C |C |C |C | |560 |0x0230 |Closure | | | | | | | |C |C |C | diff --git a/src/python_testing/TC_DeviceConformance.py b/src/python_testing/TC_DeviceConformance.py index da422cd0e595b7..01da406b6a2a1f 100644 --- a/src/python_testing/TC_DeviceConformance.py +++ b/src/python_testing/TC_DeviceConformance.py @@ -97,6 +97,7 @@ def test_TC_IDM_10_6(self): if not success: self.fail_current_test("Problems with Device type revisions on one or more endpoints") + def steps_TC_IDM_14_1(self): return [TestStep(0, "TH performs a wildcard read of all attributes and endpoints on the device"), TestStep(1, """ For each root-node-restricted cluster in the list, ensure the cluster does not appear on any endpoint that is not the root node. diff --git a/src/python_testing/matter_testing_infrastructure/data_model_xmls.gni b/src/python_testing/matter_testing_infrastructure/data_model_xmls.gni index 92bf7493290793..c38059bb7b0f98 100644 --- a/src/python_testing/matter_testing_infrastructure/data_model_xmls.gni +++ b/src/python_testing/matter_testing_infrastructure/data_model_xmls.gni @@ -1810,7 +1810,6 @@ data_model_XMLS_1_6 = [ "${chip_root}/data_model/1.6/device_types/Aggregator.xml", "${chip_root}/data_model/1.6/device_types/AirPurifier.xml", "${chip_root}/data_model/1.6/device_types/AirQualitySensor.xml", - "${chip_root}/data_model/1.6/device_types/AmbientContextSensor.xml", "${chip_root}/data_model/1.6/device_types/AudioDoorbell.xml", "${chip_root}/data_model/1.6/device_types/BaseDeviceType.xml", "${chip_root}/data_model/1.6/device_types/BasicVideoPlayer.xml", diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index 1532b877b0c602..cfa9321a6f6cb2 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -246,6 +246,15 @@ def __str__(self) -> str: return f"{self.name}{desc}" +@dataclass +class XmlComposedDeviceTypeRequirement: + device_type_id: int + device_type_name: str + conformance: ConformanceCallable + min_instances: Optional[int] = None + max_instances: Optional[int] = None + + @dataclass class XmlDeviceType: name: str @@ -258,6 +267,7 @@ class XmlDeviceType: revision_desc: dict[int, str] superset_of_device_type_name: Optional[str] = None superset_of_device_type_id: int = 0 + composed_device_types: list['XmlComposedDeviceTypeRequirement'] = field(default_factory=list) def __str__(self): msg = f'{self.name} - Revision {self.revision}, Class {self.classification_class}, Scope {self.classification_scope}\n' @@ -1516,6 +1526,55 @@ def append_overrides(override_element_type: str): severity=ProblemSeverity.WARNING, problem=f"Unable to parse conformance for cluster - {ex}")) # NOTE: Spec currently does a bad job of matching these exactly to the names and codes # so this will need a bit of fancy handling here to get this right. + + try: + main_composed_elements = d.findall('composedDeviceTypes') + for composed in main_composed_elements: + for composed_dt in composed.findall('deviceType'): + try: + composed_id = int(composed_dt.attrib['deviceTypeId'], 0) + composed_name = composed_dt.attrib['deviceTypeName'] + except (KeyError, ValueError): + problems.append(ProblemNotice("Parse Device Type XML", location=location, + severity=ProblemSeverity.WARNING, problem="Invalid composed device type id or name")) + continue + + # Conformance + conformance_xml, tmp_problem = get_conformance(composed_dt, id) + if tmp_problem: + problems.append(tmp_problem) + continue + + try: + conformance = parse_callable_from_xml(conformance_xml, ConformanceParseParameters(feature_map={}, attribute_map={}, command_map={})) + except ConformanceException as ex: + problems.append(ProblemNotice("Parse Device Type XML", location=location, + severity=ProblemSeverity.WARNING, problem=f"Unable to parse conformance for composed device type - {ex}")) + continue + + min_instances = None + max_instances = None + constraint = composed_dt.find('constraint') + if constraint is not None: + min_el = constraint.find('min') + if min_el is not None and 'value' in min_el.attrib: + min_instances = int(min_el.attrib['value'], 0) + + max_el = constraint.find('max') + if max_el is not None and 'value' in max_el.attrib: + max_instances = int(max_el.attrib['value'], 0) + + device_types[id].composed_device_types.append(XmlComposedDeviceTypeRequirement( + device_type_id=composed_id, + device_type_name=composed_name, + conformance=conformance, + min_instances=min_instances, + max_instances=max_instances + )) + except Exception as e: + problems.append(ProblemNotice("Parse Device Type XML", location=location, + severity=ProblemSeverity.WARNING, problem=f"Error parsing composedDeviceTypes: {e}")) + return device_types, problems diff --git a/src/python_testing/test_testing/DeviceConformanceTests.py b/src/python_testing/test_testing/DeviceConformanceTests.py index 18913c9122166c..1b9c5fe6374588 100644 --- a/src/python_testing/test_testing/DeviceConformanceTests.py +++ b/src/python_testing/test_testing/DeviceConformanceTests.py @@ -28,7 +28,7 @@ from matter.testing.problem_notices import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, DeviceTypePathLocation, ProblemNotice, ProblemSeverity) from matter.testing.spec_parsing import (CommandType, PrebuiltDataModelDirectory, XmlDeviceType, XmlDeviceTypeClusterRequirements, - build_xml_device_types, build_xml_namespaces) + build_xml_device_types, build_xml_namespaces, conformance_support) from matter.tlv import uint @@ -473,6 +473,44 @@ def check_command_overrides(cluster_requirement: XmlDeviceTypeClusterRequirement location = ClusterPathLocation(endpoint_id=endpoint_id, cluster_id=extra) fn(location=location, problem=f"Extra cluster found on endpoint with device types {device_type_list}") + # First, count instances of each device type across the entire device + device_type_counts: dict[int, int] = {} + for endpoint_id, endpoint in self.endpoints.items(): + if Clusters.Descriptor not in endpoint: + continue + device_types = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] + for dt in device_types: + device_type_counts[dt.deviceType] = device_type_counts.get(dt.deviceType, 0) + 1 + + # Now evaluate composed device type requirements for each device type found + for endpoint_id, endpoint in self.endpoints.items(): + if Clusters.Descriptor not in endpoint: + continue + + device_types = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] + for dt in device_types: + device_type_id = dt.deviceType + if device_type_id not in self.xml_device_types: + continue + + xml_device = self.xml_device_types[device_type_id] + for req in xml_device.composed_device_types: + # Conformance Assessment + conformance_decision = req.conformance(EMPTY_CLUSTER_GLOBAL_ATTRIBUTES) + count = device_type_counts.get(req.device_type_id, 0) + location = DeviceTypePathLocation(endpoint_id=endpoint_id, device_type_id=device_type_id) + + if conformance_decision.is_mandatory() and count == 0: + record_error(location, f"Mandatory composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} is missing on the device") + elif not conformance_allowed(conformance_decision, allow_provisional) and count > 0: + record_error(location, f"Disallowed composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} is present on the device") + + if conformance_allowed(conformance_decision, allow_provisional): + if req.min_instances is not None and count < req.min_instances: + record_error(location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} expects at least {req.min_instances} instances, but found {count}") + if req.max_instances is not None and count > req.max_instances: + record_error(location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} expects at most {req.max_instances} instances, but found {count}") + return success, problems def check_root_endpoint_for_application_device_types(self) -> list[ProblemNotice]: From 617535fe0871e9b40c22b81e7197929689416393 Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Mon, 23 Mar 2026 20:04:37 +0000 Subject: [PATCH 02/21] Restyled by ruff --- .../matter/testing/spec_parsing.py | 4 ++-- src/python_testing/test_testing/DeviceConformanceTests.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index cfa9321a6f6cb2..5e86a8a07cb871 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -1544,7 +1544,7 @@ def append_overrides(override_element_type: str): if tmp_problem: problems.append(tmp_problem) continue - + try: conformance = parse_callable_from_xml(conformance_xml, ConformanceParseParameters(feature_map={}, attribute_map={}, command_map={})) except ConformanceException as ex: @@ -1559,7 +1559,7 @@ def append_overrides(override_element_type: str): min_el = constraint.find('min') if min_el is not None and 'value' in min_el.attrib: min_instances = int(min_el.attrib['value'], 0) - + max_el = constraint.find('max') if max_el is not None and 'value' in max_el.attrib: max_instances = int(max_el.attrib['value'], 0) diff --git a/src/python_testing/test_testing/DeviceConformanceTests.py b/src/python_testing/test_testing/DeviceConformanceTests.py index 1b9c5fe6374588..36eb77cf6ecbca 100644 --- a/src/python_testing/test_testing/DeviceConformanceTests.py +++ b/src/python_testing/test_testing/DeviceConformanceTests.py @@ -28,7 +28,7 @@ from matter.testing.problem_notices import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, DeviceTypePathLocation, ProblemNotice, ProblemSeverity) from matter.testing.spec_parsing import (CommandType, PrebuiltDataModelDirectory, XmlDeviceType, XmlDeviceTypeClusterRequirements, - build_xml_device_types, build_xml_namespaces, conformance_support) + build_xml_device_types, build_xml_namespaces) from matter.tlv import uint @@ -492,7 +492,7 @@ def check_command_overrides(cluster_requirement: XmlDeviceTypeClusterRequirement device_type_id = dt.deviceType if device_type_id not in self.xml_device_types: continue - + xml_device = self.xml_device_types[device_type_id] for req in xml_device.composed_device_types: # Conformance Assessment @@ -504,7 +504,7 @@ def check_command_overrides(cluster_requirement: XmlDeviceTypeClusterRequirement record_error(location, f"Mandatory composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} is missing on the device") elif not conformance_allowed(conformance_decision, allow_provisional) and count > 0: record_error(location, f"Disallowed composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} is present on the device") - + if conformance_allowed(conformance_decision, allow_provisional): if req.min_instances is not None and count < req.min_instances: record_error(location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} expects at least {req.min_instances} instances, but found {count}") From de7823471ae8dff43a06316486f7f19ab2aa2fef Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Mon, 23 Mar 2026 20:04:40 +0000 Subject: [PATCH 03/21] Restyled by autopep8 --- src/python_testing/TC_DeviceConformance.py | 1 - .../matter/testing/spec_parsing.py | 3 ++- .../test_testing/DeviceConformanceTests.py | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/python_testing/TC_DeviceConformance.py b/src/python_testing/TC_DeviceConformance.py index 01da406b6a2a1f..da422cd0e595b7 100644 --- a/src/python_testing/TC_DeviceConformance.py +++ b/src/python_testing/TC_DeviceConformance.py @@ -97,7 +97,6 @@ def test_TC_IDM_10_6(self): if not success: self.fail_current_test("Problems with Device type revisions on one or more endpoints") - def steps_TC_IDM_14_1(self): return [TestStep(0, "TH performs a wildcard read of all attributes and endpoints on the device"), TestStep(1, """ For each root-node-restricted cluster in the list, ensure the cluster does not appear on any endpoint that is not the root node. diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index 5e86a8a07cb871..3217a9e4b21560 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -1546,7 +1546,8 @@ def append_overrides(override_element_type: str): continue try: - conformance = parse_callable_from_xml(conformance_xml, ConformanceParseParameters(feature_map={}, attribute_map={}, command_map={})) + conformance = parse_callable_from_xml(conformance_xml, ConformanceParseParameters( + feature_map={}, attribute_map={}, command_map={})) except ConformanceException as ex: problems.append(ProblemNotice("Parse Device Type XML", location=location, severity=ProblemSeverity.WARNING, problem=f"Unable to parse conformance for composed device type - {ex}")) diff --git a/src/python_testing/test_testing/DeviceConformanceTests.py b/src/python_testing/test_testing/DeviceConformanceTests.py index 36eb77cf6ecbca..b3c750555b697b 100644 --- a/src/python_testing/test_testing/DeviceConformanceTests.py +++ b/src/python_testing/test_testing/DeviceConformanceTests.py @@ -501,15 +501,19 @@ def check_command_overrides(cluster_requirement: XmlDeviceTypeClusterRequirement location = DeviceTypePathLocation(endpoint_id=endpoint_id, device_type_id=device_type_id) if conformance_decision.is_mandatory() and count == 0: - record_error(location, f"Mandatory composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} is missing on the device") + record_error( + location, f"Mandatory composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} is missing on the device") elif not conformance_allowed(conformance_decision, allow_provisional) and count > 0: - record_error(location, f"Disallowed composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} is present on the device") + record_error( + location, f"Disallowed composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} is present on the device") if conformance_allowed(conformance_decision, allow_provisional): if req.min_instances is not None and count < req.min_instances: - record_error(location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} expects at least {req.min_instances} instances, but found {count}") + record_error( + location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} expects at least {req.min_instances} instances, but found {count}") if req.max_instances is not None and count > req.max_instances: - record_error(location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} expects at most {req.max_instances} instances, but found {count}") + record_error( + location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} expects at most {req.max_instances} instances, but found {count}") return success, problems From c585edf8a156a5d5f558c5c91f3b1d2beeee0e83 Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Mon, 23 Mar 2026 20:17:10 +0000 Subject: [PATCH 04/21] Support allowed constraint --- .../matter/testing/spec_parsing.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index 3217a9e4b21560..ff77bd27c96df1 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -1557,13 +1557,18 @@ def append_overrides(override_element_type: str): max_instances = None constraint = composed_dt.find('constraint') if constraint is not None: - min_el = constraint.find('min') - if min_el is not None and 'value' in min_el.attrib: - min_instances = int(min_el.attrib['value'], 0) - - max_el = constraint.find('max') - if max_el is not None and 'value' in max_el.attrib: - max_instances = int(max_el.attrib['value'], 0) + allowed_el = constraint.find('allowed') + if allowed_el is not None and 'value' in allowed_el.attrib: + min_instances = int(allowed_el.attrib['value'], 0) + max_instances = int(allowed_el.attrib['value'], 0) + else: + min_el = constraint.find('min') + if min_el is not None and 'value' in min_el.attrib: + min_instances = int(min_el.attrib['value'], 0) + + max_el = constraint.find('max') + if max_el is not None and 'value' in max_el.attrib: + max_instances = int(max_el.attrib['value'], 0) device_types[id].composed_device_types.append(XmlComposedDeviceTypeRequirement( device_type_id=composed_id, From c120a2631f9bb0d29c89822d84b503b4a4f0f97f Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Mon, 23 Mar 2026 20:48:46 +0000 Subject: [PATCH 05/21] fix mypy error --- .../matter/testing/spec_parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index ff77bd27c96df1..bacb9eb24b23f2 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -1540,7 +1540,7 @@ def append_overrides(override_element_type: str): continue # Conformance - conformance_xml, tmp_problem = get_conformance(composed_dt, id) + conformance_xml, tmp_problem = get_conformance(composed_dt, uint(id)) if tmp_problem: problems.append(tmp_problem) continue From 65dfb83a5c7cd53ed92b466c9fcbdbd26af49862 Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Mon, 23 Mar 2026 21:19:34 +0000 Subject: [PATCH 06/21] Keep device_type_check untouched --- src/python_testing/TC_DeviceConformance.py | 6 +++++- .../test_testing/DeviceConformanceTests.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/python_testing/TC_DeviceConformance.py b/src/python_testing/TC_DeviceConformance.py index da422cd0e595b7..80c245b3869a93 100644 --- a/src/python_testing/TC_DeviceConformance.py +++ b/src/python_testing/TC_DeviceConformance.py @@ -88,7 +88,11 @@ def test_TC_IDM_10_5(self): allow_provisional = self.user_params.get("allow_provisional", False) success, problems = self.check_device_type(fail_on_extra_clusters, allow_provisional) self.problems.extend(problems) - if not success: + + flat_dt_req_success, flat_dt_req_problems = self.check_flat_model_device_type_requirements(allow_provisional) + self.problems.extend(flat_dt_req_problems) + + if not success or not flat_dt_req_success: self.fail_current_test("Problems with Device type conformance on one or more endpoints") def test_TC_IDM_10_6(self): diff --git a/src/python_testing/test_testing/DeviceConformanceTests.py b/src/python_testing/test_testing/DeviceConformanceTests.py index b3c750555b697b..58d480273b04bd 100644 --- a/src/python_testing/test_testing/DeviceConformanceTests.py +++ b/src/python_testing/test_testing/DeviceConformanceTests.py @@ -473,6 +473,17 @@ def check_command_overrides(cluster_requirement: XmlDeviceTypeClusterRequirement location = ClusterPathLocation(endpoint_id=endpoint_id, cluster_id=extra) fn(location=location, problem=f"Extra cluster found on endpoint with device types {device_type_list}") + return success, problems + + def check_flat_model_device_type_requirements(self, allow_provisional: bool = False) -> tuple[bool, list[ProblemNotice]]: + success = True + problems = [] + + def record_error(location, problem): + nonlocal success + problems.append(ProblemNotice("IDM-10.5", location, ProblemSeverity.ERROR, problem, "")) + success = False + # First, count instances of each device type across the entire device device_type_counts: dict[int, int] = {} for endpoint_id, endpoint in self.endpoints.items(): From 3e2fd39abbc68895359016ffcd4e2d08f652174f Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Tue, 24 Mar 2026 19:34:24 +0000 Subject: [PATCH 07/21] Limit the test for 1.6 and above --- .../matter/testing/spec_parsing.py | 6 ++++-- .../test_testing/DeviceConformanceTests.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index bacb9eb24b23f2..1cf5cff160da41 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -1542,8 +1542,10 @@ def append_overrides(override_element_type: str): # Conformance conformance_xml, tmp_problem = get_conformance(composed_dt, uint(id)) if tmp_problem: - problems.append(tmp_problem) - continue + # Composed device types in versions 1.5.1 and earlier often lack explicit conformance in XML. + # Default them to optionalConform to prevent parser warnings. + # TODO: Report as a problem once all data_model files are updated with composedDeviceType definitions. + conformance_xml = ElementTree.Element('optionalConform') try: conformance = parse_callable_from_xml(conformance_xml, ConformanceParseParameters( diff --git a/src/python_testing/test_testing/DeviceConformanceTests.py b/src/python_testing/test_testing/DeviceConformanceTests.py index 58d480273b04bd..2430d8ed8070d7 100644 --- a/src/python_testing/test_testing/DeviceConformanceTests.py +++ b/src/python_testing/test_testing/DeviceConformanceTests.py @@ -17,6 +17,7 @@ from typing import Callable, Optional +import logging import matter.clusters as Clusters from matter.testing.basic_composition import BasicCompositionTests @@ -31,6 +32,7 @@ build_xml_device_types, build_xml_namespaces) from matter.tlv import uint +logger = logging.getLogger(__name__) def get_supersets(xml_device_types: dict[int, XmlDeviceType]) -> list[set[int]]: ''' Returns a list of the sets of device type id that each constitute a single superset. @@ -479,6 +481,15 @@ def check_flat_model_device_type_requirements(self, allow_provisional: bool = Fa success = True problems = [] + try: + spec_version = self.endpoints[0][Clusters.BasicInformation][Clusters.BasicInformation.Attributes.SpecificationVersion] + except KeyError: + spec_version = 0 + + if spec_version < 0x01060000: + logger.info("Skipping flat model device type requirements check: this test is not enabled for versions below 1.6") + return success, problems + def record_error(location, problem): nonlocal success problems.append(ProblemNotice("IDM-10.5", location, ProblemSeverity.ERROR, problem, "")) From 796a766f5442b361038120bf0cd6cb8894be462e Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Tue, 24 Mar 2026 19:35:20 +0000 Subject: [PATCH 08/21] Restyled by autopep8 --- src/python_testing/test_testing/DeviceConformanceTests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/python_testing/test_testing/DeviceConformanceTests.py b/src/python_testing/test_testing/DeviceConformanceTests.py index 2430d8ed8070d7..0042e6f59a282d 100644 --- a/src/python_testing/test_testing/DeviceConformanceTests.py +++ b/src/python_testing/test_testing/DeviceConformanceTests.py @@ -34,6 +34,7 @@ logger = logging.getLogger(__name__) + def get_supersets(xml_device_types: dict[int, XmlDeviceType]) -> list[set[int]]: ''' Returns a list of the sets of device type id that each constitute a single superset. From 6060661ead35b650ddfbc42e3d363afcf48a2501 Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Tue, 24 Mar 2026 19:35:23 +0000 Subject: [PATCH 09/21] Restyled by isort --- src/python_testing/test_testing/DeviceConformanceTests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python_testing/test_testing/DeviceConformanceTests.py b/src/python_testing/test_testing/DeviceConformanceTests.py index 0042e6f59a282d..191c7e09a17229 100644 --- a/src/python_testing/test_testing/DeviceConformanceTests.py +++ b/src/python_testing/test_testing/DeviceConformanceTests.py @@ -16,8 +16,8 @@ # -from typing import Callable, Optional import logging +from typing import Callable, Optional import matter.clusters as Clusters from matter.testing.basic_composition import BasicCompositionTests From e45ee93dcb5543e152e62f903598ac24c092bb94 Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Wed, 13 May 2026 16:02:33 +0000 Subject: [PATCH 10/21] Test composed device types --- src/python_testing/TC_DeviceConformance.py | 6 +-- .../testing/device_conformance_tests.py | 37 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/python_testing/TC_DeviceConformance.py b/src/python_testing/TC_DeviceConformance.py index d8fd228f647558..55de563545d9e1 100644 --- a/src/python_testing/TC_DeviceConformance.py +++ b/src/python_testing/TC_DeviceConformance.py @@ -125,10 +125,10 @@ def test_TC_IDM_10_5(self): allow_provisional_test_event_only_disallowed_for_certification) self.problems.extend(problems) - flat_dt_req_success, flat_dt_req_problems = self.check_flat_model_device_type_requirements(allow_provisional) - self.problems.extend(flat_dt_req_problems) + composed_dt_req_success, composed_dt_req_problems = self.check_composed_device_type_requirements(allow_provisional_test_event_only_disallowed_for_certification) + self.problems.extend(composed_dt_req_problems) - if not success or not flat_dt_req_success: + if not success or not composed_dt_req_success: self.fail_current_test("Problems with Device type conformance on one or more endpoints") def test_TC_IDM_10_6(self): diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py index 144b2c43d02794..80c95d039048b2 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py @@ -478,7 +478,7 @@ def check_command_overrides(cluster_requirement: XmlDeviceTypeClusterRequirement return success, problems - def check_flat_model_device_type_requirements(self, allow_provisional: bool = False) -> tuple[bool, list[ProblemNotice]]: + def check_composed_device_type_requirements(self, allow_provisional: bool = False) -> tuple[bool, list[ProblemNotice]]: success = True problems = [] @@ -496,16 +496,7 @@ def record_error(location, problem): problems.append(ProblemNotice("IDM-10.5", location, ProblemSeverity.ERROR, problem, "")) success = False - # First, count instances of each device type across the entire device - device_type_counts: dict[int, int] = {} - for endpoint_id, endpoint in self.endpoints.items(): - if Clusters.Descriptor not in endpoint: - continue - device_types = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] - for dt in device_types: - device_type_counts[dt.deviceType] = device_type_counts.get(dt.deviceType, 0) + 1 - - # Now evaluate composed device type requirements for each device type found + # Evaluate composed device type requirements for each device type found for endpoint_id, endpoint in self.endpoints.items(): if Clusters.Descriptor not in endpoint: continue @@ -520,23 +511,37 @@ def record_error(location, problem): for req in xml_device.composed_device_types: # Conformance Assessment conformance_decision = req.conformance(EMPTY_CLUSTER_GLOBAL_ATTRIBUTES) - count = device_type_counts.get(req.device_type_id, 0) + + # Count instances in child endpoints + parts_list = [] + if Clusters.Descriptor.Attributes.PartsList in endpoint[Clusters.Descriptor]: + parts_list = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] + + count = 0 + for child_ep_id in parts_list: + if child_ep_id in self.endpoints: + child_ep = self.endpoints[child_ep_id] + if Clusters.Descriptor in child_ep: + child_dt_list = child_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] + if any(child_dt.deviceType == req.device_type_id for child_dt in child_dt_list): + count += 1 + location = DeviceTypePathLocation(endpoint_id=endpoint_id, device_type_id=device_type_id) if conformance_decision.is_mandatory() and count == 0: record_error( - location, f"Mandatory composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} is missing on the device") + location, f"Mandatory composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} on endpoint {endpoint_id} is missing in child endpoints") elif not conformance_allowed(conformance_decision, allow_provisional) and count > 0: record_error( - location, f"Disallowed composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} is present on the device") + location, f"Disallowed composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} on endpoint {endpoint_id} is present in child endpoints") if conformance_allowed(conformance_decision, allow_provisional): if req.min_instances is not None and count < req.min_instances: record_error( - location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} expects at least {req.min_instances} instances, but found {count}") + location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} on endpoint {endpoint_id} expects at least {req.min_instances} instances in child endpoints, but found {count}") if req.max_instances is not None and count > req.max_instances: record_error( - location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} expects at most {req.max_instances} instances, but found {count}") + location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} on endpoint {endpoint_id} expects at most {req.max_instances} instances in child endpoints, but found {count}") return success, problems From fea6963e4de9dd229a2b0e2253e2d98b4bf61a33 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 16:04:08 +0000 Subject: [PATCH 11/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/python_testing/TC_DeviceConformance.py | 3 ++- .../matter/testing/device_conformance_tests.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/python_testing/TC_DeviceConformance.py b/src/python_testing/TC_DeviceConformance.py index 55de563545d9e1..51c25fe34fd8a8 100644 --- a/src/python_testing/TC_DeviceConformance.py +++ b/src/python_testing/TC_DeviceConformance.py @@ -125,7 +125,8 @@ def test_TC_IDM_10_5(self): allow_provisional_test_event_only_disallowed_for_certification) self.problems.extend(problems) - composed_dt_req_success, composed_dt_req_problems = self.check_composed_device_type_requirements(allow_provisional_test_event_only_disallowed_for_certification) + composed_dt_req_success, composed_dt_req_problems = self.check_composed_device_type_requirements( + allow_provisional_test_event_only_disallowed_for_certification) self.problems.extend(composed_dt_req_problems) if not success or not composed_dt_req_success: diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py index 80c95d039048b2..a1f9b7f301f6b9 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py @@ -511,12 +511,12 @@ def record_error(location, problem): for req in xml_device.composed_device_types: # Conformance Assessment conformance_decision = req.conformance(EMPTY_CLUSTER_GLOBAL_ATTRIBUTES) - + # Count instances in child endpoints parts_list = [] if Clusters.Descriptor.Attributes.PartsList in endpoint[Clusters.Descriptor]: parts_list = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] - + count = 0 for child_ep_id in parts_list: if child_ep_id in self.endpoints: @@ -525,7 +525,7 @@ def record_error(location, problem): child_dt_list = child_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] if any(child_dt.deviceType == req.device_type_id for child_dt in child_dt_list): count += 1 - + location = DeviceTypePathLocation(endpoint_id=endpoint_id, device_type_id=device_type_id) if conformance_decision.is_mandatory() and count == 0: From a72524c8c6ff4aeebb18ba3d16aa20c084c44f43 Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Wed, 27 May 2026 13:44:50 +0000 Subject: [PATCH 12/21] Update composed device type xmls --- data_model/1.6/clusters/cluster_ids.json | 2 +- .../1.6/device_types/BatteryStorage.xml | 144 +++++++++++++++++- data_model/1.6/device_types/EVSE.xml | 12 ++ data_model/1.6/device_types/HeatPump.xml | 54 +++++++ data_model/1.6/device_types/Intercom.xml | 9 ++ .../1.6/device_types/MeterReferencePoint.xml | 14 ++ data_model/1.6/device_types/SolarPower.xml | 47 ++++++ data_model/1.6/device_types/WaterHeater.xml | 9 ++ 8 files changed, 288 insertions(+), 3 deletions(-) diff --git a/data_model/1.6/clusters/cluster_ids.json b/data_model/1.6/clusters/cluster_ids.json index bc13e5dd0e91e5..be322816aefd6b 100644 --- a/data_model/1.6/clusters/cluster_ids.json +++ b/data_model/1.6/clusters/cluster_ids.json @@ -134,4 +134,4 @@ "2050": "TLS Client Management", "2822": "Meter Identification", "2823": "Commodity Metering" -} +} \ No newline at end of file diff --git a/data_model/1.6/device_types/BatteryStorage.xml b/data_model/1.6/device_types/BatteryStorage.xml index 9175ba4bb69dcd..20aa1a0c812ee7 100644 --- a/data_model/1.6/device_types/BatteryStorage.xml +++ b/data_model/1.6/device_types/BatteryStorage.xml @@ -71,24 +71,91 @@ Davis, CA 95616, USA + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -98,11 +165,84 @@ Davis, CA 95616, USA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data_model/1.6/device_types/EVSE.xml b/data_model/1.6/device_types/EVSE.xml index 352c8a824a688f..f4eb8a43e5e44c 100644 --- a/data_model/1.6/device_types/EVSE.xml +++ b/data_model/1.6/device_types/EVSE.xml @@ -83,6 +83,18 @@ Davis, CA 95616, USA + + + + + + + + + + + + diff --git a/data_model/1.6/device_types/HeatPump.xml b/data_model/1.6/device_types/HeatPump.xml index 6d7774b25eb0f6..06cddca8621a6a 100644 --- a/data_model/1.6/device_types/HeatPump.xml +++ b/data_model/1.6/device_types/HeatPump.xml @@ -73,6 +73,15 @@ Davis, CA 95616, USA + + + + + + + + + @@ -82,6 +91,19 @@ Davis, CA 95616, USA + + + + + + + + + + + + + @@ -90,13 +112,45 @@ Davis, CA 95616, USA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data_model/1.6/device_types/Intercom.xml b/data_model/1.6/device_types/Intercom.xml index 657ab802e4899e..7f7071b2ac4e7c 100644 --- a/data_model/1.6/device_types/Intercom.xml +++ b/data_model/1.6/device_types/Intercom.xml @@ -125,6 +125,15 @@ Davis, CA 95616, USA + + + + + + + + + diff --git a/data_model/1.6/device_types/MeterReferencePoint.xml b/data_model/1.6/device_types/MeterReferencePoint.xml index a17b4e77de4d08..33dfb8d23bf0e9 100644 --- a/data_model/1.6/device_types/MeterReferencePoint.xml +++ b/data_model/1.6/device_types/MeterReferencePoint.xml @@ -85,6 +85,20 @@ Davis, CA 95616, USA + + + + + + + + + + + + + + diff --git a/data_model/1.6/device_types/SolarPower.xml b/data_model/1.6/device_types/SolarPower.xml index 5b3bb2f5367f9d..7efb30fe926804 100644 --- a/data_model/1.6/device_types/SolarPower.xml +++ b/data_model/1.6/device_types/SolarPower.xml @@ -70,6 +70,15 @@ Davis, CA 95616, USA + + + + + + + + + @@ -82,9 +91,22 @@ Davis, CA 95616, USA + + + + + + + + + + + + + @@ -93,9 +115,34 @@ Davis, CA 95616, USA + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data_model/1.6/device_types/WaterHeater.xml b/data_model/1.6/device_types/WaterHeater.xml index cea2109748407b..01df30739a2391 100644 --- a/data_model/1.6/device_types/WaterHeater.xml +++ b/data_model/1.6/device_types/WaterHeater.xml @@ -84,6 +84,15 @@ Davis, CA 95616, USA + + + + + + + + + From 0f2d955d8e3b60e0d6305cc377a2af0b6e5fb094 Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Wed, 27 May 2026 13:45:17 +0000 Subject: [PATCH 13/21] Update test step to support complex composed device type and element requirement check --- .../testing/device_conformance_tests.py | 203 ++++++---- .../matter/testing/spec_parsing.py | 249 ++++++------ .../TestComposedDeviceTypeMatching.py | 358 ++++++++++++++++++ .../TestSpecParsingComposedDeviceTypes.py | 173 +++++++++ 4 files changed, 793 insertions(+), 190 deletions(-) create mode 100644 src/python_testing/test_testing/TestComposedDeviceTypeMatching.py create mode 100644 src/python_testing/test_testing/TestSpecParsingComposedDeviceTypes.py diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py index 8b58ac7e61d616..23960e7fa9b108 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py @@ -23,7 +23,7 @@ from matter.testing.basic_composition import BasicCompositionTests from matter.testing.choice_conformance import (evaluate_attribute_choice_conformance, evaluate_command_choice_conformance, evaluate_feature_choice_conformance) -from matter.testing.conformance import EMPTY_CLUSTER_GLOBAL_ATTRIBUTES, ConformanceAssessmentData, conformance_allowed +from matter.testing.conformance import EMPTY_CLUSTER_GLOBAL_ATTRIBUTES, ConformanceAssessmentData, ConformanceDecision, conformance_allowed from matter.testing.global_attribute_ids import (ClusterIdType, DeviceTypeIdType, GlobalAttributeIds, cluster_id_type, device_type_id_type, is_valid_device_type_id) from matter.testing.problem_notices import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, DeviceTypePathLocation, @@ -347,6 +347,40 @@ def record_error(location, problem): location, f"Expected Device type revision for device type {device_type_id} {self.xml_device_types[device_type_id].name} on endpoint {endpoint_id} does not match revision on DUT. Expected: {expected_revision} DUT: {actual_revision}") return success, problems + def _check_feature_overrides(self, cluster_requirement, cluster_info, feature_map, record_error, location, device_type_desc, allow_provisional): + for mask, conformance in cluster_requirement.feature_overrides.items(): + conformance_decision_with_choice = conformance(cluster_info) + if conformance_decision_with_choice.is_mandatory() and ((feature_map & mask) == 0): + record_error( + location=location, problem=f"Feature bit {mask.bit_length() - 1} in cluster {cluster_requirement.name} is required by element override for {device_type_desc}, but is not present in the feature map") + if not conformance_allowed(conformance_decision_with_choice, allow_provisional) and ((feature_map & mask) != 0): + record_error( + location=location, problem=f"Feature bit {mask.bit_length() - 1} in cluster {cluster_requirement.name} is disallowed by element override for {device_type_desc}, but is present in the feature map") + + def _check_attribute_overrides(self, cluster_requirement, cluster_info, attribute_list, record_error, location, device_type_desc, allow_provisional, device_type_id, cluster_id): + for _id, conformance in cluster_requirement.attribute_overrides.items(): + conformance_decision_with_choice = conformance(cluster_info) + if conformance_decision_with_choice.is_mandatory() and _id not in attribute_list: + record_error( + location=location, problem=f"Attribute {_id} in cluster {cluster_requirement.name} is required by element override for {device_type_desc}, but is not present in the attribute list") + if not conformance_allowed(conformance_decision_with_choice, allow_provisional) and _id in attribute_list: + if device_type_id == 0x050F and cluster_id == Clusters.Thermostat.id and _id == Clusters.Thermostat.Attributes.SystemMode.attribute_id: + # This is a specific problem in the water heater device type where it is specifically disallowing a thing that shouldn't be disallowed + # For now, ignore this requirement until the spec is fixed + continue + record_error( + location=location, problem=f"Attribute {_id} in cluster {cluster_requirement.name} is disallowed by element override for {device_type_desc}, but is present in the attribute list") + + def _check_command_overrides(self, cluster_requirement, cluster_info, cmd_list, record_error, location, device_type_desc, allow_provisional): + for _id, conformance in cluster_requirement.command_overrides.items(): + conformance_decision_with_choice = conformance(cluster_info) + if conformance_decision_with_choice.is_mandatory() and _id not in cmd_list: + record_error( + location=location, problem=f"Command {_id} in cluster {cluster_requirement.name} is required by element override for {device_type_desc}, but is not present in the cmd list") + if not conformance_allowed(conformance_decision_with_choice, allow_provisional) and _id in cmd_list: + record_error( + location=location, problem=f"Command {_id} in cluster {cluster_requirement.name} is disallowed by element override for {device_type_desc}, but is present in the cmd list") + def check_device_type(self, fail_on_extra_clusters: bool = True, allow_provisional_test_event_only_disallowed_for_certification: bool = False) -> tuple[bool, list[ProblemNotice]]: success = True problems = [] @@ -419,40 +453,6 @@ def record_warning(location, problem): # Optional cluster not on this endpoint continue - def check_feature_overrides(cluster_requirement: XmlDeviceTypeClusterRequirements, cluster_info: ConformanceAssessmentData): - for mask, conformance in cluster_requirement.feature_overrides.items(): - conformance_decision_with_choice = conformance(cluster_info) - if conformance_decision_with_choice.is_mandatory() and ((feature_map & mask) == 0): - record_error( - location=location, problem=f"Feature bit {mask.bit_length() - 1} in cluster {cluster_requirement.name} is required by element override for device type {xml_device.name}, but is not present in the feature map") - if not conformance_allowed(conformance_decision_with_choice, allow_provisional_test_event_only_disallowed_for_certification) and ((feature_map & mask) != 0): - record_error( - location=location, problem=f"Feature bit {mask.bit_length() - 1} in cluster {cluster_requirement.name} is disallowed by element override for device type {xml_device.name}, but is present in the feature map") - - def check_attribute_overrides(cluster_requirement: XmlDeviceTypeClusterRequirements, cluster_info: ConformanceAssessmentData) -> None: - for _id, conformance in cluster_requirement.attribute_overrides.items(): - conformance_decision_with_choice = conformance(cluster_info) - if conformance_decision_with_choice.is_mandatory() and _id not in attribute_list: - record_error( - location=location, problem=f"Attribute {_id} in cluster {cluster_requirement.name} is required by element override for device type {xml_device.name}, but is not present in the attribute list") - if not conformance_allowed(conformance_decision_with_choice, allow_provisional_test_event_only_disallowed_for_certification) and _id in attribute_list: - if device_type_id == water_heater_id and cluster_id == Clusters.Thermostat.id and _id == Clusters.Thermostat.Attributes.SystemMode.attribute_id: - # This is a specific problem in the water heater device type where it is specifically disallowing a thing that shouldn't be disallowed - # For now, ignore this requirement until the spec is fixed - continue - record_error( - location=location, problem=f"Attribute {_id} in cluster {cluster_requirement.name} is disallowed by element override for device type {xml_device.name}, but is present in the attribute list") - - def check_command_overrides(cluster_requirement: XmlDeviceTypeClusterRequirements, cluster_info: ConformanceAssessmentData): - for _id, conformance in cluster_requirement.command_overrides.items(): - conformance_decision_with_choice = conformance(cluster_info) - if conformance_decision_with_choice.is_mandatory() and _id not in cmd_list: - record_error( - location=location, problem=f"Command {_id} in cluster {cluster_requirement.name} is required by element override for device type {xml_device.name}, but is not present in the cmd list") - if not conformance_allowed(conformance_decision_with_choice, allow_provisional_test_event_only_disallowed_for_certification) and _id in cmd_list: - record_error( - location=location, problem=f"Command {_id} in cluster {cluster_requirement.name} is disallowed by element override for device type {xml_device.name}, but is present in the cmd list") - cluster = Clusters.ClusterObjects.ALL_CLUSTERS[cluster_id] feature_map = endpoint[cluster][cluster.Attributes.FeatureMap] attribute_list = endpoint[cluster][cluster.Attributes.AttributeList] @@ -460,9 +460,9 @@ def check_command_overrides(cluster_requirement: XmlDeviceTypeClusterRequirement revision = endpoint[cluster][cluster.Attributes.ClusterRevision] cluster_info = ConformanceAssessmentData(feature_map, attribute_list, cmd_list, revision) - check_feature_overrides(cluster_requirement, cluster_info) - check_attribute_overrides(cluster_requirement, cluster_info) - check_command_overrides(cluster_requirement, cluster_info) + self._check_feature_overrides(cluster_requirement, cluster_info, feature_map, record_error, location, f"device type {xml_device.name}", allow_provisional_test_event_only_disallowed_for_certification) + self._check_attribute_overrides(cluster_requirement, cluster_info, attribute_list, record_error, location, f"device type {xml_device.name}", allow_provisional_test_event_only_disallowed_for_certification, device_type_id, cluster_id) + self._check_command_overrides(cluster_requirement, cluster_info, cmd_list, record_error, location, f"device type {xml_device.name}", allow_provisional_test_event_only_disallowed_for_certification) # If we want to check for extra clusters on the endpoint, we need to know the entire set of clusters in all the device type # lists across all the device types on the endpoint. @@ -508,40 +508,113 @@ def record_error(location, problem): continue xml_device = self.xml_device_types[device_type_id] + from collections import defaultdict + reqs_by_dt = defaultdict(list) for req in xml_device.composed_device_types: - # Conformance Assessment - conformance_decision = req.conformance(EMPTY_CLUSTER_GLOBAL_ATTRIBUTES) - - # Count instances in child endpoints - parts_list = [] - if Clusters.Descriptor.Attributes.PartsList in endpoint[Clusters.Descriptor]: - parts_list = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] - - count = 0 + reqs_by_dt[req.device_type_id].append(req) + + parts_list = [] + if Clusters.Descriptor.Attributes.PartsList in endpoint[Clusters.Descriptor]: + parts_list = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] + + for req_dt_id, req_list in reqs_by_dt.items(): + # Find all child endpoints that have this device type + matching_eps = [] for child_ep_id in parts_list: if child_ep_id in self.endpoints: child_ep = self.endpoints[child_ep_id] if Clusters.Descriptor in child_ep: child_dt_list = child_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] - if any(child_dt.deviceType == req.device_type_id for child_dt in child_dt_list): - count += 1 - + if any(child_dt.deviceType == req_dt_id for child_dt in child_dt_list): + matching_eps.append(child_ep_id) + + # For each requirement, check which endpoints satisfy it + req_matches = defaultdict(list) + for req_idx, req in enumerate(req_list): + for ep_id in matching_eps: + child_ep = self.endpoints[ep_id] + server_list = child_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.ServerList] if Clusters.Descriptor.Attributes.ServerList in child_ep[Clusters.Descriptor] else [] + + matches = True + for cid, cr in req.cluster_requirements.items(): + cconformance = cr.conformance(EMPTY_CLUSTER_GLOBAL_ATTRIBUTES) + if cconformance.is_mandatory() and cid not in server_list: + matches = False + break + elif cconformance.decision == ConformanceDecision.DISALLOWED and cid in server_list: + matches = False + break + + if matches: + # Also check element overrides! + failed_override = False + def dummy_record_error(location, problem): + nonlocal failed_override + failed_override = True + + override_location = DeviceTypePathLocation(endpoint_id=ep_id, device_type_id=req.device_type_id) + + for cid, cr in req.cluster_requirements.items(): + if cid not in server_list: + continue + cluster = Clusters.ClusterObjects.ALL_CLUSTERS[cid] + feature_map = child_ep[cluster][cluster.Attributes.FeatureMap] + attribute_list = child_ep[cluster][cluster.Attributes.AttributeList] + cmd_list = child_ep[cluster][cluster.Attributes.AcceptedCommandList] + revision = child_ep[cluster][cluster.Attributes.ClusterRevision] + cluster_info = ConformanceAssessmentData(feature_map, attribute_list, cmd_list, revision) + + self._check_feature_overrides(cr, cluster_info, feature_map, dummy_record_error, override_location, f"composed device type {req.device_type_name}", allow_provisional) + self._check_attribute_overrides(cr, cluster_info, attribute_list, dummy_record_error, override_location, f"composed device type {req.device_type_name}", allow_provisional, req.device_type_id, cid) + self._check_command_overrides(cr, cluster_info, cmd_list, dummy_record_error, override_location, f"composed device type {req.device_type_name}", allow_provisional) + + if not failed_override: + req_matches[req_idx].append(ep_id) + + # Try to satisfy all reqs with overrides first! + reqs_with_overrides = [] + for r in req_list: + if r.cluster_requirements: + reqs_with_overrides.append(r) + + reqs_without_overrides = [r for r in req_list if r not in reqs_with_overrides] + + def satisfy_overrides(idx, assigned): + if idx == len(reqs_with_overrides): + return assigned + req = reqs_with_overrides[idx] + req_idx = req_list.index(req) + for ep_id in req_matches[req_idx]: + if ep_id not in assigned: + res = satisfy_overrides(idx + 1, assigned | {ep_id}) + if res is not None: + return res + return None + + assigned_for_overrides = satisfy_overrides(0, set()) + location = DeviceTypePathLocation(endpoint_id=endpoint_id, device_type_id=device_type_id) - - if conformance_decision.is_mandatory() and count == 0: - record_error( - location, f"Mandatory composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} on endpoint {endpoint_id} is missing in child endpoints") - elif not conformance_allowed(conformance_decision, allow_provisional) and count > 0: - record_error( - location, f"Disallowed composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} on endpoint {endpoint_id} is present in child endpoints") - - if conformance_allowed(conformance_decision, allow_provisional): - if req.min_instances is not None and count < req.min_instances: - record_error( - location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} on endpoint {endpoint_id} expects at least {req.min_instances} instances in child endpoints, but found {count}") - if req.max_instances is not None and count > req.max_instances: - record_error( - location, f"Composed device type {req.device_type_name} ({req.device_type_id}) for {xml_device.name} on endpoint {endpoint_id} expects at most {req.max_instances} instances in child endpoints, but found {count}") + + if reqs_with_overrides and assigned_for_overrides is None: + record_error(location, f"Could not find distinct child endpoints satisfying all labeled instances of composed device type {req_list[0].device_type_name}") + continue + + # Now check constraints on base requirements + total_matching = len(matching_eps) + + for req in reqs_without_overrides: + conformance_decision = req.conformance(EMPTY_CLUSTER_GLOBAL_ATTRIBUTES) + + if conformance_decision.is_mandatory() and total_matching == 0: + record_error(location, f"Mandatory composed device type {req.device_type_name} ({req.device_type_id}) is missing in child endpoints") + elif not conformance_allowed(conformance_decision, allow_provisional) and total_matching > 0: + record_error(location, f"Disallowed composed device type {req.device_type_name} ({req.device_type_id}) is present in child endpoints") + + if conformance_allowed(conformance_decision, allow_provisional): + if req.min_instances is not None and total_matching < req.min_instances: + record_error(location, f"Composed device type {req.device_type_name} ({req.device_type_id}) expects at least {req.min_instances} instances in child endpoints, but found {total_matching}") + if req.max_instances is not None and total_matching > req.max_instances: + record_error(location, f"Composed device type {req.device_type_name} ({req.device_type_id}) expects at most {req.max_instances} instances in child endpoints, but found {total_matching}") return success, problems diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index 4e4eb4aff56f83..77c8da59cee6f3 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -253,6 +253,7 @@ class XmlComposedDeviceTypeRequirement: conformance: ConformanceCallable min_instances: Optional[int] = None max_instances: Optional[int] = None + cluster_requirements: dict[uint, XmlDeviceTypeClusterRequirements] = field(default_factory=dict) @dataclass @@ -1366,6 +1367,65 @@ def build_xml_namespaces(data_model_directory: typing.Union[PrebuiltDataModelDir def parse_single_device_type(root: ElementTree.Element, cluster_definition_xml: dict[uint, XmlCluster]) -> tuple[dict[int, XmlDeviceType], list[ProblemNotice]]: problems: list[ProblemNotice] = [] device_types: dict[int, XmlDeviceType] = {} + def append_overrides(override_element_type: str, c: ElementTree.Element, cluster: XmlDeviceTypeClusterRequirements, c_id: uint, cluster_conformance_params: ConformanceParseParameters, location, problems: list[ProblemNotice]): + if override_element_type == 'feature': + name_to_id_map = {f.name: _id for _id, f in cluster_definition_xml[c_id].features.items()} + name_to_id_map.update(cluster_definition_xml[c_id].feature_map) + override = cluster.feature_overrides + elif override_element_type == 'attribute': + name_to_id_map = cluster_definition_xml[c_id].attribute_map + override = cluster.attribute_overrides + elif override_element_type == 'command': + name_to_id_map = cluster_definition_xml[c_id].command_map + override = cluster.command_overrides + else: + problems.append(ProblemNotice("Parse Device Type XML", location=location, + severity=ProblemSeverity.WARNING, problem=f"Request for unknown element override type {override_element_type} - this is a script error")) + return + + container = c.find(f'{override_element_type}s') + if container is None: + return + + elements = container.iter(override_element_type) + for e in elements: + try: + element_name = e.attrib['name'] + except KeyError: + if override_element_type == 'feature': + try: + element_name = e.attrib['code'] + except KeyError: + element_name = None + else: + element_name = None + if element_name is None: + problems.append(ProblemNotice("Parse Device Type XML", location=location, + severity=ProblemSeverity.WARNING, problem=f"Missing {override_element_type} name for override in cluster 0x{c_id:04X}, e={str(e.attrib)}")) + continue + + try: + conformance_xml, tmp_problem = get_conformance(e, c_id) + if tmp_problem: + continue + conformance_override = parse_callable_from_xml(conformance_xml, cluster_conformance_params) + + from matter.testing.spec_parsing import _fuzzy_name, is_disallowed + map_id = [name_to_id_map[n] for n in name_to_id_map if _fuzzy_name(n) == _fuzzy_name(element_name)] + if len(map_id) == 0: + if is_disallowed(conformance_override): + import logging + logging.getLogger(__name__).info(f"Ignoring unknown {override_element_type} {element_name} in cluster {c_id} because the conformance is disallowed") + continue + problems.append(ProblemNotice("Parse Device Type XML", location=location, + severity=ProblemSeverity.WARNING, problem=f"Unknown {override_element_type} {element_name} in cluster 0x{c_id:04X} - map = {map_id}")) + else: + override[map_id[0]] = conformance_override + + except ConformanceException as ex: + problems.append(ProblemNotice("Parse Device Type XML", location=location, + severity=ProblemSeverity.WARNING, problem=f"Unable to parse {override_element_type} conformance for {element_name} in cluster 0x{c_id:04X} - {ex}")) + d = root if d.tag == 'deviceType': device_name = d.attrib['name'] @@ -1422,7 +1482,7 @@ def parse_single_device_type(root: ElementTree.Element, cluster_definition_xml: for c in clusters: try: try: - cid = uint(int(c.attrib['id'], 0)) + c_id = uint(int(c.attrib['id'], 0)) except ValueError: location = DeviceTypePathLocation(device_type_id=tid) problems.append(ProblemNotice("Parse Device Type XML", location=location, @@ -1430,155 +1490,94 @@ def parse_single_device_type(root: ElementTree.Element, cluster_definition_xml: continue # Workaround for 1.3 device types with zigbee clusters and old scenes # This is OK because there are other tests that ensure that unknown clusters do not appear on the device - if cid not in cluster_definition_xml: - LOGGER.info(f"Skipping unknown cluster {cid:04X}") + if c_id not in cluster_definition_xml: + LOGGER.info(f"Skipping unknown cluster {c_id:04X}") continue - conformance_xml, tmp_problem = get_conformance(c, cid) + conformance_xml, tmp_problem = get_conformance(c, c_id) if tmp_problem: problems.append(tmp_problem) continue cluster_conformance_params = ConformanceParseParameters( - feature_map=cluster_definition_xml[cid].feature_map, attribute_map=cluster_definition_xml[cid].attribute_map, command_map=cluster_definition_xml[cid].command_map) + feature_map=cluster_definition_xml[c_id].feature_map, attribute_map=cluster_definition_xml[c_id].attribute_map, command_map=cluster_definition_xml[c_id].command_map) conformance = parse_callable_from_xml(conformance_xml, cluster_conformance_params) side_dict = {'server': ClusterSide.SERVER, 'client': ClusterSide.CLIENT} side = side_dict[c.attrib['side']] cluster_name = c.attrib['name'] - if cid in CLUSTER_NAME_FIXES: - cluster_name = CLUSTER_NAME_FIXES[cid] + if c_id in CLUSTER_NAME_FIXES: + cluster_name = CLUSTER_NAME_FIXES[c_id] cluster = XmlDeviceTypeClusterRequirements(name=cluster_name, side=side, conformance=conformance) - def append_overrides(override_element_type: str): - if override_element_type == 'feature': - # The device types use feature name rather than feature code. So we need to build a new map. - name_to_id_map = {f.name: _id for _id, f in cluster_definition_xml[cid].features.items()} - # But also...that's not universal, so let's be tolerant to using the code too. - name_to_id_map.update(cluster_definition_xml[cid].feature_map) - override = cluster.feature_overrides - elif override_element_type == 'attribute': - name_to_id_map = cluster_definition_xml[cid].attribute_map - override = cluster.attribute_overrides - elif override_element_type == 'command': - name_to_id_map = cluster_definition_xml[cid].command_map - override = cluster.command_overrides - else: - problems.append(ProblemNotice("Parse Device Type XML", location=location, - severity=ProblemSeverity.WARNING, problem=f"Request for unknown element override type {override_element_type} - this is a script error")) - return - - container = c.find(f'{override_element_type}s') - if container is None: - return - - elements = container.iter(override_element_type) - for e in elements: - try: - element_name = e.attrib['name'] - except KeyError: - if override_element_type == 'feature': - try: - element_name = e.attrib['code'] - except KeyError: - element_name = None - else: - element_name = None - if element_name is None: - problems.append(ProblemNotice("Parse Device Type XML", location=location, - severity=ProblemSeverity.WARNING, problem=f"Missing {override_element_type} name for override in cluster 0x{cid:04X}, e={str(e.attrib)}")) - continue - - try: - conformance_xml, tmp_problem = get_conformance(e, cid) - if tmp_problem: - # It's not actually a problem if there is no conformance override - it might be a constraint override. Just continue - continue - conformance_override = parse_callable_from_xml(conformance_xml, cluster_conformance_params) - - map_id = [name_to_id_map[n] for n in name_to_id_map if _fuzzy_name(n) == - _fuzzy_name(element_name)] - if len(map_id) == 0: - # The thermostat in particular explicitly disallows some zigbee things that don't appear in the spec due to - # ifdefs. We can ignore problems if the device type spec disallows things that don't exist. - if is_disallowed(conformance_override): - LOGGER.info( - f"Ignoring unknown {override_element_type} {element_name} in cluster {cid} because the conformance is disallowed") - continue - problems.append(ProblemNotice("Parse Device Type XML", location=location, - severity=ProblemSeverity.WARNING, problem=f"Unknown {override_element_type} {element_name} in cluster 0x{cid:04X} - map = {map_id}")) - else: - override[map_id[0]] = conformance_override - - except ConformanceException as ex: - problems.append(ProblemNotice("Parse Device Type XML", location=location, - severity=ProblemSeverity.WARNING, problem=f"Unable to parse {override_element_type} conformance for {element_name} in cluster 0x{cid:04X} - {ex}")) - - append_overrides('feature') - append_overrides('attribute') - append_overrides('command') + append_overrides('feature', c, cluster, c_id, cluster_conformance_params, location, problems) + append_overrides('attribute', c, cluster, c_id, cluster_conformance_params, location, problems) + append_overrides('command', c, cluster, c_id, cluster_conformance_params, location, problems) if side == ClusterSide.SERVER: - device_types[tid].server_clusters[cid] = cluster + device_types[tid].server_clusters[c_id] = cluster else: - device_types[tid].client_clusters[cid] = cluster + device_types[tid].client_clusters[c_id] = cluster except ConformanceException as ex: - location = DeviceTypePathLocation(device_type_id=tid, cluster_id=cid) + location = DeviceTypePathLocation(device_type_id=tid, cluster_id=c_id) problems.append(ProblemNotice("Parse Device Type XML", location=location, severity=ProblemSeverity.WARNING, problem=f"Unable to parse conformance for cluster - {ex}")) # NOTE: Spec currently does a bad job of matching these exactly to the names and codes # so this will need a bit of fancy handling here to get this right. try: - main_composed_elements = d.findall('composedDeviceTypes') - for composed in main_composed_elements: - for composed_dt in composed.findall('deviceType'): - try: - composed_id = int(composed_dt.attrib['deviceTypeId'], 0) - composed_name = composed_dt.attrib['deviceTypeName'] - except (KeyError, ValueError): - problems.append(ProblemNotice("Parse Device Type XML", location=location, - severity=ProblemSeverity.WARNING, problem="Invalid composed device type id or name")) - continue + composed_dts = d.findall('composedDeviceTypes/deviceType') + for composed_dt in composed_dts: + composed_id = int(composed_dt.attrib['deviceTypeId'], 0) + composed_name = composed_dt.attrib['deviceTypeName'] + location = DeviceTypePathLocation(device_type_id=tid) - # Conformance - conformance_xml, tmp_problem = get_conformance(composed_dt, uint(id)) - if tmp_problem: - # Composed device types in versions 1.5.1 and earlier often lack explicit conformance in XML. - # Default them to optionalConform to prevent parser warnings. - # TODO: Report as a problem once all data_model files are updated with composedDeviceType definitions. - conformance_xml = ElementTree.Element('optionalConform') + # Conformance + conformance_xml, _ = get_conformance(composed_dt, uint(tid)) - try: - conformance = parse_callable_from_xml(conformance_xml, ConformanceParseParameters( - feature_map={}, attribute_map={}, command_map={})) - except ConformanceException as ex: - problems.append(ProblemNotice("Parse Device Type XML", location=location, - severity=ProblemSeverity.WARNING, problem=f"Unable to parse conformance for composed device type - {ex}")) - continue - min_instances = None - max_instances = None - constraint = composed_dt.find('constraint') - if constraint is not None: - allowed_el = constraint.find('allowed') - if allowed_el is not None and 'value' in allowed_el.attrib: - min_instances = int(allowed_el.attrib['value'], 0) - max_instances = int(allowed_el.attrib['value'], 0) - else: - min_el = constraint.find('min') - if min_el is not None and 'value' in min_el.attrib: - min_instances = int(min_el.attrib['value'], 0) - - max_el = constraint.find('max') - if max_el is not None and 'value' in max_el.attrib: - max_instances = int(max_el.attrib['value'], 0) - - device_types[id].composed_device_types.append(XmlComposedDeviceTypeRequirement( - device_type_id=composed_id, - device_type_name=composed_name, - conformance=conformance, - min_instances=min_instances, - max_instances=max_instances - )) + conformance = parse_callable_from_xml(conformance_xml, ConformanceParseParameters( + feature_map={}, attribute_map={}, command_map={})) + + min_instances = None + max_instances = None + + if (constraint := composed_dt.find('constraint')) is not None: + if (allowed := constraint.find('allowed')) is not None: + min_instances = max_instances = int(allowed.attrib.get('value', 0), 0) + else: + min_el = constraint.find('min') + max_el = constraint.find('max') + + min_instances = int(min_el.attrib.get('value', 0), 0) if min_el is not None else None + max_instances = int(max_el.attrib.get('value', 0), 0) if max_el is not None else None + + cluster_requirements = {} + cr_el = composed_dt.find('clusterRequirements') + if cr_el is not None: + for c in cr_el.findall('cluster'): + c_id = uint(int(c.attrib['id'], 0)) + c_name = c.attrib['name'] + c_conformance_xml, _ = get_conformance(c, c_id) + c_conformance = parse_callable_from_xml(c_conformance_xml, ConformanceParseParameters(feature_map={}, attribute_map={}, command_map={})) + cluster = XmlDeviceTypeClusterRequirements(name=c_name, side=ClusterSide.SERVER, conformance=c_conformance) + cluster_requirements[c_id] = cluster + + cluster_conformance_params = ConformanceParseParameters( + feature_map=cluster_definition_xml[c_id].feature_map, + attribute_map=cluster_definition_xml[c_id].attribute_map, + command_map=cluster_definition_xml[c_id].command_map + ) + append_overrides('feature', c, cluster, c_id, cluster_conformance_params, location, problems) + append_overrides('attribute', c, cluster, c_id, cluster_conformance_params, location, problems) + append_overrides('command', c, cluster, c_id, cluster_conformance_params, location, problems) + + device_types[tid].composed_device_types.append(XmlComposedDeviceTypeRequirement( + device_type_id=composed_id, + device_type_name=composed_name, + conformance=conformance, + min_instances=min_instances, + max_instances=max_instances, + cluster_requirements=cluster_requirements + )) except Exception as e: problems.append(ProblemNotice("Parse Device Type XML", location=location, severity=ProblemSeverity.WARNING, problem=f"Error parsing composedDeviceTypes: {e}")) diff --git a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py new file mode 100644 index 00000000000000..90aee6744ebf35 --- /dev/null +++ b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py @@ -0,0 +1,358 @@ +# +# Copyright (c) 2026 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +from xml.etree import ElementTree +from collections import defaultdict + +from mobly import asserts + +import matter.clusters as Clusters +from matter.testing.device_conformance_tests import DeviceConformanceTests +from matter.testing.spec_parsing import XmlDeviceType, XmlComposedDeviceTypeRequirement, XmlDeviceTypeClusterRequirements, parse_callable_from_xml, ConformanceParseParameters, ClusterSide +from matter.testing.conformance import EMPTY_CLUSTER_GLOBAL_ATTRIBUTES, ConformanceAssessmentData +from matter.testing.problem_notices import ProblemNotice, ProblemSeverity, DeviceTypePathLocation +from matter.testing.runner import default_matter_test_main + +log = logging.getLogger(__name__) + +def get_mandatory_conformance(): + return parse_callable_from_xml(ElementTree.Element('mandatoryConform'), ConformanceParseParameters({}, {}, {})) + +def get_optional_conformance(): + return parse_callable_from_xml(ElementTree.Element('optionalConform'), ConformanceParseParameters({}, {}, {})) + +class TestComposedDeviceTypeMatching(DeviceConformanceTests): + def setup_class(self): + # We don't load real XMLs here to isolate the tests + self.xml_device_types = {} + self.xml_clusters = {} + self.problems = [] + + def _create_mock_composed_req(self, dt_id, name, conformance, min_instances=None, max_instances=None, cluster_requirements=None): + return XmlComposedDeviceTypeRequirement( + device_type_id=dt_id, + device_type_name=name, + conformance=conformance, + min_instances=min_instances, + max_instances=max_instances, + cluster_requirements=cluster_requirements or {} + ) + + # ========================================================================== + # Scenario 1: Simple 1-to-1 Match + # ========================================================================== + # Requirement: Parent device type requires exactly 1 instance of Child DT. + # Topology: Endpoint 1 has Parent DT. Endpoint 2 has Child DT listed in PartsList. + # Expected: PASS. + # ========================================================================== + def test_scenario_simple_match(self): + dt_parent_id = 0x0001 + dt_child_id = 0x0002 + + # Mock spec data + self.xml_device_types = { + dt_parent_id: XmlDeviceType( + name="Parent Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={}, + composed_device_types=[ + self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance(), min_instances=1, max_instances=1) + ] + ), + dt_child_id: XmlDeviceType( + name="Child Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={} + ) + } + + # Mock device endpoints + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [2] + } + }, + 2: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)] + } + } + } + + success, problems = self.check_composed_device_type_requirements() + for p in problems: + log.info(p) + asserts.assert_true(success, "Failure on simple 1-to-1 match scenario") + + # ========================================================================== + # Scenario 2: Missing Mandatory Instance + # ========================================================================== + # Requirement: Parent device type requires exactly 1 instance of Child DT. + # Topology: Endpoint 1 has Parent DT. PartsList is empty. + # Expected: FAIL (Missing mandatory composed device type). + # ========================================================================== + def test_scenario_missing_mandatory(self): + dt_parent_id = 0x0001 + dt_child_id = 0x0002 + + self.xml_device_types = { + dt_parent_id: XmlDeviceType( + name="Parent Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={}, + composed_device_types=[ + self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance(), min_instances=1, max_instances=1) + ] + ), + dt_child_id: XmlDeviceType( + name="Child Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={} + ) + } + + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [] + } + } + } + + success, problems = self.check_composed_device_type_requirements() + asserts.assert_false(success, "Unexpected success when mandatory composed device type is missing") + + # ========================================================================== + # Scenario 3: Bipartite Matching (Distinct Overrides) + # ========================================================================== + # Requirement: Parent requires TWO instances of Child DT. + # Instance #1 requires Cluster X to be present. + # Instance #2 requires Cluster Y to be present. + # Topology: Endpoint 1 has Parent DT. PartsList = [2, 3]. + # Endpoint 2 has Child DT and Cluster X. + # Endpoint 3 has Child DT and Cluster Y. + # Expected: PASS (The test should find a valid 1-to-1 assignment). + # ========================================================================== + def test_scenario_bipartite_matching(self): + dt_parent_id = 0x0001 + dt_child_id = 0x0002 + cluster_x_id = 0x0090 + cluster_y_id = 0x0091 + + # Mock spec data + req_base = self._create_mock_composed_req(dt_child_id, "Child Device Base", get_mandatory_conformance(), min_instances=2) + + req1 = self._create_mock_composed_req(dt_child_id, "Child Device #1", get_mandatory_conformance()) + req1.cluster_requirements = { + cluster_x_id: XmlDeviceTypeClusterRequirements(name="Cluster X", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + } + + req2 = self._create_mock_composed_req(dt_child_id, "Child Device #2", get_mandatory_conformance()) + req2.cluster_requirements = { + cluster_y_id: XmlDeviceTypeClusterRequirements(name="Cluster Y", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + } + + self.xml_device_types = { + dt_parent_id: XmlDeviceType( + name="Parent Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={}, + composed_device_types=[req_base, req1, req2] + ), + dt_child_id: XmlDeviceType( + name="Child Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={} + ) + } + + # Mock device endpoints + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [2, 3] + } + }, + 2: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_x_id] + }, + Clusters.ElectricalPowerMeasurement: { + Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x01, + Clusters.ElectricalPowerMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalPowerMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 + } + }, + 3: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_y_id] + }, + Clusters.ElectricalEnergyMeasurement: { + Clusters.ElectricalEnergyMeasurement.Attributes.FeatureMap: 0x02, + Clusters.ElectricalEnergyMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalEnergyMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalEnergyMeasurement.Attributes.ClusterRevision: 1 + } + } + } + + success, problems = self.check_composed_device_type_requirements() + for p in problems: + log.info(p) + asserts.assert_true(success, "Failure on bipartite matching scenario") + + # ========================================================================== + # Scenario 4: Bipartite Matching Conflict + # ========================================================================== + # Requirement: Parent requires TWO instances of Child DT. + # Instance #1 requires Cluster X. + # Instance #2 requires Cluster Y. + # Topology: Endpoint 1 has Parent DT. PartsList = [2, 3]. + # Endpoint 2 has Child DT and Cluster X. + # Endpoint 3 has Child DT and Cluster X (Conflict!). + # Expected: FAIL (Could not find distinct child endpoints satisfying all labeled instances). + # ========================================================================== + def test_scenario_bipartite_conflict(self): + dt_parent_id = 0x0001 + dt_child_id = 0x0002 + cluster_x_id = 0x0090 + cluster_y_id = 0x0091 + + # Mock spec data + req1 = self._create_mock_composed_req(dt_child_id, "Child Device #1", get_mandatory_conformance()) + req1.cluster_requirements = { + cluster_x_id: XmlDeviceTypeClusterRequirements(name="Cluster X", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + } + + req2 = self._create_mock_composed_req(dt_child_id, "Child Device #2", get_mandatory_conformance()) + req2.cluster_requirements = { + cluster_y_id: XmlDeviceTypeClusterRequirements(name="Cluster Y", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + } + + self.xml_device_types = { + dt_parent_id: XmlDeviceType( + name="Parent Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={}, + composed_device_types=[req1, req2] + ), + dt_child_id: XmlDeviceType( + name="Child Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={} + ) + } + + # Mock device endpoints + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [2, 3] + } + }, + 2: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_x_id] + }, + Clusters.ElectricalPowerMeasurement: { + Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x01, + Clusters.ElectricalPowerMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalPowerMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 + } + }, + 3: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_x_id] # Conflict! + }, + Clusters.ElectricalPowerMeasurement: { + Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x01, + Clusters.ElectricalPowerMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalPowerMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 + } + } + } + + success, problems = self.check_composed_device_type_requirements() + asserts.assert_false(success, "Unexpected success in bipartite conflict scenario") + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/test_testing/TestSpecParsingComposedDeviceTypes.py b/src/python_testing/test_testing/TestSpecParsingComposedDeviceTypes.py new file mode 100644 index 00000000000000..a258f4863ab778 --- /dev/null +++ b/src/python_testing/test_testing/TestSpecParsingComposedDeviceTypes.py @@ -0,0 +1,173 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +from xml.etree import ElementTree + +from mobly import asserts + +import matter.clusters as Clusters +from matter.testing.device_conformance_tests import DeviceConformanceTests +from matter.testing.spec_parsing import parse_single_device_type, XmlCluster, XmlFeature, parse_callable_from_xml, ConformanceParseParameters +from matter.testing.runner import default_matter_test_main + +log = logging.getLogger(__name__) + +# A mock XML representing a device type with composedDeviceTypes +MOCK_DEVICE_TYPE_XML = """ + + + + + + + + + + + + + + + + + + + + + + + + +""" + +class TestSpecParsingComposedDeviceTypes(DeviceConformanceTests): + def setup_class(self): + self.xml_device_types = {} + self.xml_clusters = {} + self.problems = [] + + # Parse the mock XML + root = ElementTree.fromstring(MOCK_DEVICE_TYPE_XML) + + # We need to mock the cluster definition for the parser to resolve features + mock_cluster = XmlCluster( + name="Cluster X", + revision=1, + derived=None, + features={0x01: XmlFeature(code="ALTC", name="Alternate Cluster", conformance=parse_callable_from_xml(ElementTree.Element('mandatoryConform'), ConformanceParseParameters({}, {}, {})))}, + feature_map={"ALTC": 0x01}, # Map code to mask + attribute_map={}, + command_map={}, + attributes={}, + accepted_commands={}, + generated_commands={}, + unknown_commands=[], + events={}, + structs={}, + enums={}, + bitmaps={}, + pics="", + is_provisional=False, + revision_desc={} + ) + mock_clusters = {0x0090: mock_cluster} + + device_types, problems = parse_single_device_type(root, mock_clusters) + self.problems.extend(problems) + self.xml_device_types.update(device_types) + self.xml_clusters.update(mock_clusters) + + def test_parsing_correctness(self): + asserts.assert_equal(len(self.problems), 0, f"Unexpected problems during parsing: {self.problems}") + + dt_parent_id = 0x0001 + asserts.assert_in(dt_parent_id, self.xml_device_types, "Parent device type not found in parsed output") + + xml_device = self.xml_device_types[dt_parent_id] + asserts.assert_equal(len(xml_device.composed_device_types), 2, "Expected 2 composed device type requirements") + + # Check base requirement + req_base = xml_device.composed_device_types[0] + asserts.assert_equal(req_base.min_instances, 2, "Expected min_instances = 2 for base requirement") + + # Check override requirement + req_override = xml_device.composed_device_types[1] + asserts.assert_in(0x0090, req_override.cluster_requirements, "Expected cluster requirement for 0x0090") + + cr = req_override.cluster_requirements[0x0090] + asserts.assert_in(0x01, cr.feature_overrides, "Expected feature override for mask 0x01") + + def test_enforcement_with_fake_device(self): + dt_parent_id = 0x0001 + dt_child_id = 0x0002 + cluster_x_id = 0x0090 + + # Scenario: We need at least 2 child devices. + # One of them must have Cluster X with feature ALTC (mask 0x01). + + # Create mock endpoints + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [2, 3] + } + }, + 2: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_x_id] + }, + + } + } + + # Let's use the cluster class as key! + cluster_class = Clusters.ClustersObjects.ALL_CLUSTERS[cluster_x_id] if hasattr(Clusters, 'ClustersObjects') else None + # Wait, let's check how to get cluster class in TestConformanceTest! + # It used `Clusters.ClusterObjects.ALL_CLUSTERS[cid]`! + + cluster_class = Clusters.ClusterObjects.ALL_CLUSTERS[cluster_x_id] + + self.endpoints[2][cluster_class] = { + cluster_class.Attributes.FeatureMap: 0x01, # Has ALTC! + cluster_class.Attributes.AttributeList: [], + cluster_class.Attributes.AcceptedCommandList: [], + cluster_class.Attributes.ClusterRevision: 1 + } + + # EP3: Child device without Cluster X (to satisfy min 2 constraint) + self.endpoints[3] = { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [] + } + } + + success, problems = self.check_composed_device_type_requirements() + for p in problems: + log.info(p) + asserts.assert_true(success, "Failure on enforcement with fake device") + +if __name__ == "__main__": + default_matter_test_main() From 39049b328473fd0ca9fb2542f40a48a84fa04922 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 13:47:43 +0000 Subject: [PATCH 14/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../testing/device_conformance_tests.py | 82 +++++++++++-------- .../matter/testing/spec_parsing.py | 18 ++-- .../TestComposedDeviceTypeMatching.py | 67 ++++++++------- .../TestSpecParsingComposedDeviceTypes.py | 40 +++++---- 4 files changed, 118 insertions(+), 89 deletions(-) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py index 23960e7fa9b108..b2bad3fc9205e2 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py @@ -23,13 +23,14 @@ from matter.testing.basic_composition import BasicCompositionTests from matter.testing.choice_conformance import (evaluate_attribute_choice_conformance, evaluate_command_choice_conformance, evaluate_feature_choice_conformance) -from matter.testing.conformance import EMPTY_CLUSTER_GLOBAL_ATTRIBUTES, ConformanceAssessmentData, ConformanceDecision, conformance_allowed +from matter.testing.conformance import (EMPTY_CLUSTER_GLOBAL_ATTRIBUTES, ConformanceAssessmentData, ConformanceDecision, + conformance_allowed) from matter.testing.global_attribute_ids import (ClusterIdType, DeviceTypeIdType, GlobalAttributeIds, cluster_id_type, device_type_id_type, is_valid_device_type_id) from matter.testing.problem_notices import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, DeviceTypePathLocation, ProblemNotice, ProblemSeverity) -from matter.testing.spec_parsing import (CommandType, PrebuiltDataModelDirectory, XmlDeviceType, XmlDeviceTypeClusterRequirements, - build_xml_device_types, build_xml_namespaces) +from matter.testing.spec_parsing import (CommandType, PrebuiltDataModelDirectory, XmlDeviceType, build_xml_device_types, + build_xml_namespaces) from matter.tlv import uint logger = logging.getLogger(__name__) @@ -460,9 +461,12 @@ def record_warning(location, problem): revision = endpoint[cluster][cluster.Attributes.ClusterRevision] cluster_info = ConformanceAssessmentData(feature_map, attribute_list, cmd_list, revision) - self._check_feature_overrides(cluster_requirement, cluster_info, feature_map, record_error, location, f"device type {xml_device.name}", allow_provisional_test_event_only_disallowed_for_certification) - self._check_attribute_overrides(cluster_requirement, cluster_info, attribute_list, record_error, location, f"device type {xml_device.name}", allow_provisional_test_event_only_disallowed_for_certification, device_type_id, cluster_id) - self._check_command_overrides(cluster_requirement, cluster_info, cmd_list, record_error, location, f"device type {xml_device.name}", allow_provisional_test_event_only_disallowed_for_certification) + self._check_feature_overrides(cluster_requirement, cluster_info, feature_map, record_error, location, + f"device type {xml_device.name}", allow_provisional_test_event_only_disallowed_for_certification) + self._check_attribute_overrides(cluster_requirement, cluster_info, attribute_list, record_error, location, + f"device type {xml_device.name}", allow_provisional_test_event_only_disallowed_for_certification, device_type_id, cluster_id) + self._check_command_overrides(cluster_requirement, cluster_info, cmd_list, record_error, location, + f"device type {xml_device.name}", allow_provisional_test_event_only_disallowed_for_certification) # If we want to check for extra clusters on the endpoint, we need to know the entire set of clusters in all the device type # lists across all the device types on the endpoint. @@ -512,7 +516,7 @@ def record_error(location, problem): reqs_by_dt = defaultdict(list) for req in xml_device.composed_device_types: reqs_by_dt[req.device_type_id].append(req) - + parts_list = [] if Clusters.Descriptor.Attributes.PartsList in endpoint[Clusters.Descriptor]: parts_list = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] @@ -527,33 +531,35 @@ def record_error(location, problem): child_dt_list = child_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] if any(child_dt.deviceType == req_dt_id for child_dt in child_dt_list): matching_eps.append(child_ep_id) - + # For each requirement, check which endpoints satisfy it req_matches = defaultdict(list) for req_idx, req in enumerate(req_list): for ep_id in matching_eps: child_ep = self.endpoints[ep_id] - server_list = child_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.ServerList] if Clusters.Descriptor.Attributes.ServerList in child_ep[Clusters.Descriptor] else [] - + server_list = child_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.ServerList] if Clusters.Descriptor.Attributes.ServerList in child_ep[Clusters.Descriptor] else [ + ] + matches = True for cid, cr in req.cluster_requirements.items(): cconformance = cr.conformance(EMPTY_CLUSTER_GLOBAL_ATTRIBUTES) if cconformance.is_mandatory() and cid not in server_list: matches = False break - elif cconformance.decision == ConformanceDecision.DISALLOWED and cid in server_list: + if cconformance.decision == ConformanceDecision.DISALLOWED and cid in server_list: matches = False break - + if matches: # Also check element overrides! failed_override = False + def dummy_record_error(location, problem): nonlocal failed_override failed_override = True - + override_location = DeviceTypePathLocation(endpoint_id=ep_id, device_type_id=req.device_type_id) - + for cid, cr in req.cluster_requirements.items(): if cid not in server_list: continue @@ -563,22 +569,25 @@ def dummy_record_error(location, problem): cmd_list = child_ep[cluster][cluster.Attributes.AcceptedCommandList] revision = child_ep[cluster][cluster.Attributes.ClusterRevision] cluster_info = ConformanceAssessmentData(feature_map, attribute_list, cmd_list, revision) - - self._check_feature_overrides(cr, cluster_info, feature_map, dummy_record_error, override_location, f"composed device type {req.device_type_name}", allow_provisional) - self._check_attribute_overrides(cr, cluster_info, attribute_list, dummy_record_error, override_location, f"composed device type {req.device_type_name}", allow_provisional, req.device_type_id, cid) - self._check_command_overrides(cr, cluster_info, cmd_list, dummy_record_error, override_location, f"composed device type {req.device_type_name}", allow_provisional) - + + self._check_feature_overrides( + cr, cluster_info, feature_map, dummy_record_error, override_location, f"composed device type {req.device_type_name}", allow_provisional) + self._check_attribute_overrides(cr, cluster_info, attribute_list, dummy_record_error, override_location, + f"composed device type {req.device_type_name}", allow_provisional, req.device_type_id, cid) + self._check_command_overrides( + cr, cluster_info, cmd_list, dummy_record_error, override_location, f"composed device type {req.device_type_name}", allow_provisional) + if not failed_override: req_matches[req_idx].append(ep_id) - + # Try to satisfy all reqs with overrides first! reqs_with_overrides = [] for r in req_list: if r.cluster_requirements: reqs_with_overrides.append(r) - + reqs_without_overrides = [r for r in req_list if r not in reqs_with_overrides] - + def satisfy_overrides(idx, assigned): if idx == len(reqs_with_overrides): return assigned @@ -590,31 +599,36 @@ def satisfy_overrides(idx, assigned): if res is not None: return res return None - + assigned_for_overrides = satisfy_overrides(0, set()) - + location = DeviceTypePathLocation(endpoint_id=endpoint_id, device_type_id=device_type_id) - + if reqs_with_overrides and assigned_for_overrides is None: - record_error(location, f"Could not find distinct child endpoints satisfying all labeled instances of composed device type {req_list[0].device_type_name}") + record_error( + location, f"Could not find distinct child endpoints satisfying all labeled instances of composed device type {req_list[0].device_type_name}") continue - + # Now check constraints on base requirements total_matching = len(matching_eps) - + for req in reqs_without_overrides: conformance_decision = req.conformance(EMPTY_CLUSTER_GLOBAL_ATTRIBUTES) - + if conformance_decision.is_mandatory() and total_matching == 0: - record_error(location, f"Mandatory composed device type {req.device_type_name} ({req.device_type_id}) is missing in child endpoints") + record_error( + location, f"Mandatory composed device type {req.device_type_name} ({req.device_type_id}) is missing in child endpoints") elif not conformance_allowed(conformance_decision, allow_provisional) and total_matching > 0: - record_error(location, f"Disallowed composed device type {req.device_type_name} ({req.device_type_id}) is present in child endpoints") - + record_error( + location, f"Disallowed composed device type {req.device_type_name} ({req.device_type_id}) is present in child endpoints") + if conformance_allowed(conformance_decision, allow_provisional): if req.min_instances is not None and total_matching < req.min_instances: - record_error(location, f"Composed device type {req.device_type_name} ({req.device_type_id}) expects at least {req.min_instances} instances in child endpoints, but found {total_matching}") + record_error( + location, f"Composed device type {req.device_type_name} ({req.device_type_id}) expects at least {req.min_instances} instances in child endpoints, but found {total_matching}") if req.max_instances is not None and total_matching > req.max_instances: - record_error(location, f"Composed device type {req.device_type_name} ({req.device_type_id}) expects at most {req.max_instances} instances in child endpoints, but found {total_matching}") + record_error( + location, f"Composed device type {req.device_type_name} ({req.device_type_id}) expects at most {req.max_instances} instances in child endpoints, but found {total_matching}") return success, problems diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index 77c8da59cee6f3..cd74ef7a23a4e5 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -1367,6 +1367,7 @@ def build_xml_namespaces(data_model_directory: typing.Union[PrebuiltDataModelDir def parse_single_device_type(root: ElementTree.Element, cluster_definition_xml: dict[uint, XmlCluster]) -> tuple[dict[int, XmlDeviceType], list[ProblemNotice]]: problems: list[ProblemNotice] = [] device_types: dict[int, XmlDeviceType] = {} + def append_overrides(override_element_type: str, c: ElementTree.Element, cluster: XmlDeviceTypeClusterRequirements, c_id: uint, cluster_conformance_params: ConformanceParseParameters, location, problems: list[ProblemNotice]): if override_element_type == 'feature': name_to_id_map = {f.name: _id for _id, f in cluster_definition_xml[c_id].features.items()} @@ -1415,7 +1416,8 @@ def append_overrides(override_element_type: str, c: ElementTree.Element, cluster if len(map_id) == 0: if is_disallowed(conformance_override): import logging - logging.getLogger(__name__).info(f"Ignoring unknown {override_element_type} {element_name} in cluster {c_id} because the conformance is disallowed") + logging.getLogger(__name__).info( + f"Ignoring unknown {override_element_type} {element_name} in cluster {c_id} because the conformance is disallowed") continue problems.append(ProblemNotice("Parse Device Type XML", location=location, severity=ProblemSeverity.WARNING, problem=f"Unknown {override_element_type} {element_name} in cluster 0x{c_id:04X} - map = {map_id}")) @@ -1533,20 +1535,19 @@ def append_overrides(override_element_type: str, c: ElementTree.Element, cluster # Conformance conformance_xml, _ = get_conformance(composed_dt, uint(tid)) - conformance = parse_callable_from_xml(conformance_xml, ConformanceParseParameters( feature_map={}, attribute_map={}, command_map={})) min_instances = None max_instances = None - + if (constraint := composed_dt.find('constraint')) is not None: if (allowed := constraint.find('allowed')) is not None: min_instances = max_instances = int(allowed.attrib.get('value', 0), 0) else: min_el = constraint.find('min') max_el = constraint.find('max') - + min_instances = int(min_el.attrib.get('value', 0), 0) if min_el is not None else None max_instances = int(max_el.attrib.get('value', 0), 0) if max_el is not None else None @@ -1557,13 +1558,14 @@ def append_overrides(override_element_type: str, c: ElementTree.Element, cluster c_id = uint(int(c.attrib['id'], 0)) c_name = c.attrib['name'] c_conformance_xml, _ = get_conformance(c, c_id) - c_conformance = parse_callable_from_xml(c_conformance_xml, ConformanceParseParameters(feature_map={}, attribute_map={}, command_map={})) + c_conformance = parse_callable_from_xml(c_conformance_xml, ConformanceParseParameters( + feature_map={}, attribute_map={}, command_map={})) cluster = XmlDeviceTypeClusterRequirements(name=c_name, side=ClusterSide.SERVER, conformance=c_conformance) cluster_requirements[c_id] = cluster - + cluster_conformance_params = ConformanceParseParameters( - feature_map=cluster_definition_xml[c_id].feature_map, - attribute_map=cluster_definition_xml[c_id].attribute_map, + feature_map=cluster_definition_xml[c_id].feature_map, + attribute_map=cluster_definition_xml[c_id].attribute_map, command_map=cluster_definition_xml[c_id].command_map ) append_overrides('feature', c, cluster, c_id, cluster_conformance_params, location, problems) diff --git a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py index 90aee6744ebf35..6db829fd27af03 100644 --- a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py +++ b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py @@ -17,25 +17,26 @@ import logging from xml.etree import ElementTree -from collections import defaultdict from mobly import asserts import matter.clusters as Clusters from matter.testing.device_conformance_tests import DeviceConformanceTests -from matter.testing.spec_parsing import XmlDeviceType, XmlComposedDeviceTypeRequirement, XmlDeviceTypeClusterRequirements, parse_callable_from_xml, ConformanceParseParameters, ClusterSide -from matter.testing.conformance import EMPTY_CLUSTER_GLOBAL_ATTRIBUTES, ConformanceAssessmentData -from matter.testing.problem_notices import ProblemNotice, ProblemSeverity, DeviceTypePathLocation from matter.testing.runner import default_matter_test_main +from matter.testing.spec_parsing import (ClusterSide, ConformanceParseParameters, XmlComposedDeviceTypeRequirement, XmlDeviceType, + XmlDeviceTypeClusterRequirements, parse_callable_from_xml) log = logging.getLogger(__name__) + def get_mandatory_conformance(): return parse_callable_from_xml(ElementTree.Element('mandatoryConform'), ConformanceParseParameters({}, {}, {})) + def get_optional_conformance(): return parse_callable_from_xml(ElementTree.Element('optionalConform'), ConformanceParseParameters({}, {}, {})) + class TestComposedDeviceTypeMatching(DeviceConformanceTests): def setup_class(self): # We don't load real XMLs here to isolate the tests @@ -63,7 +64,7 @@ def _create_mock_composed_req(self, dt_id, name, conformance, min_instances=None def test_scenario_simple_match(self): dt_parent_id = 0x0001 dt_child_id = 0x0002 - + # Mock spec data self.xml_device_types = { dt_parent_id: XmlDeviceType( @@ -75,7 +76,8 @@ def test_scenario_simple_match(self): classification_scope="endpoint", revision_desc={}, composed_device_types=[ - self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance(), min_instances=1, max_instances=1) + self._create_mock_composed_req(dt_child_id, "Child Device", + get_mandatory_conformance(), min_instances=1, max_instances=1) ] ), dt_child_id: XmlDeviceType( @@ -88,7 +90,7 @@ def test_scenario_simple_match(self): revision_desc={} ) } - + # Mock device endpoints self.endpoints = { 0: { @@ -104,11 +106,12 @@ def test_scenario_simple_match(self): }, 2: { Clusters.Descriptor: { - Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)] + Clusters.Descriptor.Attributes.DeviceTypeList: [ + Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)] } } } - + success, problems = self.check_composed_device_type_requirements() for p in problems: log.info(p) @@ -124,7 +127,7 @@ def test_scenario_simple_match(self): def test_scenario_missing_mandatory(self): dt_parent_id = 0x0001 dt_child_id = 0x0002 - + self.xml_device_types = { dt_parent_id: XmlDeviceType( name="Parent Device", @@ -135,7 +138,8 @@ def test_scenario_missing_mandatory(self): classification_scope="endpoint", revision_desc={}, composed_device_types=[ - self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance(), min_instances=1, max_instances=1) + self._create_mock_composed_req(dt_child_id, "Child Device", + get_mandatory_conformance(), min_instances=1, max_instances=1) ] ), dt_child_id: XmlDeviceType( @@ -148,7 +152,7 @@ def test_scenario_missing_mandatory(self): revision_desc={} ) } - + self.endpoints = { 0: { Clusters.BasicInformation: { @@ -162,10 +166,10 @@ def test_scenario_missing_mandatory(self): } } } - + success, problems = self.check_composed_device_type_requirements() asserts.assert_false(success, "Unexpected success when mandatory composed device type is missing") - + # ========================================================================== # Scenario 3: Bipartite Matching (Distinct Overrides) # ========================================================================== @@ -182,20 +186,22 @@ def test_scenario_bipartite_matching(self): dt_child_id = 0x0002 cluster_x_id = 0x0090 cluster_y_id = 0x0091 - + # Mock spec data req_base = self._create_mock_composed_req(dt_child_id, "Child Device Base", get_mandatory_conformance(), min_instances=2) - + req1 = self._create_mock_composed_req(dt_child_id, "Child Device #1", get_mandatory_conformance()) req1.cluster_requirements = { - cluster_x_id: XmlDeviceTypeClusterRequirements(name="Cluster X", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + cluster_x_id: XmlDeviceTypeClusterRequirements( + name="Cluster X", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) } - + req2 = self._create_mock_composed_req(dt_child_id, "Child Device #2", get_mandatory_conformance()) req2.cluster_requirements = { - cluster_y_id: XmlDeviceTypeClusterRequirements(name="Cluster Y", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + cluster_y_id: XmlDeviceTypeClusterRequirements( + name="Cluster Y", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) } - + self.xml_device_types = { dt_parent_id: XmlDeviceType( name="Parent Device", @@ -217,7 +223,7 @@ def test_scenario_bipartite_matching(self): revision_desc={} ) } - + # Mock device endpoints self.endpoints = { 0: { @@ -256,7 +262,7 @@ def test_scenario_bipartite_matching(self): } } } - + success, problems = self.check_composed_device_type_requirements() for p in problems: log.info(p) @@ -278,18 +284,20 @@ def test_scenario_bipartite_conflict(self): dt_child_id = 0x0002 cluster_x_id = 0x0090 cluster_y_id = 0x0091 - + # Mock spec data req1 = self._create_mock_composed_req(dt_child_id, "Child Device #1", get_mandatory_conformance()) req1.cluster_requirements = { - cluster_x_id: XmlDeviceTypeClusterRequirements(name="Cluster X", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + cluster_x_id: XmlDeviceTypeClusterRequirements( + name="Cluster X", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) } - + req2 = self._create_mock_composed_req(dt_child_id, "Child Device #2", get_mandatory_conformance()) req2.cluster_requirements = { - cluster_y_id: XmlDeviceTypeClusterRequirements(name="Cluster Y", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + cluster_y_id: XmlDeviceTypeClusterRequirements( + name="Cluster Y", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) } - + self.xml_device_types = { dt_parent_id: XmlDeviceType( name="Parent Device", @@ -311,7 +319,7 @@ def test_scenario_bipartite_conflict(self): revision_desc={} ) } - + # Mock device endpoints self.endpoints = { 0: { @@ -350,9 +358,10 @@ def test_scenario_bipartite_conflict(self): } } } - + success, problems = self.check_composed_device_type_requirements() asserts.assert_false(success, "Unexpected success in bipartite conflict scenario") + if __name__ == "__main__": default_matter_test_main() diff --git a/src/python_testing/test_testing/TestSpecParsingComposedDeviceTypes.py b/src/python_testing/test_testing/TestSpecParsingComposedDeviceTypes.py index a258f4863ab778..8dfbe80c8b3e32 100644 --- a/src/python_testing/test_testing/TestSpecParsingComposedDeviceTypes.py +++ b/src/python_testing/test_testing/TestSpecParsingComposedDeviceTypes.py @@ -22,8 +22,9 @@ import matter.clusters as Clusters from matter.testing.device_conformance_tests import DeviceConformanceTests -from matter.testing.spec_parsing import parse_single_device_type, XmlCluster, XmlFeature, parse_callable_from_xml, ConformanceParseParameters from matter.testing.runner import default_matter_test_main +from matter.testing.spec_parsing import (ConformanceParseParameters, XmlCluster, XmlFeature, parse_callable_from_xml, + parse_single_device_type) log = logging.getLogger(__name__) @@ -55,6 +56,7 @@ """ + class TestSpecParsingComposedDeviceTypes(DeviceConformanceTests): def setup_class(self): self.xml_device_types = {} @@ -63,14 +65,15 @@ def setup_class(self): # Parse the mock XML root = ElementTree.fromstring(MOCK_DEVICE_TYPE_XML) - + # We need to mock the cluster definition for the parser to resolve features mock_cluster = XmlCluster( name="Cluster X", revision=1, derived=None, - features={0x01: XmlFeature(code="ALTC", name="Alternate Cluster", conformance=parse_callable_from_xml(ElementTree.Element('mandatoryConform'), ConformanceParseParameters({}, {}, {})))}, - feature_map={"ALTC": 0x01}, # Map code to mask + features={0x01: XmlFeature(code="ALTC", name="Alternate Cluster", conformance=parse_callable_from_xml( + ElementTree.Element('mandatoryConform'), ConformanceParseParameters({}, {}, {})))}, + feature_map={"ALTC": 0x01}, # Map code to mask attribute_map={}, command_map={}, attributes={}, @@ -86,7 +89,7 @@ def setup_class(self): revision_desc={} ) mock_clusters = {0x0090: mock_cluster} - + device_types, problems = parse_single_device_type(root, mock_clusters) self.problems.extend(problems) self.xml_device_types.update(device_types) @@ -94,21 +97,21 @@ def setup_class(self): def test_parsing_correctness(self): asserts.assert_equal(len(self.problems), 0, f"Unexpected problems during parsing: {self.problems}") - + dt_parent_id = 0x0001 asserts.assert_in(dt_parent_id, self.xml_device_types, "Parent device type not found in parsed output") - + xml_device = self.xml_device_types[dt_parent_id] asserts.assert_equal(len(xml_device.composed_device_types), 2, "Expected 2 composed device type requirements") - + # Check base requirement req_base = xml_device.composed_device_types[0] asserts.assert_equal(req_base.min_instances, 2, "Expected min_instances = 2 for base requirement") - + # Check override requirement req_override = xml_device.composed_device_types[1] asserts.assert_in(0x0090, req_override.cluster_requirements, "Expected cluster requirement for 0x0090") - + cr = req_override.cluster_requirements[0x0090] asserts.assert_in(0x01, cr.feature_overrides, "Expected feature override for mask 0x01") @@ -116,10 +119,10 @@ def test_enforcement_with_fake_device(self): dt_parent_id = 0x0001 dt_child_id = 0x0002 cluster_x_id = 0x0090 - + # Scenario: We need at least 2 child devices. # One of them must have Cluster X with feature ALTC (mask 0x01). - + # Create mock endpoints self.endpoints = { 0: { @@ -141,21 +144,21 @@ def test_enforcement_with_fake_device(self): } } - + # Let's use the cluster class as key! cluster_class = Clusters.ClustersObjects.ALL_CLUSTERS[cluster_x_id] if hasattr(Clusters, 'ClustersObjects') else None # Wait, let's check how to get cluster class in TestConformanceTest! # It used `Clusters.ClusterObjects.ALL_CLUSTERS[cid]`! - + cluster_class = Clusters.ClusterObjects.ALL_CLUSTERS[cluster_x_id] - + self.endpoints[2][cluster_class] = { - cluster_class.Attributes.FeatureMap: 0x01, # Has ALTC! + cluster_class.Attributes.FeatureMap: 0x01, # Has ALTC! cluster_class.Attributes.AttributeList: [], cluster_class.Attributes.AcceptedCommandList: [], cluster_class.Attributes.ClusterRevision: 1 } - + # EP3: Child device without Cluster X (to satisfy min 2 constraint) self.endpoints[3] = { Clusters.Descriptor: { @@ -163,11 +166,12 @@ def test_enforcement_with_fake_device(self): Clusters.Descriptor.Attributes.ServerList: [] } } - + success, problems = self.check_composed_device_type_requirements() for p in problems: log.info(p) asserts.assert_true(success, "Failure on enforcement with fake device") + if __name__ == "__main__": default_matter_test_main() From 65c9a5f315d26bd2967a96e4ffb5e9448fdaae7c Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Wed, 27 May 2026 13:48:32 +0000 Subject: [PATCH 15/21] Restyled by whitespace --- data_model/1.6/clusters/cluster_ids.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_model/1.6/clusters/cluster_ids.json b/data_model/1.6/clusters/cluster_ids.json index be322816aefd6b..bc13e5dd0e91e5 100644 --- a/data_model/1.6/clusters/cluster_ids.json +++ b/data_model/1.6/clusters/cluster_ids.json @@ -134,4 +134,4 @@ "2050": "TLS Client Management", "2822": "Meter Identification", "2823": "Commodity Metering" -} \ No newline at end of file +} From 6cd2a4336e8f8460d1a40e7a80bcdeed70897bbd Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Wed, 27 May 2026 16:39:25 +0000 Subject: [PATCH 16/21] Fix py findings --- .../matter/testing/device_conformance_tests.py | 11 ++--------- .../matter/testing/spec_parsing.py | 6 +++--- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py index b2bad3fc9205e2..fc8dccfd330cdd 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py @@ -386,13 +386,7 @@ def check_device_type(self, fail_on_extra_clusters: bool = True, allow_provision success = True problems = [] - # This is a specific problem in the 1.5 specification for water heater. For now this requirement is being removed as it is - # disallowed to overwrite a mandatory cluster requirement to disallowed in the device type - try: - water_heater_id = self._get_device_type_id('Water Heater') - except KeyError: - # water heater isn't in the spec, so just set it to an unused ID for checks - water_heater_id = 0 + def record_problem(location, problem, severity): problems.append(ProblemNotice("IDM-10.5", location, severity, problem, "")) @@ -537,8 +531,7 @@ def record_error(location, problem): for req_idx, req in enumerate(req_list): for ep_id in matching_eps: child_ep = self.endpoints[ep_id] - server_list = child_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.ServerList] if Clusters.Descriptor.Attributes.ServerList in child_ep[Clusters.Descriptor] else [ - ] + server_list = child_ep[Clusters.Descriptor].get(Clusters.Descriptor.Attributes.ServerList, []) matches = True for cid, cr in req.cluster_requirements.items(): diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index cd74ef7a23a4e5..c78f8d48269d67 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -1543,13 +1543,13 @@ def append_overrides(override_element_type: str, c: ElementTree.Element, cluster if (constraint := composed_dt.find('constraint')) is not None: if (allowed := constraint.find('allowed')) is not None: - min_instances = max_instances = int(allowed.attrib.get('value', 0), 0) + min_instances = max_instances = int(allowed.attrib.get('value', "0"), 0) else: min_el = constraint.find('min') max_el = constraint.find('max') - min_instances = int(min_el.attrib.get('value', 0), 0) if min_el is not None else None - max_instances = int(max_el.attrib.get('value', 0), 0) if max_el is not None else None + min_instances = int(min_el.attrib.get('value', "0"), 0) if min_el is not None else None + max_instances = int(max_el.attrib.get('value', "0"), 0) if max_el is not None else None cluster_requirements = {} cr_el = composed_dt.find('clusterRequirements') From c9dd96bbef3da572298e16686d984718e9ef8463 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 16:40:57 +0000 Subject: [PATCH 17/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../matter/testing/device_conformance_tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py index fc8dccfd330cdd..131b9b3c1c9687 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py @@ -386,8 +386,6 @@ def check_device_type(self, fail_on_extra_clusters: bool = True, allow_provision success = True problems = [] - - def record_problem(location, problem, severity): problems.append(ProblemNotice("IDM-10.5", location, severity, problem, "")) From cfffde283ac321509986d610286ca6d5b80a9a70 Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Wed, 27 May 2026 20:37:57 +0000 Subject: [PATCH 18/21] Support deviceTypeLocation --- data_model/1.6/device_types/AirPurifier.xml | 8 +- .../1.6/device_types/BatteryStorage.xml | 18 +- data_model/1.6/device_types/BridgedNode.xml | 2 +- data_model/1.6/device_types/Camera.xml | 2 +- data_model/1.6/device_types/Closure.xml | 6 +- data_model/1.6/device_types/Cooktop.xml | 2 +- data_model/1.6/device_types/EVSE.xml | 6 +- .../1.6/device_types/ElectricalMeter.xml | 2 +- data_model/1.6/device_types/ExtractorHood.xml | 2 +- data_model/1.6/device_types/Fan.xml | 2 +- .../1.6/device_types/FloodlightCamera.xml | 4 +- data_model/1.6/device_types/HeatPump.xml | 12 +- data_model/1.6/device_types/Intercom.xml | 2 +- .../1.6/device_types/IrrigationSystem.xml | 2 +- .../1.6/device_types/MeterReferencePoint.xml | 4 +- data_model/1.6/device_types/MicrowaveOven.xml | 2 +- data_model/1.6/device_types/Oven.xml | 4 +- data_model/1.6/device_types/Refrigerator.xml | 2 +- .../1.6/device_types/RoomAirConditioner.xml | 4 +- .../1.6/device_types/RootNodeDeviceType.xml | 2 +- data_model/1.6/device_types/SmokeCOAlarm.xml | 2 +- .../1.6/device_types/SnapshotCamera.xml | 2 +- data_model/1.6/device_types/SolarPower.xml | 8 +- .../1.6/device_types/ThreadBorderRouter.xml | 2 +- data_model/1.6/device_types/VideoDoorbell.xml | 4 +- data_model/1.6/device_types/WaterHeater.xml | 8 +- .../testing/device_conformance_tests.py | 42 +- .../matter/testing/spec_parsing.py | 3 + .../TestComposedDeviceTypeMatching.py | 477 +++++++++++++++++- 29 files changed, 567 insertions(+), 69 deletions(-) diff --git a/data_model/1.6/device_types/AirPurifier.xml b/data_model/1.6/device_types/AirPurifier.xml index b2f9b7380fcb66..a31e5388d8c797 100644 --- a/data_model/1.6/device_types/AirPurifier.xml +++ b/data_model/1.6/device_types/AirPurifier.xml @@ -84,16 +84,16 @@ Davis, CA 95616, USA - + - + - + - + diff --git a/data_model/1.6/device_types/BatteryStorage.xml b/data_model/1.6/device_types/BatteryStorage.xml index 20aa1a0c812ee7..31a41ab618182e 100644 --- a/data_model/1.6/device_types/BatteryStorage.xml +++ b/data_model/1.6/device_types/BatteryStorage.xml @@ -69,7 +69,7 @@ Davis, CA 95616, USA - + @@ -81,13 +81,13 @@ Davis, CA 95616, USA - + - + @@ -123,7 +123,7 @@ Davis, CA 95616, USA - + @@ -159,13 +159,13 @@ Davis, CA 95616, USA - + - + @@ -184,7 +184,7 @@ Davis, CA 95616, USA - + @@ -229,10 +229,10 @@ Davis, CA 95616, USA - + - + diff --git a/data_model/1.6/device_types/BridgedNode.xml b/data_model/1.6/device_types/BridgedNode.xml index 37b8ca211bb1e5..2fe80eb464bb4c 100644 --- a/data_model/1.6/device_types/BridgedNode.xml +++ b/data_model/1.6/device_types/BridgedNode.xml @@ -99,7 +99,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/Camera.xml b/data_model/1.6/device_types/Camera.xml index 422e061329b18a..3f3f49343dfe5e 100644 --- a/data_model/1.6/device_types/Camera.xml +++ b/data_model/1.6/device_types/Camera.xml @@ -133,7 +133,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/Closure.xml b/data_model/1.6/device_types/Closure.xml index 71febda55e2521..3a0729f073fb19 100644 --- a/data_model/1.6/device_types/Closure.xml +++ b/data_model/1.6/device_types/Closure.xml @@ -77,13 +77,13 @@ Davis, CA 95616, USA - + - + - + diff --git a/data_model/1.6/device_types/Cooktop.xml b/data_model/1.6/device_types/Cooktop.xml index 9e02a055973f11..38ae89b5970463 100644 --- a/data_model/1.6/device_types/Cooktop.xml +++ b/data_model/1.6/device_types/Cooktop.xml @@ -76,7 +76,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/EVSE.xml b/data_model/1.6/device_types/EVSE.xml index f4eb8a43e5e44c..c49c8992fdd008 100644 --- a/data_model/1.6/device_types/EVSE.xml +++ b/data_model/1.6/device_types/EVSE.xml @@ -78,7 +78,7 @@ Davis, CA 95616, USA - + @@ -96,7 +96,7 @@ Davis, CA 95616, USA - + @@ -110,7 +110,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/ElectricalMeter.xml b/data_model/1.6/device_types/ElectricalMeter.xml index a0220e021c0bb9..b07524013efdd6 100644 --- a/data_model/1.6/device_types/ElectricalMeter.xml +++ b/data_model/1.6/device_types/ElectricalMeter.xml @@ -77,7 +77,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/ExtractorHood.xml b/data_model/1.6/device_types/ExtractorHood.xml index 6f694a115b75c8..d8c6906ec30f5f 100644 --- a/data_model/1.6/device_types/ExtractorHood.xml +++ b/data_model/1.6/device_types/ExtractorHood.xml @@ -88,7 +88,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/Fan.xml b/data_model/1.6/device_types/Fan.xml index 255a96552f12ce..b5ee7a1fe296a2 100644 --- a/data_model/1.6/device_types/Fan.xml +++ b/data_model/1.6/device_types/Fan.xml @@ -80,7 +80,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/FloodlightCamera.xml b/data_model/1.6/device_types/FloodlightCamera.xml index 5f667f75f9ef36..91e1226f017724 100644 --- a/data_model/1.6/device_types/FloodlightCamera.xml +++ b/data_model/1.6/device_types/FloodlightCamera.xml @@ -63,13 +63,13 @@ Davis, CA 95616, USA - + - + diff --git a/data_model/1.6/device_types/HeatPump.xml b/data_model/1.6/device_types/HeatPump.xml index 06cddca8621a6a..901c80b6abeeb8 100644 --- a/data_model/1.6/device_types/HeatPump.xml +++ b/data_model/1.6/device_types/HeatPump.xml @@ -71,7 +71,7 @@ Davis, CA 95616, USA - + @@ -83,7 +83,7 @@ Davis, CA 95616, USA - + @@ -110,7 +110,7 @@ Davis, CA 95616, USA - + @@ -129,7 +129,7 @@ Davis, CA 95616, USA - + @@ -141,7 +141,7 @@ Davis, CA 95616, USA - + @@ -156,7 +156,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/Intercom.xml b/data_model/1.6/device_types/Intercom.xml index 7f7071b2ac4e7c..89bda06b09814c 100644 --- a/data_model/1.6/device_types/Intercom.xml +++ b/data_model/1.6/device_types/Intercom.xml @@ -120,7 +120,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/IrrigationSystem.xml b/data_model/1.6/device_types/IrrigationSystem.xml index efe3ba0e2bd951..06a73b5d396196 100644 --- a/data_model/1.6/device_types/IrrigationSystem.xml +++ b/data_model/1.6/device_types/IrrigationSystem.xml @@ -77,7 +77,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/MeterReferencePoint.xml b/data_model/1.6/device_types/MeterReferencePoint.xml index 33dfb8d23bf0e9..dc97eaaadcbd7f 100644 --- a/data_model/1.6/device_types/MeterReferencePoint.xml +++ b/data_model/1.6/device_types/MeterReferencePoint.xml @@ -78,7 +78,7 @@ Davis, CA 95616, USA - + @@ -100,7 +100,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/MicrowaveOven.xml b/data_model/1.6/device_types/MicrowaveOven.xml index 15700f5366b4c6..33deabb38f3d15 100644 --- a/data_model/1.6/device_types/MicrowaveOven.xml +++ b/data_model/1.6/device_types/MicrowaveOven.xml @@ -99,7 +99,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/Oven.xml b/data_model/1.6/device_types/Oven.xml index bdfbcdccd10068..40a5d2f449b939 100644 --- a/data_model/1.6/device_types/Oven.xml +++ b/data_model/1.6/device_types/Oven.xml @@ -77,13 +77,13 @@ Davis, CA 95616, USA - + - + diff --git a/data_model/1.6/device_types/Refrigerator.xml b/data_model/1.6/device_types/Refrigerator.xml index 0ada9c55dfab2e..c968ed41fde007 100644 --- a/data_model/1.6/device_types/Refrigerator.xml +++ b/data_model/1.6/device_types/Refrigerator.xml @@ -101,7 +101,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/RoomAirConditioner.xml b/data_model/1.6/device_types/RoomAirConditioner.xml index a6689b778e0ac2..1722ecde3b8a77 100644 --- a/data_model/1.6/device_types/RoomAirConditioner.xml +++ b/data_model/1.6/device_types/RoomAirConditioner.xml @@ -110,10 +110,10 @@ Davis, CA 95616, USA - + - + diff --git a/data_model/1.6/device_types/RootNodeDeviceType.xml b/data_model/1.6/device_types/RootNodeDeviceType.xml index d65bfa44e2eabf..1731fcbbd74066 100644 --- a/data_model/1.6/device_types/RootNodeDeviceType.xml +++ b/data_model/1.6/device_types/RootNodeDeviceType.xml @@ -320,7 +320,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/SmokeCOAlarm.xml b/data_model/1.6/device_types/SmokeCOAlarm.xml index 44e69d00760c5d..4c4af4557cedbe 100644 --- a/data_model/1.6/device_types/SmokeCOAlarm.xml +++ b/data_model/1.6/device_types/SmokeCOAlarm.xml @@ -83,7 +83,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/SnapshotCamera.xml b/data_model/1.6/device_types/SnapshotCamera.xml index e84bcd873816ff..a5d2ebd4003f33 100644 --- a/data_model/1.6/device_types/SnapshotCamera.xml +++ b/data_model/1.6/device_types/SnapshotCamera.xml @@ -106,7 +106,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/SolarPower.xml b/data_model/1.6/device_types/SolarPower.xml index 7efb30fe926804..c03cb84a98f309 100644 --- a/data_model/1.6/device_types/SolarPower.xml +++ b/data_model/1.6/device_types/SolarPower.xml @@ -68,7 +68,7 @@ Davis, CA 95616, USA - + @@ -80,7 +80,7 @@ Davis, CA 95616, USA - + @@ -110,7 +110,7 @@ Davis, CA 95616, USA - + @@ -132,7 +132,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/ThreadBorderRouter.xml b/data_model/1.6/device_types/ThreadBorderRouter.xml index 590f8be2befebc..9d14fc58a82f68 100644 --- a/data_model/1.6/device_types/ThreadBorderRouter.xml +++ b/data_model/1.6/device_types/ThreadBorderRouter.xml @@ -75,7 +75,7 @@ Davis, CA 95616, USA - + diff --git a/data_model/1.6/device_types/VideoDoorbell.xml b/data_model/1.6/device_types/VideoDoorbell.xml index 6660f387bdcd30..0073611aa47fff 100644 --- a/data_model/1.6/device_types/VideoDoorbell.xml +++ b/data_model/1.6/device_types/VideoDoorbell.xml @@ -63,13 +63,13 @@ Davis, CA 95616, USA - + - + diff --git a/data_model/1.6/device_types/WaterHeater.xml b/data_model/1.6/device_types/WaterHeater.xml index 01df30739a2391..6646b0c04c823b 100644 --- a/data_model/1.6/device_types/WaterHeater.xml +++ b/data_model/1.6/device_types/WaterHeater.xml @@ -82,7 +82,7 @@ Davis, CA 95616, USA - + @@ -94,7 +94,7 @@ Davis, CA 95616, USA - + @@ -105,10 +105,10 @@ Davis, CA 95616, USA - + - + diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py index 131b9b3c1c9687..9150838f27bcf5 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py @@ -474,6 +474,26 @@ def record_warning(location, problem): return success, problems + def _get_candidate_endpoints(self, endpoint_id: int, location: str, parts_list: list[int]) -> list[int]: + if location == 'deviceEndpoint': + return [endpoint_id] + elif location == 'rootEndpoint': + return [0] + elif location == 'anywhere': + return list(self.endpoints.keys()) + elif location == 'descendantEndpoint': + descendants = [] + def add_descendants(ep): + if ep in self.endpoints and Clusters.Descriptor in self.endpoints[ep]: + pl = self.endpoints[ep][Clusters.Descriptor].get(Clusters.Descriptor.Attributes.PartsList, []) + for child in pl: + descendants.append(child) + add_descendants(child) + add_descendants(endpoint_id) + return descendants + else: # childEndpoint or fallback + return parts_list + def check_composed_device_type_requirements(self, allow_provisional: bool = False) -> tuple[bool, list[ProblemNotice]]: success = True problems = [] @@ -514,19 +534,19 @@ def record_error(location, problem): parts_list = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] for req_dt_id, req_list in reqs_by_dt.items(): - # Find all child endpoints that have this device type - matching_eps = [] - for child_ep_id in parts_list: - if child_ep_id in self.endpoints: - child_ep = self.endpoints[child_ep_id] - if Clusters.Descriptor in child_ep: - child_dt_list = child_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] - if any(child_dt.deviceType == req_dt_id for child_dt in child_dt_list): - matching_eps.append(child_ep_id) - - # For each requirement, check which endpoints satisfy it req_matches = defaultdict(list) for req_idx, req in enumerate(req_list): + candidate_eps = self._get_candidate_endpoints(endpoint_id, req.device_type_location, parts_list) + + matching_eps = [] + for candidate_ep_id in candidate_eps: + if candidate_ep_id in self.endpoints: + candidate_ep = self.endpoints[candidate_ep_id] + if Clusters.Descriptor in candidate_ep: + dt_list = candidate_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] + if any(dt.deviceType == req_dt_id for dt in dt_list): + matching_eps.append(candidate_ep_id) + for ep_id in matching_eps: child_ep = self.endpoints[ep_id] server_list = child_ep[Clusters.Descriptor].get(Clusters.Descriptor.Attributes.ServerList, []) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index c78f8d48269d67..1d3415d61511c6 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -253,6 +253,7 @@ class XmlComposedDeviceTypeRequirement: conformance: ConformanceCallable min_instances: Optional[int] = None max_instances: Optional[int] = None + device_type_location: str = 'childEndpoint' cluster_requirements: dict[uint, XmlDeviceTypeClusterRequirements] = field(default_factory=dict) @@ -1572,12 +1573,14 @@ def append_overrides(override_element_type: str, c: ElementTree.Element, cluster append_overrides('attribute', c, cluster, c_id, cluster_conformance_params, location, problems) append_overrides('command', c, cluster, c_id, cluster_conformance_params, location, problems) + device_type_location = composed_dt.attrib.get('deviceTypeLocation', 'childEndpoint') device_types[tid].composed_device_types.append(XmlComposedDeviceTypeRequirement( device_type_id=composed_id, device_type_name=composed_name, conformance=conformance, min_instances=min_instances, max_instances=max_instances, + device_type_location=device_type_location, cluster_requirements=cluster_requirements )) except Exception as e: diff --git a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py index 6db829fd27af03..9110e0c0b255b4 100644 --- a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py +++ b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py @@ -44,13 +44,14 @@ def setup_class(self): self.xml_clusters = {} self.problems = [] - def _create_mock_composed_req(self, dt_id, name, conformance, min_instances=None, max_instances=None, cluster_requirements=None): + def _create_mock_composed_req(self, dt_id, name, conformance, min_instances=None, max_instances=None, cluster_requirements=None, device_type_location='childEndpoint'): return XmlComposedDeviceTypeRequirement( device_type_id=dt_id, device_type_name=name, conformance=conformance, min_instances=min_instances, max_instances=max_instances, + device_type_location=device_type_location, cluster_requirements=cluster_requirements or {} ) @@ -268,6 +269,106 @@ def test_scenario_bipartite_matching(self): log.info(p) asserts.assert_true(success, "Failure on bipartite matching scenario") + # ========================================================================== + # Scenario 3b: Bipartite Matching (Swapped Requirements) + # ========================================================================== + # Requirement: Parent requires TWO instances of Child DT. + # Instance #1 requires Cluster Y to be present. + # Instance #2 requires Cluster X to be present. + # (Swapped order in composed_device_types list compared to Scenario 3) + # Topology: Endpoint 1 has Parent DT. PartsList = [2, 3]. + # Endpoint 2 has Child DT and Cluster X. + # Endpoint 3 has Child DT and Cluster Y. + # Expected: PASS (The test should find a valid 1-to-1 assignment regardless of order). + # ========================================================================== + def test_scenario_bipartite_matching_swapped(self): + log.info("Running Scenario 3b: Bipartite Matching (Swapped Requirements)") + dt_parent_id = 0x0001 + dt_child_id = 0x0002 + cluster_x_id = 0x0090 + cluster_y_id = 0x0091 + + # Mock spec data + req_base = self._create_mock_composed_req(dt_child_id, "Child Device Base", get_mandatory_conformance(), min_instances=2) + + req1 = self._create_mock_composed_req(dt_child_id, "Child Device #1", get_mandatory_conformance()) + req1.cluster_requirements = { + cluster_x_id: XmlDeviceTypeClusterRequirements( + name="Cluster X", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + } + + req2 = self._create_mock_composed_req(dt_child_id, "Child Device #2", get_mandatory_conformance()) + req2.cluster_requirements = { + cluster_y_id: XmlDeviceTypeClusterRequirements( + name="Cluster Y", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + } + + self.xml_device_types = { + dt_parent_id: XmlDeviceType( + name="Parent Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={}, + composed_device_types=[req_base, req2, req1] # Swapped! + ), + dt_child_id: XmlDeviceType( + name="Child Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={} + ) + } + + # Mock device endpoints + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [2, 3] + } + }, + 2: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_x_id] + }, + Clusters.ElectricalPowerMeasurement: { + Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x01, + Clusters.ElectricalPowerMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalPowerMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 + } + }, + 3: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_y_id] + }, + Clusters.ElectricalEnergyMeasurement: { + Clusters.ElectricalEnergyMeasurement.Attributes.FeatureMap: 0x02, + Clusters.ElectricalEnergyMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalEnergyMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalEnergyMeasurement.Attributes.ClusterRevision: 1 + } + } + } + + success, problems = self.check_composed_device_type_requirements() + for p in problems: + log.info(p) + asserts.assert_true(success, "Failure on bipartite matching swapped scenario") + # ========================================================================== # Scenario 4: Bipartite Matching Conflict # ========================================================================== @@ -363,5 +464,379 @@ def test_scenario_bipartite_conflict(self): asserts.assert_false(success, "Unexpected success in bipartite conflict scenario") + # ========================================================================== + # Scenario 5: Anywhere Location Match + # ========================================================================== + def test_scenario_anywhere_location(self): + log.info("Running Scenario 5: Anywhere Location Match") + + dt_parent_id = 0x0001 + dt_child_id = 0x0002 + dt_power_source_id = 0x0011 + + # Mock spec + self.xml_device_types = { + dt_parent_id: XmlDeviceType( + name="Parent Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="Simple", + classification_scope="Endpoint", + revision_desc={}, + composed_device_types=[ + self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance()), + self._create_mock_composed_req(dt_power_source_id, "Power Source", get_mandatory_conformance(), device_type_location='anywhere') + ] + ), + dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, classification_class="Simple", classification_scope="Endpoint", revision_desc={}), + dt_power_source_id: XmlDeviceType(name="Power Source", revision=1, server_clusters={}, client_clusters={}, classification_class="Utility", classification_scope="Endpoint", revision_desc={}) + } + + # Mock endpoints + # EP1: Parent Device + # EP2: Child Device (direct child of EP1) + # EP3: Power Source (NOT in EP1 parts list!) + + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [2] # Only EP2 is a child! + } + }, + 2: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [] + } + }, + 3: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_power_source_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [] + } + } + } + + success, problems = self.check_composed_device_type_requirements() + for p in problems: + log.info(p) + asserts.assert_true(success, "Failure in anywhere location scenario") + + + # ========================================================================== + # Scenario 6: Self Location Match + # ========================================================================== + def test_scenario_self_location(self): + log.info("Running Scenario 6: Self Location Match") + + dt_parent_id = 0x0001 + dt_power_source_id = 0x0011 + + # Mock spec + self.xml_device_types = { + dt_parent_id: XmlDeviceType( + name="Parent Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="Simple", + classification_scope="Endpoint", + revision_desc={}, + composed_device_types=[ + self._create_mock_composed_req(dt_power_source_id, "Power Source", get_mandatory_conformance(), device_type_location='deviceEndpoint') + ] + ), + dt_power_source_id: XmlDeviceType(name="Power Source", revision=1, server_clusters={}, client_clusters={}, classification_class="Utility", classification_scope="Endpoint", revision_desc={}) + } + + # Mock endpoints + # EP1: Parent Device AND Power Source! + + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [ + Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1), + Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_power_source_id, revision=1) + ], + Clusters.Descriptor.Attributes.PartsList: [] + } + } + } + + success, problems = self.check_composed_device_type_requirements() + for p in problems: + log.info(p) + asserts.assert_true(success, "Failure in self location scenario (positive case)") + + # Now make it fail by removing Power Source from EP1 and adding to EP2 (child) + self.endpoints[1][Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] = [ + Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1) + ] + self.endpoints[1][Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] = [2] + + self.endpoints[2] = { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_power_source_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [] + } + } + + success, problems = self.check_composed_device_type_requirements() + asserts.assert_false(success, "Unexpected success in self location scenario (negative case)") + + + # ========================================================================== + # Scenario 7: Scattered Matching (Cyclic) + # ========================================================================== + # Requirement: Parent requires THREE instances of Child DT. + # Instance #1 requires Cluster X. + # Instance #2 requires Cluster Y. + # Instance #3 requires Cluster Z. + # Topology: Endpoint 1 has Parent DT. PartsList = [2, 3, 4, 5, 6]. + # Endpoint 2 has Child DT and Clusters X, Y. + # Endpoint 3 has Child DT and Clusters Y, Z. + # Endpoint 4 has Child DT and Clusters Z, X. + # Endpoint 5 has Child DT (Base). + # Endpoint 6 has Child DT (Base). + # Expected: PASS (The test should find a valid assignment resolving overlapping capabilities). + # ========================================================================== + def test_scenario_scattered_matching(self): + log.info("Running Scenario 7: Scattered Matching (Cyclic)") + + dt_parent_id = 0x0001 + dt_child_id = 0x0002 + cluster_x_id = 0x0090 + cluster_y_id = 0x0091 + cluster_z_id = 0x0201 # Thermostat + + # Mock spec data + req_base = self._create_mock_composed_req(dt_child_id, "Child Device Base", get_mandatory_conformance(), min_instances=3) + + req1 = self._create_mock_composed_req(dt_child_id, "Child Device #1", get_mandatory_conformance()) + req1.cluster_requirements = { + cluster_x_id: XmlDeviceTypeClusterRequirements(name="Cluster X", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + } + + req2 = self._create_mock_composed_req(dt_child_id, "Child Device #2", get_mandatory_conformance()) + req2.cluster_requirements = { + cluster_y_id: XmlDeviceTypeClusterRequirements(name="Cluster Y", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + } + + req3 = self._create_mock_composed_req(dt_child_id, "Child Device #3", get_mandatory_conformance()) + req3.cluster_requirements = { + cluster_z_id: XmlDeviceTypeClusterRequirements(name="Cluster Z", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + } + + self.xml_device_types = { + dt_parent_id: XmlDeviceType( + name="Parent Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={}, + composed_device_types=[req_base, req1, req2, req3] + ), + dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, classification_class="simple", classification_scope="endpoint", revision_desc={}) + } + + # Mock device endpoints + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [2, 3, 4, 5, 6] + } + }, + 2: { # Has X and Y + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_x_id, cluster_y_id] + }, + Clusters.ElectricalPowerMeasurement: { + Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x01, + Clusters.ElectricalPowerMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalPowerMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 + }, + Clusters.ElectricalEnergyMeasurement: { + Clusters.ElectricalEnergyMeasurement.Attributes.FeatureMap: 0x02, + Clusters.ElectricalEnergyMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalEnergyMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalEnergyMeasurement.Attributes.ClusterRevision: 1 + } + }, + 3: { # Has Y and Z + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_y_id, cluster_z_id] + }, + Clusters.ElectricalEnergyMeasurement: { + Clusters.ElectricalEnergyMeasurement.Attributes.FeatureMap: 0x02, + Clusters.ElectricalEnergyMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalEnergyMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalEnergyMeasurement.Attributes.ClusterRevision: 1 + }, + Clusters.Thermostat: { + Clusters.Thermostat.Attributes.FeatureMap: 0x03, + Clusters.Thermostat.Attributes.AttributeList: [], + Clusters.Thermostat.Attributes.AcceptedCommandList: [], + Clusters.Thermostat.Attributes.ClusterRevision: 1 + } + }, + 4: { # Has Z and X + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_z_id, cluster_x_id] + }, + Clusters.Thermostat: { + Clusters.Thermostat.Attributes.FeatureMap: 0x03, + Clusters.Thermostat.Attributes.AttributeList: [], + Clusters.Thermostat.Attributes.AcceptedCommandList: [], + Clusters.Thermostat.Attributes.ClusterRevision: 1 + }, + Clusters.ElectricalPowerMeasurement: { + Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x01, + Clusters.ElectricalPowerMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalPowerMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 + } + }, + 5: { # Base + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [] + } + }, + 6: { # Base + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [] + } + } + } + + success, problems = self.check_composed_device_type_requirements() + for p in problems: + log.info(p) + asserts.assert_true(success, "Failure on scattered matching scenario") + + + # ========================================================================== + # Scenario 8: Complex Location Matching + # ========================================================================== + # Requirement: Parent requires: + # - 1 instance of Child DT on childEndpoint (direct child). + # - 1 instance of Power Source on anywhere (any endpoint). + # - 1 instance of Thermostat on deviceEndpoint (same endpoint). + # Topology (Positive): + # Endpoint 1 has Parent DT and Thermostat. PartsList = [2]. + # Endpoint 2 has Child DT. + # Endpoint 3 has Power Source (Standalone). + # Expected: PASS + # Topology (Negative): + # Move Thermostat to Endpoint 2 (Child). + # Expected: FAIL (Thermostat is not on Self). + # ========================================================================== + def test_scenario_complex_location_matching(self): + log.info("Running Scenario 8: Complex Location Matching") + + dt_parent_id = 0x0001 + dt_child_id = 0x0002 + dt_power_source_id = 0x0011 + dt_thermostat_id = 0x0301 + + # Mock spec + self.xml_device_types = { + dt_parent_id: XmlDeviceType( + name="Parent Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={}, + composed_device_types=[ + self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance()), + self._create_mock_composed_req(dt_power_source_id, "Power Source", get_mandatory_conformance(), device_type_location='anywhere'), + self._create_mock_composed_req(dt_thermostat_id, "Thermostat", get_mandatory_conformance(), device_type_location='deviceEndpoint') + ] + ), + dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, classification_class="simple", classification_scope="endpoint", revision_desc={}), + dt_power_source_id: XmlDeviceType(name="Power Source", revision=1, server_clusters={}, client_clusters={}, classification_class="Utility", classification_scope="endpoint", revision_desc={}), + dt_thermostat_id: XmlDeviceType(name="Thermostat", revision=1, server_clusters={}, client_clusters={}, classification_class="Simple", classification_scope="endpoint", revision_desc={}) + } + + # Mock endpoints + # EP1: Parent Device AND Thermostat! + # EP2: Child Device (direct child of EP1) + # EP3: Power Source (Standalone) + + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [ + Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1), + Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_thermostat_id, revision=1) + ], + Clusters.Descriptor.Attributes.PartsList: [2] + } + }, + 2: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [] + } + }, + 3: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_power_source_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [] + } + } + } + + success, problems = self.check_composed_device_type_requirements() + for p in problems: + log.info(p) + asserts.assert_true(success, "Failure in complex location scenario (positive case)") + + # Break it by moving Thermostat to EP2 + self.endpoints[1][Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] = [ + Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1) + ] + self.endpoints[2][Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] = [ + Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1), + Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_thermostat_id, revision=1) + ] + + success, problems = self.check_composed_device_type_requirements() + asserts.assert_false(success, "Unexpected success in complex location scenario (negative case)") + + if __name__ == "__main__": default_matter_test_main() From 18e534d8b66174546b2bd0453473fcfbc1c4d8c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 20:39:30 +0000 Subject: [PATCH 19/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../testing/device_conformance_tests.py | 15 +-- .../TestComposedDeviceTypeMatching.py | 109 ++++++++++-------- 2 files changed, 68 insertions(+), 56 deletions(-) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py index 9150838f27bcf5..135686fd48ff4b 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py @@ -477,12 +477,13 @@ def record_warning(location, problem): def _get_candidate_endpoints(self, endpoint_id: int, location: str, parts_list: list[int]) -> list[int]: if location == 'deviceEndpoint': return [endpoint_id] - elif location == 'rootEndpoint': + if location == 'rootEndpoint': return [0] - elif location == 'anywhere': + if location == 'anywhere': return list(self.endpoints.keys()) - elif location == 'descendantEndpoint': + if location == 'descendantEndpoint': descendants = [] + def add_descendants(ep): if ep in self.endpoints and Clusters.Descriptor in self.endpoints[ep]: pl = self.endpoints[ep][Clusters.Descriptor].get(Clusters.Descriptor.Attributes.PartsList, []) @@ -491,8 +492,8 @@ def add_descendants(ep): add_descendants(child) add_descendants(endpoint_id) return descendants - else: # childEndpoint or fallback - return parts_list + # childEndpoint or fallback + return parts_list def check_composed_device_type_requirements(self, allow_provisional: bool = False) -> tuple[bool, list[ProblemNotice]]: success = True @@ -537,7 +538,7 @@ def record_error(location, problem): req_matches = defaultdict(list) for req_idx, req in enumerate(req_list): candidate_eps = self._get_candidate_endpoints(endpoint_id, req.device_type_location, parts_list) - + matching_eps = [] for candidate_ep_id in candidate_eps: if candidate_ep_id in self.endpoints: @@ -546,7 +547,7 @@ def record_error(location, problem): dt_list = candidate_ep[Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] if any(dt.deviceType == req_dt_id for dt in dt_list): matching_eps.append(candidate_ep_id) - + for ep_id in matching_eps: child_ep = self.endpoints[ep_id] server_list = child_ep[Clusters.Descriptor].get(Clusters.Descriptor.Attributes.ServerList, []) diff --git a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py index 9110e0c0b255b4..f5c81fe8ac71b6 100644 --- a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py +++ b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py @@ -463,17 +463,17 @@ def test_scenario_bipartite_conflict(self): success, problems = self.check_composed_device_type_requirements() asserts.assert_false(success, "Unexpected success in bipartite conflict scenario") - # ========================================================================== # Scenario 5: Anywhere Location Match # ========================================================================== + def test_scenario_anywhere_location(self): log.info("Running Scenario 5: Anywhere Location Match") - + dt_parent_id = 0x0001 dt_child_id = 0x0002 dt_power_source_id = 0x0011 - + # Mock spec self.xml_device_types = { dt_parent_id: XmlDeviceType( @@ -486,18 +486,20 @@ def test_scenario_anywhere_location(self): revision_desc={}, composed_device_types=[ self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance()), - self._create_mock_composed_req(dt_power_source_id, "Power Source", get_mandatory_conformance(), device_type_location='anywhere') + self._create_mock_composed_req(dt_power_source_id, "Power Source", + get_mandatory_conformance(), device_type_location='anywhere') ] ), dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, classification_class="Simple", classification_scope="Endpoint", revision_desc={}), - dt_power_source_id: XmlDeviceType(name="Power Source", revision=1, server_clusters={}, client_clusters={}, classification_class="Utility", classification_scope="Endpoint", revision_desc={}) + dt_power_source_id: XmlDeviceType(name="Power Source", revision=1, server_clusters={}, client_clusters={ + }, classification_class="Utility", classification_scope="Endpoint", revision_desc={}) } - + # Mock endpoints # EP1: Parent Device # EP2: Child Device (direct child of EP1) # EP3: Power Source (NOT in EP1 parts list!) - + self.endpoints = { 0: { Clusters.BasicInformation: { @@ -507,7 +509,7 @@ def test_scenario_anywhere_location(self): 1: { Clusters.Descriptor: { Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1)], - Clusters.Descriptor.Attributes.PartsList: [2] # Only EP2 is a child! + Clusters.Descriptor.Attributes.PartsList: [2] # Only EP2 is a child! } }, 2: { @@ -523,22 +525,22 @@ def test_scenario_anywhere_location(self): } } } - + success, problems = self.check_composed_device_type_requirements() for p in problems: log.info(p) asserts.assert_true(success, "Failure in anywhere location scenario") - # ========================================================================== # Scenario 6: Self Location Match # ========================================================================== + def test_scenario_self_location(self): log.info("Running Scenario 6: Self Location Match") - + dt_parent_id = 0x0001 dt_power_source_id = 0x0011 - + # Mock spec self.xml_device_types = { dt_parent_id: XmlDeviceType( @@ -550,15 +552,17 @@ def test_scenario_self_location(self): classification_scope="Endpoint", revision_desc={}, composed_device_types=[ - self._create_mock_composed_req(dt_power_source_id, "Power Source", get_mandatory_conformance(), device_type_location='deviceEndpoint') + self._create_mock_composed_req(dt_power_source_id, "Power Source", + get_mandatory_conformance(), device_type_location='deviceEndpoint') ] ), - dt_power_source_id: XmlDeviceType(name="Power Source", revision=1, server_clusters={}, client_clusters={}, classification_class="Utility", classification_scope="Endpoint", revision_desc={}) + dt_power_source_id: XmlDeviceType(name="Power Source", revision=1, server_clusters={}, client_clusters={ + }, classification_class="Utility", classification_scope="Endpoint", revision_desc={}) } - + # Mock endpoints # EP1: Parent Device AND Power Source! - + self.endpoints = { 0: { Clusters.BasicInformation: { @@ -575,29 +579,28 @@ def test_scenario_self_location(self): } } } - + success, problems = self.check_composed_device_type_requirements() for p in problems: log.info(p) asserts.assert_true(success, "Failure in self location scenario (positive case)") - + # Now make it fail by removing Power Source from EP1 and adding to EP2 (child) self.endpoints[1][Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] = [ Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1) ] self.endpoints[1][Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] = [2] - + self.endpoints[2] = { Clusters.Descriptor: { Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_power_source_id, revision=1)], Clusters.Descriptor.Attributes.PartsList: [] } } - + success, problems = self.check_composed_device_type_requirements() asserts.assert_false(success, "Unexpected success in self location scenario (negative case)") - # ========================================================================== # Scenario 7: Scattered Matching (Cyclic) # ========================================================================== @@ -613,33 +616,37 @@ def test_scenario_self_location(self): # Endpoint 6 has Child DT (Base). # Expected: PASS (The test should find a valid assignment resolving overlapping capabilities). # ========================================================================== + def test_scenario_scattered_matching(self): log.info("Running Scenario 7: Scattered Matching (Cyclic)") - + dt_parent_id = 0x0001 dt_child_id = 0x0002 cluster_x_id = 0x0090 cluster_y_id = 0x0091 cluster_z_id = 0x0201 # Thermostat - + # Mock spec data req_base = self._create_mock_composed_req(dt_child_id, "Child Device Base", get_mandatory_conformance(), min_instances=3) - + req1 = self._create_mock_composed_req(dt_child_id, "Child Device #1", get_mandatory_conformance()) req1.cluster_requirements = { - cluster_x_id: XmlDeviceTypeClusterRequirements(name="Cluster X", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + cluster_x_id: XmlDeviceTypeClusterRequirements( + name="Cluster X", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) } - + req2 = self._create_mock_composed_req(dt_child_id, "Child Device #2", get_mandatory_conformance()) req2.cluster_requirements = { - cluster_y_id: XmlDeviceTypeClusterRequirements(name="Cluster Y", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + cluster_y_id: XmlDeviceTypeClusterRequirements( + name="Cluster Y", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) } - + req3 = self._create_mock_composed_req(dt_child_id, "Child Device #3", get_mandatory_conformance()) req3.cluster_requirements = { - cluster_z_id: XmlDeviceTypeClusterRequirements(name="Cluster Z", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) + cluster_z_id: XmlDeviceTypeClusterRequirements( + name="Cluster Z", side=ClusterSide.SERVER, conformance=get_mandatory_conformance()) } - + self.xml_device_types = { dt_parent_id: XmlDeviceType( name="Parent Device", @@ -651,9 +658,10 @@ def test_scenario_scattered_matching(self): revision_desc={}, composed_device_types=[req_base, req1, req2, req3] ), - dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, classification_class="simple", classification_scope="endpoint", revision_desc={}) + dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, + classification_class="simple", classification_scope="endpoint", revision_desc={}) } - + # Mock device endpoints self.endpoints = { 0: { @@ -667,7 +675,7 @@ def test_scenario_scattered_matching(self): Clusters.Descriptor.Attributes.PartsList: [2, 3, 4, 5, 6] } }, - 2: { # Has X and Y + 2: { # Has X and Y Clusters.Descriptor: { Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], Clusters.Descriptor.Attributes.ServerList: [cluster_x_id, cluster_y_id] @@ -685,7 +693,7 @@ def test_scenario_scattered_matching(self): Clusters.ElectricalEnergyMeasurement.Attributes.ClusterRevision: 1 } }, - 3: { # Has Y and Z + 3: { # Has Y and Z Clusters.Descriptor: { Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], Clusters.Descriptor.Attributes.ServerList: [cluster_y_id, cluster_z_id] @@ -703,7 +711,7 @@ def test_scenario_scattered_matching(self): Clusters.Thermostat.Attributes.ClusterRevision: 1 } }, - 4: { # Has Z and X + 4: { # Has Z and X Clusters.Descriptor: { Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], Clusters.Descriptor.Attributes.ServerList: [cluster_z_id, cluster_x_id] @@ -721,26 +729,25 @@ def test_scenario_scattered_matching(self): Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 } }, - 5: { # Base + 5: { # Base Clusters.Descriptor: { Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], Clusters.Descriptor.Attributes.ServerList: [] } }, - 6: { # Base + 6: { # Base Clusters.Descriptor: { Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], Clusters.Descriptor.Attributes.ServerList: [] } } } - + success, problems = self.check_composed_device_type_requirements() for p in problems: log.info(p) asserts.assert_true(success, "Failure on scattered matching scenario") - # ========================================================================== # Scenario 8: Complex Location Matching # ========================================================================== @@ -757,14 +764,15 @@ def test_scenario_scattered_matching(self): # Move Thermostat to Endpoint 2 (Child). # Expected: FAIL (Thermostat is not on Self). # ========================================================================== + def test_scenario_complex_location_matching(self): log.info("Running Scenario 8: Complex Location Matching") - + dt_parent_id = 0x0001 dt_child_id = 0x0002 dt_power_source_id = 0x0011 dt_thermostat_id = 0x0301 - + # Mock spec self.xml_device_types = { dt_parent_id: XmlDeviceType( @@ -777,20 +785,23 @@ def test_scenario_complex_location_matching(self): revision_desc={}, composed_device_types=[ self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance()), - self._create_mock_composed_req(dt_power_source_id, "Power Source", get_mandatory_conformance(), device_type_location='anywhere'), - self._create_mock_composed_req(dt_thermostat_id, "Thermostat", get_mandatory_conformance(), device_type_location='deviceEndpoint') + self._create_mock_composed_req(dt_power_source_id, "Power Source", + get_mandatory_conformance(), device_type_location='anywhere'), + self._create_mock_composed_req(dt_thermostat_id, "Thermostat", + get_mandatory_conformance(), device_type_location='deviceEndpoint') ] ), dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, classification_class="simple", classification_scope="endpoint", revision_desc={}), dt_power_source_id: XmlDeviceType(name="Power Source", revision=1, server_clusters={}, client_clusters={}, classification_class="Utility", classification_scope="endpoint", revision_desc={}), - dt_thermostat_id: XmlDeviceType(name="Thermostat", revision=1, server_clusters={}, client_clusters={}, classification_class="Simple", classification_scope="endpoint", revision_desc={}) + dt_thermostat_id: XmlDeviceType(name="Thermostat", revision=1, server_clusters={}, client_clusters={ + }, classification_class="Simple", classification_scope="endpoint", revision_desc={}) } - + # Mock endpoints # EP1: Parent Device AND Thermostat! # EP2: Child Device (direct child of EP1) # EP3: Power Source (Standalone) - + self.endpoints = { 0: { Clusters.BasicInformation: { @@ -819,12 +830,12 @@ def test_scenario_complex_location_matching(self): } } } - + success, problems = self.check_composed_device_type_requirements() for p in problems: log.info(p) asserts.assert_true(success, "Failure in complex location scenario (positive case)") - + # Break it by moving Thermostat to EP2 self.endpoints[1][Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList] = [ Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1) @@ -833,7 +844,7 @@ def test_scenario_complex_location_matching(self): Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1), Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_thermostat_id, revision=1) ] - + success, problems = self.check_composed_device_type_requirements() asserts.assert_false(success, "Unexpected success in complex location scenario (negative case)") From b2175d8bb8307fa836c67f9b780925ab29319e3e Mon Sep 17 00:00:00 2001 From: Arya Hassanli Date: Wed, 27 May 2026 20:45:53 +0000 Subject: [PATCH 20/21] Unittest overrides --- .../testing/device_conformance_tests.py | 2 +- .../TestComposedDeviceTypeMatching.py | 104 +++++++++++++++++- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py index 9150838f27bcf5..5dc6b1afa39107 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/device_conformance_tests.py @@ -479,7 +479,7 @@ def _get_candidate_endpoints(self, endpoint_id: int, location: str, parts_list: return [endpoint_id] elif location == 'rootEndpoint': return [0] - elif location == 'anywhere': + elif location == 'anyEndpoint': return list(self.endpoints.keys()) elif location == 'descendantEndpoint': descendants = [] diff --git a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py index 9110e0c0b255b4..63fd634dd856f1 100644 --- a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py +++ b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py @@ -486,7 +486,7 @@ def test_scenario_anywhere_location(self): revision_desc={}, composed_device_types=[ self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance()), - self._create_mock_composed_req(dt_power_source_id, "Power Source", get_mandatory_conformance(), device_type_location='anywhere') + self._create_mock_composed_req(dt_power_source_id, "Power Source", get_mandatory_conformance(), device_type_location='anyEndpoint') ] ), dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, classification_class="Simple", classification_scope="Endpoint", revision_desc={}), @@ -777,7 +777,7 @@ def test_scenario_complex_location_matching(self): revision_desc={}, composed_device_types=[ self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance()), - self._create_mock_composed_req(dt_power_source_id, "Power Source", get_mandatory_conformance(), device_type_location='anywhere'), + self._create_mock_composed_req(dt_power_source_id, "Power Source", get_mandatory_conformance(), device_type_location='anyEndpoint'), self._create_mock_composed_req(dt_thermostat_id, "Thermostat", get_mandatory_conformance(), device_type_location='deviceEndpoint') ] ), @@ -838,5 +838,105 @@ def test_scenario_complex_location_matching(self): asserts.assert_false(success, "Unexpected success in complex location scenario (negative case)") + # ========================================================================== + # Scenario 9: Element Overrides (Feature) + # ========================================================================== + # Requirement: Parent requires 1 instance of Child DT. + # That instance requires Feature bit 0 to be enabled on Cluster X. + # Topology (Negative): + # Endpoint 1 has Parent DT. PartsList = [2]. + # Endpoint 2 has Child DT and Cluster X (FeatureMap = 0). + # Expected: FAIL (Feature requirement not met). + # Topology (Positive): + # Endpoint 1 has Parent DT. PartsList = [3]. + # Endpoint 3 has Child DT and Cluster X (FeatureMap = 1). + # Expected: PASS + # ========================================================================== + def test_scenario_element_overrides(self): + log.info("Running Scenario 9: Element Overrides (Feature)") + + dt_parent_id = 0x0001 + dt_child_id = 0x0002 + cluster_x_id = 0x0090 # ElectricalPowerMeasurement + + # Mock spec data + req = self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance()) + req.cluster_requirements = { + cluster_x_id: XmlDeviceTypeClusterRequirements( + name="Cluster X", + side=ClusterSide.SERVER, + conformance=get_mandatory_conformance(), + feature_overrides={0x01: get_mandatory_conformance()} # Require bit 0! + ) + } + + self.xml_device_types = { + dt_parent_id: XmlDeviceType( + name="Parent Device", + revision=1, + server_clusters={}, + client_clusters={}, + classification_class="simple", + classification_scope="endpoint", + revision_desc={}, + composed_device_types=[req] + ), + dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, classification_class="simple", classification_scope="endpoint", revision_desc={}) + } + + # Mock device endpoints + # EP1: Parent Device + # EP2: Child Device (has cluster X, but FeatureMap = 0!) -> Should fail! + + self.endpoints = { + 0: { + Clusters.BasicInformation: { + Clusters.BasicInformation.Attributes.SpecificationVersion: 0x01060000 + } + }, + 1: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_parent_id, revision=1)], + Clusters.Descriptor.Attributes.PartsList: [2] + } + }, + 2: { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_x_id] + }, + Clusters.ElectricalPowerMeasurement: { + Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x00, # No features! + Clusters.ElectricalPowerMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalPowerMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 + } + } + } + + success, problems = self.check_composed_device_type_requirements() + asserts.assert_false(success, "Unexpected success when feature override is not met") + + # Now add EP3 which meets the requirement + self.endpoints[1][Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] = [3] + self.endpoints[3] = { + Clusters.Descriptor: { + Clusters.Descriptor.Attributes.DeviceTypeList: [Clusters.Descriptor.Structs.DeviceTypeStruct(deviceType=dt_child_id, revision=1)], + Clusters.Descriptor.Attributes.ServerList: [cluster_x_id] + }, + Clusters.ElectricalPowerMeasurement: { + Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x01, # Has feature bit 0! + Clusters.ElectricalPowerMeasurement.Attributes.AttributeList: [], + Clusters.ElectricalPowerMeasurement.Attributes.AcceptedCommandList: [], + Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 + } + } + + success, problems = self.check_composed_device_type_requirements() + for p in problems: + log.info(p) + asserts.assert_true(success, "Failure when feature override is met") + + if __name__ == "__main__": default_matter_test_main() From 8cf923f001ab302efb2a2a1391a779b6f38b9f7e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 20:49:26 +0000 Subject: [PATCH 21/21] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../TestComposedDeviceTypeMatching.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py index 204a9c4883ddc5..c8f88d9063c7be 100644 --- a/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py +++ b/src/python_testing/test_testing/TestComposedDeviceTypeMatching.py @@ -848,7 +848,6 @@ def test_scenario_complex_location_matching(self): success, problems = self.check_composed_device_type_requirements() asserts.assert_false(success, "Unexpected success in complex location scenario (negative case)") - # ========================================================================== # Scenario 9: Element Overrides (Feature) # ========================================================================== @@ -863,24 +862,25 @@ def test_scenario_complex_location_matching(self): # Endpoint 3 has Child DT and Cluster X (FeatureMap = 1). # Expected: PASS # ========================================================================== + def test_scenario_element_overrides(self): log.info("Running Scenario 9: Element Overrides (Feature)") - + dt_parent_id = 0x0001 dt_child_id = 0x0002 - cluster_x_id = 0x0090 # ElectricalPowerMeasurement - + cluster_x_id = 0x0090 # ElectricalPowerMeasurement + # Mock spec data req = self._create_mock_composed_req(dt_child_id, "Child Device", get_mandatory_conformance()) req.cluster_requirements = { cluster_x_id: XmlDeviceTypeClusterRequirements( - name="Cluster X", - side=ClusterSide.SERVER, + name="Cluster X", + side=ClusterSide.SERVER, conformance=get_mandatory_conformance(), - feature_overrides={0x01: get_mandatory_conformance()} # Require bit 0! + feature_overrides={0x01: get_mandatory_conformance()} # Require bit 0! ) } - + self.xml_device_types = { dt_parent_id: XmlDeviceType( name="Parent Device", @@ -892,13 +892,14 @@ def test_scenario_element_overrides(self): revision_desc={}, composed_device_types=[req] ), - dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, classification_class="simple", classification_scope="endpoint", revision_desc={}) + dt_child_id: XmlDeviceType(name="Child Device", revision=1, server_clusters={}, client_clusters={}, + classification_class="simple", classification_scope="endpoint", revision_desc={}) } - + # Mock device endpoints # EP1: Parent Device # EP2: Child Device (has cluster X, but FeatureMap = 0!) -> Should fail! - + self.endpoints = { 0: { Clusters.BasicInformation: { @@ -917,17 +918,17 @@ def test_scenario_element_overrides(self): Clusters.Descriptor.Attributes.ServerList: [cluster_x_id] }, Clusters.ElectricalPowerMeasurement: { - Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x00, # No features! + Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x00, # No features! Clusters.ElectricalPowerMeasurement.Attributes.AttributeList: [], Clusters.ElectricalPowerMeasurement.Attributes.AcceptedCommandList: [], Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 } } } - + success, problems = self.check_composed_device_type_requirements() asserts.assert_false(success, "Unexpected success when feature override is not met") - + # Now add EP3 which meets the requirement self.endpoints[1][Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] = [3] self.endpoints[3] = { @@ -936,13 +937,13 @@ def test_scenario_element_overrides(self): Clusters.Descriptor.Attributes.ServerList: [cluster_x_id] }, Clusters.ElectricalPowerMeasurement: { - Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x01, # Has feature bit 0! + Clusters.ElectricalPowerMeasurement.Attributes.FeatureMap: 0x01, # Has feature bit 0! Clusters.ElectricalPowerMeasurement.Attributes.AttributeList: [], Clusters.ElectricalPowerMeasurement.Attributes.AcceptedCommandList: [], Clusters.ElectricalPowerMeasurement.Attributes.ClusterRevision: 1 } } - + success, problems = self.check_composed_device_type_requirements() for p in problems: log.info(p)