From bbe49aed7b81cd28208231b3243328f5c346b4cb Mon Sep 17 00:00:00 2001 From: cecille Date: Fri, 24 Nov 2023 09:44:30 -0500 Subject: [PATCH 1/5] DeviceConformance: Factor out conformance checks This will let us use conformance as a pre-requisite in other tools. --- src/python_testing/TC_DeviceConformance.py | 129 ++++++++++++--------- 1 file changed, 75 insertions(+), 54 deletions(-) diff --git a/src/python_testing/TC_DeviceConformance.py b/src/python_testing/TC_DeviceConformance.py index ddccc041cba567..2e826f1187b43d 100644 --- a/src/python_testing/TC_DeviceConformance.py +++ b/src/python_testing/TC_DeviceConformance.py @@ -23,18 +23,19 @@ from conformance_support import ConformanceDecision, conformance_allowed from global_attribute_ids import GlobalAttributeIds from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, MatterBaseTest, - async_test_body, default_matter_test_main) + async_test_body, default_matter_test_main, ProblemSeverity, ProblemNotice) from spec_parsing_support import CommandType, build_xml_clusters -class TC_DeviceConformance(MatterBaseTest, BasicCompositionTests): - @async_test_body - async def setup_class(self): - super().setup_class() - await self.setup_class_helper() +class DeviceConformanceTests(BasicCompositionTests): + async def setup_class_helper(self): + await super().setup_class_helper() self.xml_clusters, self.problems = build_xml_clusters() - def test_TC_IDM_10_2(self): + def check_conformance(self, ignore_in_progress: bool, is_ci: bool): + problems = [] + success = True + def conformance_str(conformance: Callable, feature_map: uint, feature_dict: dict[str, uint]) -> str: codes = [] for mask, details in feature_dict.items(): @@ -43,8 +44,16 @@ def conformance_str(conformance: Callable, feature_map: uint, feature_dict: dict return f'Conformance: {str(conformance)}, implemented features: {",".join(codes)}' - ignore_in_progress = self.user_params.get("ignore_in_progress", False) - is_ci = self.check_pics('PICS_SDK_CI_ONLY') + def record_problem(location, problem, severity): + problems.append(ProblemNotice("IDM-10.2", location, severity, problem, "")) + + def record_error(location, problem): + nonlocal success + record_problem(location, problem, ProblemSeverity.ERROR) + success = False + + def record_warning(location, problem): + record_problem(location, problem, ProblemSeverity.WARNING) ignore_attributes: dict[int, list[int]] = {} ignore_features: dict[int, list[int]] = {} @@ -75,8 +84,7 @@ def conformance_str(conformance: Callable, feature_map: uint, feature_dict: dict continue location = ClusterPathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id) # TODO: update this from a warning once we have all the data - self.record_warning(self.get_test_name(), location=location, - problem='Standard cluster found on device, but is not present in spec data') + record_warning(location=location, problem='Standard cluster found on device, but is not present in spec data') continue feature_map = cluster[GlobalAttributeIds.FEATURE_MAP_ID] @@ -90,25 +98,21 @@ def conformance_str(conformance: Callable, feature_map: uint, feature_dict: dict location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=GlobalAttributeIds.FEATURE_MAP_ID) if f not in self.xml_clusters[cluster_id].features.keys(): - self.record_error(self.get_test_name(), location=location, problem=f'Unknown feature with mask 0x{f:02x}') - success = False + record_error(location=location, problem=f'Unknown feature with mask 0x{f:02x}') continue if cluster_id in ignore_features and f in ignore_features[cluster_id]: continue xml_feature = self.xml_clusters[cluster_id].features[f] conformance_decision = xml_feature.conformance(feature_map, attribute_list, all_command_list) if not conformance_allowed(conformance_decision, allow_provisional): - self.record_error(self.get_test_name(), location=location, - problem=f'Disallowed feature with mask 0x{f:02x}') - success = False + record_error(location=location, problem=f'Disallowed feature with mask 0x{f:02x}') for feature_mask, xml_feature in self.xml_clusters[cluster_id].features.items(): if cluster_id in ignore_features and feature_mask in ignore_features[cluster_id]: continue conformance_decision = xml_feature.conformance(feature_map, attribute_list, all_command_list) if conformance_decision == ConformanceDecision.MANDATORY and feature_mask not in feature_masks: - self.record_error(self.get_test_name(), location=location, - problem=f'Required feature with mask 0x{f:02x} is not present in feature map. {conformance_str(xml_feature.conformance, feature_map, self.xml_clusters[cluster_id].features)}') - success = False + record_error( + location=location, problem=f'Required feature with mask 0x{f:02x} is not present in feature map. {conformance_str(xml_feature.conformance, feature_map, self.xml_clusters[cluster_id].features)}') # Attribute conformance checking for attribute_id, attribute in cluster.items(): @@ -119,29 +123,24 @@ def conformance_str(conformance: Callable, feature_map: uint, feature_dict: dict # TODO: Consolidate the range checks with IDM-10.1 once that lands if attribute_id <= 0x4FFF: # manufacturer attribute - self.record_error(self.get_test_name(), location=location, - problem='Standard attribute found on device, but not in spec') - success = False + record_error(location=location, problem='Standard attribute found on device, but not in spec') continue xml_attribute = self.xml_clusters[cluster_id].attributes[attribute_id] conformance_decision = xml_attribute.conformance(feature_map, attribute_list, all_command_list) if not conformance_allowed(conformance_decision, allow_provisional): location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id) - self.record_error(self.get_test_name(), location=location, - problem=f'Attribute 0x{attribute_id:02x} is included, but is disallowed by conformance. {conformance_str(xml_attribute.conformance, feature_map, self.xml_clusters[cluster_id].features)}') - success = False + record_error( + location=location, problem=f'Attribute 0x{attribute_id:02x} is included, but is disallowed by conformance. {conformance_str(xml_attribute.conformance, feature_map, self.xml_clusters[cluster_id].features)}') for attribute_id, xml_attribute in self.xml_clusters[cluster_id].attributes.items(): if cluster_id in ignore_attributes and attribute_id in ignore_attributes[cluster_id]: continue conformance_decision = xml_attribute.conformance(feature_map, attribute_list, all_command_list) if conformance_decision == ConformanceDecision.MANDATORY and attribute_id not in cluster.keys(): location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=attribute_id) - self.record_error(self.get_test_name(), location=location, - problem=f'Attribute 0x{attribute_id:02x} is required, but is not present on the DUT. {conformance_str(xml_attribute.conformance, feature_map, self.xml_clusters[cluster_id].features)}') - success = False + record_error( + location=location, problem=f'Attribute 0x{attribute_id:02x} is required, but is not present on the DUT. {conformance_str(xml_attribute.conformance, feature_map, self.xml_clusters[cluster_id].features)}') - def check_spec_conformance_for_commands(command_type: CommandType) -> bool: - success = True + def check_spec_conformance_for_commands(command_type: CommandType): global_attribute_id = GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID if command_type == CommandType.ACCEPTED else GlobalAttributeIds.GENERATED_COMMAND_LIST_ID xml_commands_dict = self.xml_clusters[cluster_id].accepted_commands if command_type == CommandType.ACCEPTED else self.xml_clusters[cluster_id].generated_commands command_list = cluster[global_attribute_id] @@ -152,40 +151,42 @@ def check_spec_conformance_for_commands(command_type: CommandType) -> bool: if command_id <= 0xFF: # manufacturer command continue - self.record_error(self.get_test_name(), location=location, - problem='Standard command found on device, but not in spec') - success = False + record_error(location=location, problem='Standard command found on device, but not in spec') continue xml_command = xml_commands_dict[command_id] conformance_decision = xml_command.conformance(feature_map, attribute_list, all_command_list) if not conformance_allowed(conformance_decision, allow_provisional): - self.record_error(self.get_test_name(), location=location, - problem=f'Command 0x{command_id:02x} is included, but disallowed by conformance. {conformance_str(xml_command.conformance, feature_map, self.xml_clusters[cluster_id].features)}') - success = False + record_error( + location=location, problem=f'Command 0x{command_id:02x} is included, but disallowed by conformance. {conformance_str(xml_command.conformance, feature_map, self.xml_clusters[cluster_id].features)}') for command_id, xml_command in xml_commands_dict.items(): conformance_decision = xml_command.conformance(feature_map, attribute_list, all_command_list) if conformance_decision == ConformanceDecision.MANDATORY and command_id not in command_list: location = CommandPathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, command_id=command_id) - self.record_error(self.get_test_name(), location=location, - problem=f'Command 0x{command_id:02x} is required, but is not present on the DUT. {conformance_str(xml_command.conformance, feature_map, self.xml_clusters[cluster_id].features)}') - success = False - return success + record_error( + location=location, problem=f'Command 0x{command_id:02x} is required, but is not present on the DUT. {conformance_str(xml_command.conformance, feature_map, self.xml_clusters[cluster_id].features)}') # Command conformance checking - cmd_success = check_spec_conformance_for_commands(CommandType.ACCEPTED) - success = False if not cmd_success else success - cmd_success = check_spec_conformance_for_commands(CommandType.GENERATED) - success = False if not cmd_success else success + check_spec_conformance_for_commands(CommandType.ACCEPTED) + check_spec_conformance_for_commands(CommandType.GENERATED) # TODO: Add choice checkers + print(f'success = {success}') + return success, problems - if not success: - self.fail_current_test("Problems with conformance") - - def test_IDM_10_3(self): + def check_revisions(self, ignore_in_progress: bool): + problems = [] success = True - ignore_in_progress = self.user_params.get("ignore_in_progress", False) + def record_problem(location, problem, severity): + problems.append(ProblemNotice("IDM-10.3", location, severity, problem, "")) + + def record_error(location, problem): + nonlocal success + record_problem(location, problem, ProblemSeverity.ERROR) + success = False + + def record_warning(location, problem): + record_problem(location, problem, ProblemSeverity.WARNING) ignore_revisions: list[int] = [] if ignore_in_progress: @@ -201,17 +202,37 @@ def test_IDM_10_3(self): continue location = ClusterPathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id) # TODO: update this from a warning once we have all the data - self.record_warning(self.get_test_name(), location=location, - problem='Standard cluster found on device, but is not present in spec data') + record_warning(location=location, problem='Standard cluster found on device, but is not present in spec data') continue if cluster_id in ignore_revisions: continue if int(self.xml_clusters[cluster_id].revision) != cluster[GlobalAttributeIds.CLUSTER_REVISION_ID]: location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=cluster_id, attribute_id=GlobalAttributeIds.CLUSTER_REVISION_ID) - self.record_error(self.get_test_name( - ), location=location, problem=f'Revision found on cluster ({cluster[GlobalAttributeIds.CLUSTER_REVISION_ID]}) does not match revision listed in the spec ({self.xml_clusters[cluster_id].revision})') - success = False + record_error( + location=location, problem=f'Revision found on cluster ({cluster[GlobalAttributeIds.CLUSTER_REVISION_ID]}) does not match revision listed in the spec ({self.xml_clusters[cluster_id].revision})') + + return success, problems + + +class TC_DeviceConformance(MatterBaseTest, DeviceConformanceTests): + @async_test_body + async def setup_class(self): + super().setup_class() + await self.setup_class_helper() + + def test_TC_IDM_10_2(self): + ignore_in_progress = self.user_params.get("ignore_in_progress", False) + is_ci = self.check_pics('PICS_SDK_CI_ONLY') + success, problems = self.check_conformance(ignore_in_progress, is_ci) + self.problems.extend(problems) + if not success: + self.fail_current_test("Problems with conformance") + + def test_TC_IDM_10_3(self): + ignore_in_progress = self.user_params.get("ignore_in_progress", False) + success, problems = self.check_revisions(ignore_in_progress) + self.problems.extend(problems) if not success: self.fail_current_test("Problems with cluster revision on at least one cluster") From 9f0f8569a678f93e2727c96d6eaa2c5f1aae4143 Mon Sep 17 00:00:00 2001 From: cecille Date: Fri, 24 Nov 2023 11:25:25 -0500 Subject: [PATCH 2/5] Generates a minimal representation of a device This representation gives all the top level optional elements that are implemented on the device. Anything that DOES NOT appear in this representation is either mandatory or disallowed based on the elements represented here. --- src/python_testing/MinimalRepresentation.py | 143 ++++++++++++++++++++ src/python_testing/spec_parsing_support.py | 4 +- 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/python_testing/MinimalRepresentation.py diff --git a/src/python_testing/MinimalRepresentation.py b/src/python_testing/MinimalRepresentation.py new file mode 100644 index 00000000000000..498c55ca0e61ad --- /dev/null +++ b/src/python_testing/MinimalRepresentation.py @@ -0,0 +1,143 @@ +# +# Copyright (c) 2023 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. +# + +from typing import Callable + +from dataclasses import dataclass, field +import chip.clusters as Clusters +from chip.tlv import uint +from TC_DeviceConformance import DeviceConformanceTests +from global_attribute_ids import GlobalAttributeIds +from matter_testing_support import (MatterBaseTest, async_test_body, default_matter_test_main) +from conformance_support import ConformanceDecision + + +@dataclass +class ClusterMinimalElements: + # masks + features: list[uint] = field(default_factory=list) + # IDs + attributes: list[uint] = field(default_factory=list) + # Only received commands are necessary - generated events are ALWAYS determined from accepted + commands: list[uint] = field(default_factory=list) + # TODO: need event support + + +class MinimalRepresentationChecker(DeviceConformanceTests): + def GenerateMinimals(self, ignore_in_progress: bool, is_ci: bool) -> dict[uint, dict[uint, ClusterMinimalElements]]: + if not self.xml_clusters: + self.setup_class_helper() + + success, _ = self.check_conformance(ignore_in_progress, is_ci) + if not success: + self.fail_current_test("Problems with conformance") + + # Now what we know the conformance is OK, we want to expose all the data model elements on the device + # that are OPTIONAL given the other elements that are present. We can do this by assessing the conformance + # again only on the elements we have. Because we've already run the full conformance checkers, we can rely + # on the optional response really meaning optional. + # TODO: do we also want to record the optional stuff that's NOT implemented? + # endpoint -> list of clusters by id + representation: dict[uint, dict[uint, ClusterMinimalElements]] = {} + for endpoint_id, endpoint in self.endpoints_tlv.items(): + representation[endpoint_id] = {} + for cluster_id, cluster in endpoint.items(): + minimal = ClusterMinimalElements() + if cluster_id not in self.xml_clusters.keys(): + continue + + feature_map = cluster[GlobalAttributeIds.FEATURE_MAP_ID] + attribute_list = cluster[GlobalAttributeIds.ATTRIBUTE_LIST_ID] + all_command_list = cluster[GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID] + \ + cluster[GlobalAttributeIds.GENERATED_COMMAND_LIST_ID] + accepted_command_list = cluster[GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID] + + # All optional features + feature_masks = [1 << i for i in range(32) if feature_map & (1 << i)] + for f in feature_masks: + xml_feature = self.xml_clusters[cluster_id].features[f] + conformance_decision = xml_feature.conformance(feature_map, attribute_list, all_command_list) + if conformance_decision == ConformanceDecision.OPTIONAL: + minimal.features.append(f) + + # All optional attributes + for attribute_id, attribute in cluster.items(): + if attribute_id not in self.xml_clusters[cluster_id].attributes.keys(): + if attribute_id > 0xFFFF: + # MEI + minimal.attributes.append(attribute_id) + continue + xml_attribute = self.xml_clusters[cluster_id].attributes[attribute_id] + conformance_decision = xml_attribute.conformance(feature_map, attribute_list, all_command_list) + if conformance_decision == ConformanceDecision.OPTIONAL: + minimal.attributes.append(attribute_id) + + # All optional commands + for command_id in accepted_command_list: + if command_id not in self.xml_clusters[cluster_id].accepted_commands: + if command_id > 0xFFFF: + # MEI + minimal.attributes.append(command_id) + continue + xml_command = self.xml_clusters[cluster_id].accepted_commands[command_id] + conformance_decision = xml_command.conformance(feature_map, attribute_list, all_command_list) + if conformance_decision == ConformanceDecision.OPTIONAL: + minimal.commands.append(command_id) + + representation[endpoint_id][cluster_id] = minimal + + return representation + + def PrettyPrintRepresentation(self, representation: dict[uint, dict[uint, ClusterMinimalElements]]) -> None: + for endpoint_id, cluster_list in representation.items(): + print(f'Endpoint: {endpoint_id}') + for cluster_id, minimals in cluster_list.items(): + name = self.xml_clusters[cluster_id].name + print(f' Cluster {cluster_id:04x} - {name}') + print(f' Features:') + for feature in minimals.features: + code = self.xml_clusters[cluster_id].features[feature].code + print(f' {feature:02x}: {code}') + print(f' Attributes:') + for attribute in minimals.attributes: + name = self.xml_clusters[cluster_id].attributes[attribute].name + print(f' {attribute:02x}: {name}') + print(f' Commands:') + for command in minimals.commands: + name = self.xml_clusters[cluster_id].accepted_commands[command].name + print(f' {command:02x}: {name}') + + +# Helper for running this against a test device through the python test framework +class MinimalRunner(MatterBaseTest, MinimalRepresentationChecker): + @async_test_body + async def setup_class(self): + super().setup_class() + await self.setup_class_helper() + + def test_MinimalRepresentation(self): + # Before we can generate a minimal representation, we need to make sure that the device is conformant. + # Otherwise, the values we extract aren't fully informative. + ignore_in_progress = self.user_params.get("ignore_in_progress", False) + is_ci = self.check_pics('PICS_SDK_CI_ONLY') + representation = self.GenerateMinimals(ignore_in_progress, is_ci) + print(type(representation[0])) + self.PrettyPrintRepresentation(representation) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/spec_parsing_support.py b/src/python_testing/spec_parsing_support.py index fa08f34d776a5d..13c2f91e203029 100644 --- a/src/python_testing/spec_parsing_support.py +++ b/src/python_testing/spec_parsing_support.py @@ -68,7 +68,9 @@ class XmlCluster: feature_map: dict[str, uint] attribute_map: dict[str, uint] command_map: dict[str, uint] - features: dict[str, XmlFeature] + # mask to XmlFeature + features: dict[uint, XmlFeature] + # IDs to class attributes: dict[uint, XmlAttribute] accepted_commands: dict[uint, XmlCommand] generated_commands: dict[uint, XmlCommand] From a8e2f0014ed97655b58f0632c1c26a1708daa7af Mon Sep 17 00:00:00 2001 From: cecille Date: Fri, 24 Nov 2023 11:37:25 -0500 Subject: [PATCH 3/5] Fix linter --- src/python_testing/MinimalRepresentation.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/python_testing/MinimalRepresentation.py b/src/python_testing/MinimalRepresentation.py index 498c55ca0e61ad..ba2b3c27ae2c97 100644 --- a/src/python_testing/MinimalRepresentation.py +++ b/src/python_testing/MinimalRepresentation.py @@ -15,15 +15,13 @@ # limitations under the License. # -from typing import Callable - from dataclasses import dataclass, field -import chip.clusters as Clusters + from chip.tlv import uint -from TC_DeviceConformance import DeviceConformanceTests -from global_attribute_ids import GlobalAttributeIds -from matter_testing_support import (MatterBaseTest, async_test_body, default_matter_test_main) from conformance_support import ConformanceDecision +from global_attribute_ids import GlobalAttributeIds +from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main +from TC_DeviceConformance import DeviceConformanceTests @dataclass @@ -108,15 +106,15 @@ def PrettyPrintRepresentation(self, representation: dict[uint, dict[uint, Cluste for cluster_id, minimals in cluster_list.items(): name = self.xml_clusters[cluster_id].name print(f' Cluster {cluster_id:04x} - {name}') - print(f' Features:') + print(' Features:') for feature in minimals.features: code = self.xml_clusters[cluster_id].features[feature].code print(f' {feature:02x}: {code}') - print(f' Attributes:') + print(' Attributes:') for attribute in minimals.attributes: name = self.xml_clusters[cluster_id].attributes[attribute].name print(f' {attribute:02x}: {name}') - print(f' Commands:') + print(' Commands:') for command in minimals.commands: name = self.xml_clusters[cluster_id].accepted_commands[command].name print(f' {command:02x}: {name}') From eeb6d3d2d46035f86473fecf2093cd3bc3337ead Mon Sep 17 00:00:00 2001 From: "Restyled.io" Date: Fri, 24 Nov 2023 16:39:18 +0000 Subject: [PATCH 4/5] Restyled by isort --- src/python_testing/TC_DeviceConformance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python_testing/TC_DeviceConformance.py b/src/python_testing/TC_DeviceConformance.py index 2e826f1187b43d..18ad273a6241d6 100644 --- a/src/python_testing/TC_DeviceConformance.py +++ b/src/python_testing/TC_DeviceConformance.py @@ -22,8 +22,8 @@ from chip.tlv import uint from conformance_support import ConformanceDecision, conformance_allowed from global_attribute_ids import GlobalAttributeIds -from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, MatterBaseTest, - async_test_body, default_matter_test_main, ProblemSeverity, ProblemNotice) +from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, MatterBaseTest, ProblemNotice, + ProblemSeverity, async_test_body, default_matter_test_main) from spec_parsing_support import CommandType, build_xml_clusters From ca9e1b1519e351ecf12ad8b8d803eecce8fe316f Mon Sep 17 00:00:00 2001 From: cecille Date: Fri, 15 Dec 2023 10:16:55 -0500 Subject: [PATCH 5/5] address review comments --- src/python_testing/MinimalRepresentation.py | 24 ++++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/python_testing/MinimalRepresentation.py b/src/python_testing/MinimalRepresentation.py index ba2b3c27ae2c97..c4a19b76cbbf9f 100644 --- a/src/python_testing/MinimalRepresentation.py +++ b/src/python_testing/MinimalRepresentation.py @@ -26,12 +26,10 @@ @dataclass class ClusterMinimalElements: - # masks - features: list[uint] = field(default_factory=list) - # IDs - attributes: list[uint] = field(default_factory=list) + feature_masks: list[uint] = field(default_factory=list) + attribute_ids: list[uint] = field(default_factory=list) # Only received commands are necessary - generated events are ALWAYS determined from accepted - commands: list[uint] = field(default_factory=list) + command_ids: list[uint] = field(default_factory=list) # TODO: need event support @@ -70,31 +68,31 @@ def GenerateMinimals(self, ignore_in_progress: bool, is_ci: bool) -> dict[uint, xml_feature = self.xml_clusters[cluster_id].features[f] conformance_decision = xml_feature.conformance(feature_map, attribute_list, all_command_list) if conformance_decision == ConformanceDecision.OPTIONAL: - minimal.features.append(f) + minimal.feature_masks.append(f) # All optional attributes for attribute_id, attribute in cluster.items(): if attribute_id not in self.xml_clusters[cluster_id].attributes.keys(): if attribute_id > 0xFFFF: # MEI - minimal.attributes.append(attribute_id) + minimal.attribute_ids.append(attribute_id) continue xml_attribute = self.xml_clusters[cluster_id].attributes[attribute_id] conformance_decision = xml_attribute.conformance(feature_map, attribute_list, all_command_list) if conformance_decision == ConformanceDecision.OPTIONAL: - minimal.attributes.append(attribute_id) + minimal.attribute_ids.append(attribute_id) # All optional commands for command_id in accepted_command_list: if command_id not in self.xml_clusters[cluster_id].accepted_commands: if command_id > 0xFFFF: # MEI - minimal.attributes.append(command_id) + minimal.attribute_ids.append(command_id) continue xml_command = self.xml_clusters[cluster_id].accepted_commands[command_id] conformance_decision = xml_command.conformance(feature_map, attribute_list, all_command_list) if conformance_decision == ConformanceDecision.OPTIONAL: - minimal.commands.append(command_id) + minimal.command_ids.append(command_id) representation[endpoint_id][cluster_id] = minimal @@ -107,15 +105,15 @@ def PrettyPrintRepresentation(self, representation: dict[uint, dict[uint, Cluste name = self.xml_clusters[cluster_id].name print(f' Cluster {cluster_id:04x} - {name}') print(' Features:') - for feature in minimals.features: + for feature in minimals.feature_masks: code = self.xml_clusters[cluster_id].features[feature].code print(f' {feature:02x}: {code}') print(' Attributes:') - for attribute in minimals.attributes: + for attribute in minimals.attribute_ids: name = self.xml_clusters[cluster_id].attributes[attribute].name print(f' {attribute:02x}: {name}') print(' Commands:') - for command in minimals.commands: + for command in minimals.command_ids: name = self.xml_clusters[cluster_id].accepted_commands[command].name print(f' {command:02x}: {name}')