Skip to content

Commit 8398e93

Browse files
Spec parsing: Use new ID table (project-chip#32410)
* Spec parsing: Use new ID table The ID table has now been added for every cluster, including alias clusters. This means we can now just make direct copies of the alias clusters as we parse the cluster file. It also means we can ignore the top-level cluster tag, so we don't need to worry about the new weird naming. Not yet handled in the XML - pics for aliased clusters. * Restyled by isort --------- Co-authored-by: Restyled.io <commits@restyled.io>
1 parent 3db0d30 commit 8398e93

File tree

2 files changed

+136
-61
lines changed

2 files changed

+136
-61
lines changed

src/python_testing/TestSpecParsingSupport.py

+95-3
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
from global_attribute_ids import GlobalAttributeIds
2222
from matter_testing_support import MatterBaseTest, ProblemNotice, default_matter_test_main
2323
from mobly import asserts
24-
from spec_parsing_support import (ClusterParser, XmlCluster, add_cluster_data_from_xml, check_clusters_for_unknown_commands,
25-
combine_derived_clusters_with_base)
24+
from spec_parsing_support import (ClusterParser, XmlCluster, add_cluster_data_from_xml, build_xml_clusters,
25+
check_clusters_for_unknown_commands, combine_derived_clusters_with_base)
2626

2727
# TODO: improve the test coverage here
2828
# https://github.com/project-chip/connectedhomeip/issues/30958
@@ -40,6 +40,9 @@ def single_attribute_cluster_xml(read_access: str, write_access: str, write_supp
4040
'<revision revision="2" summary="Some other revision"/>'
4141
'<revision revision="3" summary="another revision"/>'
4242
'</revisionHistory>')
43+
id_table = ('<clusterIds>'
44+
f'<clusterId id="{CLUSTER_ID}" name="{CLUSTER_NAME}"/>'
45+
'</clusterIds>')
4346
classification = '<classification hierarchy="base" role="utility" picsCode="TEST" scope="Node"/>'
4447
read_access_str = f'read="true" readPrivilege="{read_access}"' if read_access is not None else ""
4548
write_access_str = f'write="{write_supported}" writePrivilege="{write_access}"' if write_access is not None else ""
@@ -53,14 +56,15 @@ def single_attribute_cluster_xml(read_access: str, write_access: str, write_supp
5356

5457
return (f'{xml_cluster}'
5558
f'{revision_table}'
59+
f'{id_table}'
5660
f'{classification}'
5761
f'{attribute}'
5862
'</cluster>')
5963

6064

6165
def parse_cluster(xml: str) -> XmlCluster:
6266
cluster = ElementTree.fromstring(xml)
63-
parser = ClusterParser(cluster, CLUSTER_ID, CLUSTER_NAME, False)
67+
parser = ClusterParser(cluster, CLUSTER_ID, CLUSTER_NAME)
6468
return parser.create_cluster()
6569

6670

@@ -83,6 +87,9 @@ def get_access_enum_from_string(access_str: str) -> Clusters.AccessControl.Enums
8387
' <revisionHistory>'
8488
' <revision revision="1" summary="Initial version"/>'
8589
' </revisionHistory>'
90+
' <clusterIds>'
91+
' <clusterId name="Test Base"/>'
92+
' </clusterIds>'
8693
' <classification hierarchy="base" role="application" picsCode="BASE" scope="Endpoint"/>'
8794
' <features>'
8895
' <feature bit="0" code="DEPONOFF" name="OnOff" summary="Dependency with the OnOff cluster">'
@@ -151,6 +158,9 @@ def get_access_enum_from_string(access_str: str) -> Clusters.AccessControl.Enums
151158
' <revisionHistory>'
152159
' <revision revision="1" summary="Initial Release"/>'
153160
' </revisionHistory>'
161+
' <clusterIds>'
162+
' <clusterId id="0xFFFF" name="Test Derived"/>'
163+
' </clusterIds>'
154164
' <classification hierarchy="derived" baseCluster="Test Base" role="application" picsCode="MWOM" scope="Endpoint"/>'
155165
' <attributes>'
156166
' <attribute id="0x0000" name="SupportedModes">'
@@ -181,6 +191,9 @@ def get_access_enum_from_string(access_str: str) -> Clusters.AccessControl.Enums
181191
' <revisionHistory>'
182192
' <revision revision="1" summary="Initial version"/>'
183193
' </revisionHistory>'
194+
' <clusterIds>'
195+
' <clusterId id="0xFFFE" name="Test Unknown Command"/>'
196+
' </clusterIds>'
184197
' <classification hierarchy="base" role="application" picsCode="BASE" scope="Endpoint"/>'
185198
' <commands>'
186199
' <command id="0x00" name="ChangeToMode" direction="commandToClient">'
@@ -191,8 +204,32 @@ def get_access_enum_from_string(access_str: str) -> Clusters.AccessControl.Enums
191204
'</cluster>'
192205
)
193206

207+
ALIASED_CLUSTERS = (
208+
'<cluster xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="types types.xsd cluster cluster.xsd" id="" name="Test Aliases" revision="1">'
209+
' <revisionHistory>'
210+
' <revision revision="1" summary="Initial version"/>'
211+
' </revisionHistory>'
212+
' <clusterIds>'
213+
' <clusterId id="0xFFFE" name="Test Alias1"/>'
214+
' <clusterId id="0xFFFD" name="Test Alias2"/>'
215+
' </clusterIds>'
216+
' <classification hierarchy="base" role="application" picsCode="BASE" scope="Endpoint"/>'
217+
' <commands>'
218+
' <command id="0x00" name="ChangeToMode" direction="commandToServer">'
219+
' <access invokePrivilege="operate"/>'
220+
' <mandatoryConform/>'
221+
' </command>'
222+
' </commands>'
223+
'</cluster>'
224+
)
225+
194226

195227
class TestSpecParsingSupport(MatterBaseTest):
228+
def setup_class(self):
229+
super().setup_class()
230+
self.spec_xml_clusters, self.spec_problems = build_xml_clusters()
231+
self.all_spec_clusters = set([(id, c.name, c.pics) for id, c in self.spec_xml_clusters.items()])
232+
196233
def test_spec_parsing_access(self):
197234
strs = [None, 'view', 'operate', 'manage', 'admin']
198235
for read in strs:
@@ -296,6 +333,61 @@ def test_missing_command_direction(self):
296333
asserts.assert_equal(problems[0].location.cluster_id, 0xFFFE, "Unexpected problem location (cluster id)")
297334
asserts.assert_equal(problems[0].location.command_id, 0, "Unexpected problem location (command id)")
298335

336+
def test_aliased_clusters(self):
337+
clusters: dict[int, XmlCluster] = {}
338+
pure_base_clusters: dict[str, XmlCluster] = {}
339+
ids_by_name: dict[str, int] = {}
340+
problems: list[ProblemNotice] = []
341+
cluster_xml = ElementTree.fromstring(ALIASED_CLUSTERS)
342+
343+
add_cluster_data_from_xml(cluster_xml, clusters, pure_base_clusters, ids_by_name, problems)
344+
asserts.assert_equal(len(problems), 0, "Unexpected problem parsing aliased clusters")
345+
asserts.assert_equal(len(clusters), 2, "Unexpected number of clusters when parsing aliased cluster set")
346+
asserts.assert_equal(len(pure_base_clusters), 0, "Unexpected number of pure base clusters")
347+
asserts.assert_equal(len(ids_by_name), 2, "Unexpected number of ids by name")
348+
349+
ids = [(id, c.name) for id, c in clusters.items()]
350+
asserts.assert_true((0xFFFE, 'Test Alias1') in ids, "Unable to find Test Alias1 cluster in parsed clusters")
351+
asserts.assert_true((0xFFFD, 'Test Alias2') in ids, "Unable to find Test Alias2 cluster in parsed clusters")
352+
353+
def test_known_aliased_clusters(self):
354+
known_aliased_clusters = set([(0x040C, 'Carbon Monoxide Concentration Measurement', 'CMOCONC'),
355+
(0x040D, 'Carbon Dioxide Concentration Measurement', 'CDOCONC'),
356+
(0x0413, 'Nitrogen Dioxide Concentration Measurement', 'NDOCONC'),
357+
(0x0415, 'Ozone Concentration Measurement', 'OZCONC'),
358+
# Change to "PM2.5 Concentration Measurement" once https://github.com/csa-data-model/projects/issues/453 is fixed
359+
(0x042A, 'PM2', 'PMICONC'),
360+
(0x042B, 'Formaldehyde Concentration Measurement', 'FLDCONC'),
361+
(0x042C, 'PM1 Concentration Measurement', 'PMHCONC'),
362+
(0x042D, 'PM10 Concentration Measurement', 'PMKCONC'),
363+
(0x042E, 'Total Volatile Organic Compounds Concentration Measurement', 'TVOCCONC'),
364+
(0x042F, 'Radon Concentration Measurement', 'RNCONC'),
365+
(0x0071, 'HEPA Filter Monitoring', 'HEPAFREMON'),
366+
(0x0072, 'Activated Carbon Filter Monitoring', 'ACFREMON'),
367+
(0x0405, 'Relative Humidity Measurement', 'RH')])
368+
369+
missing_clusters = known_aliased_clusters - self.all_spec_clusters
370+
asserts.assert_equal(len(missing_clusters), 0, f"Missing aliased clusters from DM XML - {missing_clusters}")
371+
372+
def test_known_derived_clusters(self):
373+
known_derived_clusters = set([(0x0048, 'Oven Cavity Operational State', 'OVENOPSTATE'),
374+
(0x0049, 'Oven Mode', 'OTCCM'),
375+
(0x0051, 'Laundry Washer Mode', 'LWM'),
376+
(0x0052, 'Refrigerator And Temperature Controlled Cabinet Mode', 'TCCM'),
377+
(0x0054, 'RVC Run Mode', 'RVCRUNM'),
378+
(0x0055, 'RVC Clean Mode', 'RVCCLEANM'),
379+
(0x0057, 'Refrigerator Alarm', 'REFALM'),
380+
(0x0059, 'Dishwasher Mode', 'DISHM'),
381+
(0x005c, 'Smoke CO Alarm', 'SMOKECO'),
382+
(0x005d, 'Dishwasher Alarm', 'DISHALM'),
383+
(0x005e, 'Microwave Oven Mode', 'MWOM'),
384+
(0x0061, 'RVC Operational State', 'RVCOPSTATE')])
385+
386+
missing_clusters = known_derived_clusters - self.all_spec_clusters
387+
asserts.assert_equal(len(missing_clusters), 0, f"Missing aliased clusters from DM XML - {missing_clusters}")
388+
for d in known_derived_clusters:
389+
asserts.assert_true(self.spec_xml_clusters is not None, "Derived cluster with no base cluster marker")
390+
299391

300392
if __name__ == "__main__":
301393
default_matter_test_main()

src/python_testing/spec_parsing_support.py

+41-58
Original file line numberDiff line numberDiff line change
@@ -113,40 +113,28 @@ class CommandType(Enum):
113113
UNKNOWN = auto()
114114

115115

116-
# workaround for aliased clusters not appearing in the xml. Remove this once https://github.com/csa-data-model/projects/issues/373 is addressed
117-
CONC_CLUSTERS = {0x040C: ('Carbon Monoxide Concentration Measurement', 'CMOCONC'),
118-
0x040D: ('Carbon Dioxide Concentration Measurement', 'CDOCONC'),
119-
0x0413: ('Nitrogen Dioxide Concentration Measurement', 'NDOCONC'),
120-
0x0415: ('Ozone Concentration Measurement', 'OZCONC'),
121-
0x042A: ('PM2.5 Concentration Measurement', 'PMICONC'),
122-
0x042B: ('Formaldehyde Concentration Measurement', 'FLDCONC'),
123-
0x042C: ('PM1 Concentration Measurement', 'PMHCONC'),
124-
0x042D: ('PM10 Concentration Measurement', 'PMKCONC'),
125-
0x042E: ('Total Volatile Organic Compounds Concentration Measurement', 'TVOCCONC'),
126-
0x042F: ('Radon Concentration Measurement', 'RNCONC')}
127-
CONC_BASE_NAME = 'Concentration Measurement Clusters'
128-
RESOURCE_CLUSTERS = {0x0071: ('HEPA Filter Monitoring', 'HEPAFREMON'),
129-
0x0072: ('Activated Carbon Filter Monitoring', 'ACFREMON')}
130-
RESOURCE_BASE_NAME = 'Resource Monitoring Clusters'
131-
WATER_CLUSTER = {0x0405: ('Relative Humidity Measurement', 'RH')}
132-
WATER_BASE_NAME = 'Water Content Measurement Clusters'
133-
CLUSTER_ALIASES = {CONC_BASE_NAME: CONC_CLUSTERS, RESOURCE_BASE_NAME: RESOURCE_CLUSTERS, WATER_BASE_NAME: WATER_CLUSTER}
134-
135-
136-
def is_alias(id: uint):
137-
for base, alias in CLUSTER_ALIASES.items():
138-
if id in alias:
139-
return True
140-
return False
116+
# workaround for aliased clusters PICS not appearing in the xml. Remove this once https://github.com/csa-data-model/projects/issues/461 is addressed
117+
ALIAS_PICS = {0x040C: 'CMOCONC',
118+
0x040D: 'CDOCONC',
119+
0x0413: 'NDOCONC',
120+
0x0415: 'OZCONC',
121+
0x042A: 'PMICONC',
122+
0x042B: 'FLDCONC',
123+
0x042C: 'PMHCONC',
124+
0x042D: 'PMKCONC',
125+
0x042E: 'TVOCCONC',
126+
0x042F: 'RNCONC',
127+
0x0071: 'HEPAFREMON',
128+
0x0072: 'ACFREMON',
129+
0x0405: 'RH'}
141130

142131

143132
class ClusterParser:
144-
def __init__(self, cluster, cluster_id, name, is_alias):
133+
def __init__(self, cluster, cluster_id, name):
145134
self._problems: list[ProblemNotice] = []
146135
self._cluster = cluster
147136
self._cluster_id = cluster_id
148137
self._name = name
149-
self._is_alias = is_alias
150138

151139
self._derived = None
152140
try:
@@ -163,6 +151,9 @@ def __init__(self, cluster, cluster_id, name, is_alias):
163151
except (KeyError, StopIteration):
164152
self._pics = None
165153

154+
if self._cluster_id in ALIAS_PICS.keys():
155+
self._pics = ALIAS_PICS[cluster_id]
156+
166157
self.feature_elements = self.get_all_feature_elements()
167158
self.attribute_elements = self.get_all_attribute_elements()
168159
self.command_elements = self.get_all_command_elements()
@@ -282,12 +273,12 @@ def str_to_access_type(privilege_str: str) -> Clusters.AccessControl.Enums.Acces
282273
return Clusters.AccessControl.Enums.AccessControlEntryPrivilegeEnum.kUnknownEnumValue
283274

284275
if access_xml is None:
285-
# Derived and alias clusters can inherit their access from the base and that's fine, so don't add an error
276+
# Derived clusters can inherit their access from the base and that's fine, so don't add an error
286277
# Similarly, pure base clusters can have the access defined in the derived clusters. If neither has it defined,
287278
# we will determine this at the end when we put these together.
288279
# Things with deprecated conformance don't get an access element, and that is also fine.
289280
# If a device properly passes the conformance test, such elements are guaranteed not to appear on the device.
290-
if self._is_alias or self._derived is not None or is_disallowed(conformance):
281+
if self._derived is not None or is_disallowed(conformance):
291282
return (None, None, None)
292283

293284
location = self.get_location_from_element(element_xml)
@@ -421,31 +412,32 @@ def add_cluster_data_from_xml(xml: ElementTree.Element, clusters: dict[int, XmlC
421412
xml: XML element read from from the XML cluster file
422413
clusters: dict of id -> XmlCluster. This function will append new clusters as appropriate to this dict.
423414
pure_base_clusters: dict of base name -> XmlCluster. This data structure is used to hold pure base clusters that don't have
424-
an ID. This function will append new pure base clusters as approrpriate to this dict.
415+
an ID. This function will append new pure base clusters as appropriate to this dict.
425416
ids_by_name: dict of cluster name -> ID. This function will append new IDs as appropriate to this dict.
426417
problems: list of any problems encountered during spec parsing. This function will append problems as appropriate to this list.
427418
'''
428419
cluster = xml.iter('cluster')
429420
for c in cluster:
430-
name = c.attrib['name']
431-
if not c.attrib['id']:
432-
# Fully derived clusters have no id, but also shouldn't appear on a device.
433-
# We do need to keep them, though, because we need to update the derived
434-
# clusters. We keep them in a special dict by name, so they can be thrown
435-
# away later.
436-
cluster_id = None
437-
else:
438-
cluster_id = int(c.attrib['id'], 0)
439-
ids_by_name[name] = cluster_id
440-
441-
parser = ClusterParser(c, cluster_id, name, is_alias(cluster_id))
442-
new = parser.create_cluster()
443-
problems = problems + parser.get_problems()
444-
445-
if cluster_id:
446-
clusters[cluster_id] = new
447-
else:
448-
pure_base_clusters[name] = new
421+
ids = c.iter('clusterId')
422+
for id in ids:
423+
name = id.get('name')
424+
cluster_id = id.get('id')
425+
if cluster_id:
426+
cluster_id = int(id.get('id'), 0)
427+
ids_by_name[name] = cluster_id
428+
429+
parser = ClusterParser(c, cluster_id, name)
430+
new = parser.create_cluster()
431+
problems = problems + parser.get_problems()
432+
433+
if cluster_id:
434+
clusters[cluster_id] = new
435+
else:
436+
# Fully derived clusters have no id, but also shouldn't appear on a device.
437+
# We do need to keep them, though, because we need to update the derived
438+
# clusters. We keep them in a special dict by name, so they can be thrown
439+
# away later.
440+
pure_base_clusters[name] = new
449441

450442

451443
def check_clusters_for_unknown_commands(clusters: dict[int, XmlCluster], problems: list[ProblemNotice]):
@@ -489,15 +481,6 @@ def remove_problem(location: typing.Union[CommandPathLocation, FeaturePathLocati
489481

490482
combine_derived_clusters_with_base(clusters, pure_base_clusters, ids_by_name)
491483

492-
for alias_base_name, aliased_clusters in CLUSTER_ALIASES.items():
493-
for id, (alias_name, pics) in aliased_clusters.items():
494-
base = pure_base_clusters[alias_base_name]
495-
new = deepcopy(base)
496-
new.derived = alias_base_name
497-
new.name = alias_name
498-
new.pics = pics
499-
clusters[id] = new
500-
501484
# TODO: All these fixups should be removed BEFORE SVE if at all possible
502485
# Workaround for Color Control cluster - the spec uses a non-standard conformance. Set all to optional now, will need
503486
# to implement either arithmetic conformance handling (once spec changes land here) or specific test

0 commit comments

Comments
 (0)