| from dataclasses import dataclass | |
| from mobly import asserts | |
| from matter.testing.matter_testing import MatterBaseTest | |
| from matter.testing.spec_parsing import Clusters, XmlAttribute, build_xml_clusters, dm_from_spec_version | |
| # Maps a scenable attribute's spec data type to the typed value field of the | |
| # Scenes cluster AttributeValuePairStruct used to carry its value. | |
| _SCENE_VALUE_FIELDS = { | |
| "bool": "valueUnsigned8", | |
| "uint8": "valueUnsigned8", | |
| "percent": "valueUnsigned8", | |
| "uint16": "valueUnsigned16", | |
| "percent100ths": "valueUnsigned16", | |
| "uint24": "valueUnsigned32", | |
| "uint32": "valueUnsigned32", | |
| "uint40": "valueUnsigned64", | |
| "uint48": "valueUnsigned64", | |
| "uint56": "valueUnsigned64", | |
| "uint64": "valueUnsigned64", | |
| "map64": "valueUnsigned64", | |
| "int8": "valueSigned8", | |
| "int16": "valueSigned16", | |
| "int24": "valueSigned32", | |
| "int32": "valueSigned32", | |
| "int40": "valueSigned64", | |
| "int48": "valueSigned64", | |
| "int56": "valueSigned64", | |
| "int64": "valueSigned64", | |
| "temperature": "valueSigned16", | |
| # Will discard Scenable attributes of enum and map types for now | |
| # because their constraints are not defined in the datamodel xml | |
| # Additionally, their range of values might be feature dependent | |
| # "enum8": "valueUnsigned8", | |
| # "map8": "valueUnsigned8", | |
| # "enum16": "valueUnsigned16", | |
| # "map16": "valueUnsigned16", | |
| # "map32": "valueUnsigned32", | |
| # "enum32": "valueUnsigned32", | |
| } | |
| # Bit width and signedness of each AttributeValuePairStruct value field. | |
| _VALUE_FIELD_BITS = { | |
| "valueUnsigned8": (8, False), | |
| "valueUnsigned16": (16, False), | |
| "valueUnsigned32": (32, False), | |
| "valueUnsigned64": (64, False), | |
| "valueSigned8": (8, True), | |
| "valueSigned16": (16, True), | |
| "valueSigned32": (32, True), | |
| "valueSigned64": (64, True), | |
| } | |
| @dataclass | |
| class ScenableAttribute: | |
| """A concrete scenable attribute discovered on the DUT's scene endpoint.""" | |
| cluster: Clusters.ClusterObjects.Cluster | |
| attribute: Clusters.ClusterObjects.ClusterAttributeDescriptor | |
| xml_attribute: XmlAttribute | |
| value_field: str | |
| min_value: int = 0 | |
| max_value: int = 0 | |
| def _scene_value_field(datatype: str) -> str | None: | |
| """Returns the AttributeValuePairStruct value field for a spec data type, or None if unsupported.""" | |
| return _SCENE_VALUE_FIELDS.get(datatype.lower()) | |
| async def select_scenable_attribute(test: MatterBaseTest, scene_endpoint: int) -> ScenableAttribute: | |
| """Discovers a scenable attribute present on the scene endpoint. | |
| Cross-references the attributes the DUT actually exposes on the scene endpoint | |
| with the data model 'scene' (S) quality. | |
| Boolean attributes (e.g. OnOff) are preferred because two distinct valid values are | |
| always available and unambiguous. | |
| """ | |
| wildcard = await test.default_controller.Read(test.dut_node_id, [()]) | |
| attributes = wildcard.attributes | |
| spec_version = attributes[0][Clusters.BasicInformation][Clusters.BasicInformation.Attributes.SpecificationVersion] | |
| xml_clusters, _ = build_xml_clusters(dm_from_spec_version(spec_version)) | |
| endpoint_data = attributes.get(scene_endpoint) | |
| asserts.assert_true(endpoint_data is not None, f"Scene endpoint {scene_endpoint} not found on the DUT") | |
| fallback: ScenableAttribute | None = None | |
| for cluster, cluster_data in endpoint_data.items(): | |
| xml_cluster = xml_clusters.get(cluster.id) | |
| if xml_cluster is None: | |
| continue | |
| for attr_id in cluster_data.get(cluster.Attributes.AttributeList, []): | |
| xml_attribute = xml_cluster.attributes.get(attr_id) | |
| if xml_attribute is None or not xml_attribute.scene: | |
| continue | |
| value_field = _scene_value_field(xml_attribute.datatype) | |
| if value_field is None: | |
| continue | |
| try: | |
| candidate = ScenableAttribute( | |
| cluster=Clusters.ClusterObjects.ALL_CLUSTERS[cluster.id], | |
| attribute=Clusters.ClusterObjects.ALL_ATTRIBUTES[cluster.id][attr_id], | |
| xml_attribute=xml_attribute, | |
| value_field=value_field, | |
| ) | |
| except KeyError: | |
| continue | |
| # Boolean attributes are preferred because two distinct valid values are always available and unambiguous. | |
| if xml_attribute.datatype.lower() == "bool": | |
| _set_scenable_attribute_value_bounds_from_spec_contraints(candidate) | |
| return candidate | |
| if fallback is None: | |
| fallback = candidate | |
| asserts.assert_true(fallback is not None, f"No scenable attribute found on endpoint {scene_endpoint}") | |
| # There are two instances of scenable attributes which their contraints depends other attributes in their cluster | |
| # LevelControl currentLevel is bounded by the cluster's MinLevel and MaxLevel attributes | |
| # ColorControl ColorTemperatureMireds is bounded by the cluster's ColorTempPhysicalMinMireds and ColorTempPhysicalMaxMireds attributes | |
| if fallback.cluster == Clusters.LevelControl and fallback.attribute == Clusters.LevelControl.Attributes.CurrentLevel: | |
| min_level = await test.read_single_attribute_check_success( | |
| endpoint=scene_endpoint, cluster=Clusters.LevelControl, attribute=Clusters.LevelControl.Attributes.MinLevel | |
| ) | |
| max_level = await test.read_single_attribute_check_success( | |
| endpoint=scene_endpoint, cluster=Clusters.LevelControl, attribute=Clusters.LevelControl.Attributes.MaxLevel | |
| ) | |
| _set_scenable_attribute_value_bounds(fallback, int(min_level), int(max_level)) | |
| elif ( | |
| fallback.cluster == Clusters.ColorControl and fallback.attribute == Clusters.ColorControl.Attributes.ColorTemperatureMireds | |
| ): | |
| min_color_temp = await test.read_single_attribute_check_success( | |
| endpoint=scene_endpoint, | |
| cluster=Clusters.ColorControl, | |
| attribute=Clusters.ColorControl.Attributes.ColorTempPhysicalMinMireds, | |
| ) | |
| max_color_temp = await test.read_single_attribute_check_success( | |
| endpoint=scene_endpoint, | |
| cluster=Clusters.ColorControl, | |
| attribute=Clusters.ColorControl.Attributes.ColorTempPhysicalMaxMireds, | |
| ) | |
| _set_scenable_attribute_value_bounds(fallback, int(min_color_temp), int(max_color_temp)) | |
| else: | |
| _set_scenable_attribute_value_bounds_from_spec_contraints(fallback) | |
| return fallback | |
| def is_writable(scenable: ScenableAttribute) -> bool: | |
| return scenable.xml_attribute.write_access != Clusters.AccessControl.Enums.AccessControlEntryPrivilegeEnum.kUnknownEnumValue | |
| async def read_scenable_value(test: MatterBaseTest, scenable: ScenableAttribute, scene_endpoint: int) -> int: | |
| """Reads the current value of the selected scenable attribute as an int.""" | |
| value = await test.read_single_attribute_check_success( | |
| endpoint=scene_endpoint, cluster=scenable.cluster, attribute=scenable.attribute | |
| ) | |
| return int(value) | |
| def build_extension_fields(scenable: ScenableAttribute, value: int) -> list: | |
| """Builds the ExtensionFieldSetStructs setting the selected scenable attribute to value.""" | |
| pair = Clusters.ScenesManagement.Structs.AttributeValuePairStruct(attributeID=scenable.attribute.attribute_id) | |
| setattr(pair, scenable.value_field, int(value)) | |
| return [Clusters.ScenesManagement.Structs.ExtensionFieldSetStruct(clusterID=scenable.cluster.id, attributeValueList=[pair])] | |
| def _set_scenable_attribute_value_bounds_from_spec_contraints(scenable: ScenableAttribute): | |
| """Set the value constraints for the scenable attribute. | |
| Uses the spec constraints when available, otherwise uses the data type's | |
| representable range (excluding the maximum unsigned value, which is frequently reserved). | |
| """ | |
| if scenable.xml_attribute.datatype.lower() == "bool": | |
| lo, hi = 0, 1 | |
| else: | |
| bits, signed = _VALUE_FIELD_BITS[scenable.value_field] | |
| if signed: | |
| lo, hi = -(2 ** (bits - 1)), 2 ** (bits - 1) - 1 | |
| else: | |
| lo, hi = 0, 2**bits - 2 | |
| constraints = scenable.xml_attribute.constraints | |
| if constraints is not None: | |
| if isinstance(constraints.min_value, int): | |
| lo = constraints.min_value | |
| if isinstance(constraints.max_value, int): | |
| hi = constraints.max_value | |
| _set_scenable_attribute_value_bounds(scenable, lo, hi) | |
| def _set_scenable_attribute_value_bounds(scenable: ScenableAttribute, lo: int, hi: int): | |
| """Override the value constraints for the scenable attribute. | |
| Used when the Scenable attribute value bounds might differ from its spec constraints or type range | |
| e.g. LevelControl currentLevel is bounded by the cluster's MinLevel and MaxLevel attributes | |
| """ | |
| scenable.min_value, scenable.max_value = lo, hi | |
| def value_other_than(scenable: ScenableAttribute, value: int) -> int: | |
| """Returns an in-range value adjacent to (and different from) the given value within the attribute's value constraints.""" | |
| if value + 1 <= scenable.max_value: | |
| return value + 1 | |
| if value - 1 >= scenable.min_value: | |
| return value - 1 | |
| return scenable.min_value if value != scenable.min_value else scenable.max_value |