diff --git a/examples/bridge-app/linux/Device.cpp b/examples/bridge-app/linux/Device.cpp index 53959cc5f919c0..e049e270c49f42 100644 --- a/examples/bridge-app/linux/Device.cpp +++ b/examples/bridge-app/linux/Device.cpp @@ -275,6 +275,17 @@ void DevicePowerSource::SetDescription(std::string aDescription) } } +void DevicePowerSource::SetEndpointList(std::vector aEndpointList) +{ + bool changed = aEndpointList != mEndpointList; + mEndpointList = aEndpointList; + + if (changed && mChanged_CB) + { + mChanged_CB(this, kChanged_EndpointList); + } +} + EndpointListInfo::EndpointListInfo(uint16_t endpointListId, std::string name, EndpointListTypeEnum type) { mEndpointListId = endpointListId; diff --git a/examples/bridge-app/linux/include/Device.h b/examples/bridge-app/linux/include/Device.h index 2b1c110c1b1ab3..d5c8434b29eda1 100644 --- a/examples/bridge-app/linux/include/Device.h +++ b/examples/bridge-app/linux/include/Device.h @@ -175,8 +175,9 @@ class DevicePowerSource : public Device public: enum Changed_t { - kChanged_BatLevel = kChanged_Last << 1, - kChanged_Description = kChanged_Last << 2, + kChanged_BatLevel = kChanged_Last << 1, + kChanged_Description = kChanged_Last << 2, + kChanged_EndpointList = kChanged_Last << 3, } Changed; DevicePowerSource(const char * szDeviceName, std::string szLocation, @@ -189,12 +190,14 @@ class DevicePowerSource : public Device void SetBatChargeLevel(uint8_t aBatChargeLevel); void SetDescription(std::string aDescription); + void SetEndpointList(std::vector mEndpointList); inline uint32_t GetFeatureMap() { return mFeatureMap.Raw(); }; inline uint8_t GetBatChargeLevel() { return mBatChargeLevel; }; inline uint8_t GetOrder() { return mOrder; }; inline uint8_t GetStatus() { return mStatus; }; inline std::string GetDescription() { return mDescription; }; + std::vector & GetEndpointList() { return mEndpointList; } private: void HandleDeviceChange(Device * device, Device::Changed_t changeMask); @@ -206,6 +209,8 @@ class DevicePowerSource : public Device std::string mDescription = "Primary Battery"; chip::BitFlags mFeatureMap; DeviceCallback_fn mChanged_CB; + // This is linux, vector is not going to kill us here and it's easier. Plus, post c++11, storage is contiguous with .data() + std::vector mEndpointList; }; class EndpointListInfo diff --git a/examples/bridge-app/linux/main.cpp b/examples/bridge-app/linux/main.cpp index d460836e8ca4f7..382ff7fe456de0 100644 --- a/examples/bridge-app/linux/main.cpp +++ b/examples/bridge-app/linux/main.cpp @@ -66,7 +66,8 @@ const int kDescriptorAttributeArraySize = 254; EndpointId gCurrentEndpointId; EndpointId gFirstDynamicEndpointId; -Device * gDevices[CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT]; +// Power source is on the same endpoint as the composed device +Device * gDevices[CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT + 1]; std::vector gRooms; std::vector gActions; @@ -180,26 +181,6 @@ Action action2(0x1002, "Turn On Room 2", Actions::ActionTypeEnum::kAutomation, 0 Action action3(0x1003, "Turn Off Room 1", Actions::ActionTypeEnum::kAutomation, 0xE003, 0x01, Actions::ActionStateEnum::kInactive, false); -// --------------------------------------------------------------------------- -// -// POWER SOURCE ENDPOINT: contains the following clusters: -// - Power Source -// - Descriptor -// - Bridged Device Basic Information - -DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(powerSourceAttrs) -DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::BatChargeLevel::Id, ENUM8, 1, 0), - DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::Order::Id, INT8U, 1, 0), - DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::Status::Id, ENUM8, 1, 0), - DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::Description::Id, CHAR_STRING, 32, 0), DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); - -DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(bridgedPowerSourceClusters) -DECLARE_DYNAMIC_CLUSTER(Descriptor::Id, descriptorAttrs, nullptr, nullptr), - DECLARE_DYNAMIC_CLUSTER(BridgedDeviceBasicInformation::Id, bridgedDeviceBasicAttrs, nullptr, nullptr), - DECLARE_DYNAMIC_CLUSTER(PowerSource::Id, powerSourceAttrs, nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER_LIST_END; - -DECLARE_DYNAMIC_ENDPOINT(bridgedPowerSourceEndpoint, bridgedPowerSourceClusters); - DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(tempSensorAttrs) DECLARE_DYNAMIC_ATTRIBUTE(TemperatureMeasurement::Attributes::MeasuredValue::Id, INT16S, 2, 0), /* Measured Value */ DECLARE_DYNAMIC_ATTRIBUTE(TemperatureMeasurement::Attributes::MinMeasuredValue::Id, INT16S, 2, 0), /* Min Measured Value */ @@ -229,18 +210,28 @@ DataVersion gTempSensor2DataVersions[ArraySize(bridgedTempSensorClusters)]; // COMPOSED DEVICE ENDPOINT: contains the following clusters: // - Descriptor // - Bridged Device Basic Information +// - Power source // Composed Device Configuration +DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(powerSourceAttrs) +DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::BatChargeLevel::Id, ENUM8, 1, 0), + DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::BatReplacementNeeded::Id, BOOLEAN, 1, 0), + DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::BatReplaceability::Id, ENUM8, 1, 0), + DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::Order::Id, INT8U, 1, 0), + DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::Status::Id, ENUM8, 1, 0), + DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::Description::Id, CHAR_STRING, 32, 0), + DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::EndpointList::Id, ARRAY, 0, 0), + DECLARE_DYNAMIC_ATTRIBUTE(PowerSource::Attributes::FeatureMap::Id, BITMAP32, 4, 0), DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); + DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(bridgedComposedDeviceClusters) DECLARE_DYNAMIC_CLUSTER(Descriptor::Id, descriptorAttrs, nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER(BridgedDeviceBasicInformation::Id, bridgedDeviceBasicAttrs, nullptr, nullptr), - DECLARE_DYNAMIC_CLUSTER_LIST_END; + DECLARE_DYNAMIC_CLUSTER(PowerSource::Id, powerSourceAttrs, nullptr, nullptr), DECLARE_DYNAMIC_CLUSTER_LIST_END; DECLARE_DYNAMIC_ENDPOINT(bridgedComposedDeviceEndpoint, bridgedComposedDeviceClusters); DataVersion gComposedDeviceDataVersions[ArraySize(bridgedComposedDeviceClusters)]; DataVersion gComposedTempSensor1DataVersions[ArraySize(bridgedTempSensorClusters)]; DataVersion gComposedTempSensor2DataVersions[ArraySize(bridgedTempSensorClusters)]; -DataVersion gComposedPowerSourceDataVersions[ArraySize(bridgedPowerSourceClusters)]; } // namespace @@ -254,7 +245,7 @@ DataVersion gComposedPowerSourceDataVersions[ArraySize(bridgedPowerSourceCluster #define ZCL_ON_OFF_CLUSTER_REVISION (4u) #define ZCL_TEMPERATURE_SENSOR_CLUSTER_REVISION (1u) #define ZCL_TEMPERATURE_SENSOR_FEATURE_MAP (0u) -#define ZCL_POWER_SOURCE_CLUSTER_REVISION (1u) +#define ZCL_POWER_SOURCE_CLUSTER_REVISION (2u) // --------------------------------------------------------------------------- @@ -425,6 +416,10 @@ void HandleDevicePowerSourceStatusChanged(DevicePowerSource * dev, DevicePowerSo { MatterReportingAttributeChangeCallback(dev->GetEndpointId(), PowerSource::Id, PowerSource::Attributes::Description::Id); } + if (itemChangedMask & DevicePowerSource::kChanged_EndpointList) + { + MatterReportingAttributeChangeCallback(dev->GetEndpointId(), PowerSource::Id, PowerSource::Attributes::EndpointList::Id); + } } void HandleDeviceTempSensorStatusChanged(DeviceTempSensor * dev, DeviceTempSensor::Changed_t itemChangedMask) @@ -518,45 +513,6 @@ EmberAfStatus HandleWriteOnOffAttribute(DeviceOnOff * dev, chip::AttributeId att return EMBER_ZCL_STATUS_SUCCESS; } -EmberAfStatus HandleReadPowerSourceAttribute(DevicePowerSource * dev, chip::AttributeId attributeId, uint8_t * buffer, - uint16_t maxReadLength) -{ - using namespace app::Clusters; - if ((attributeId == PowerSource::Attributes::BatChargeLevel::Id) && (maxReadLength == 1)) - { - *buffer = dev->GetBatChargeLevel(); - } - else if ((attributeId == PowerSource::Attributes::Order::Id) && (maxReadLength == 1)) - { - *buffer = dev->GetOrder(); - } - else if ((attributeId == PowerSource::Attributes::Status::Id) && (maxReadLength == 1)) - { - *buffer = dev->GetStatus(); - } - else if ((attributeId == PowerSource::Attributes::Description::Id) && (maxReadLength == 32)) - { - MutableByteSpan zclDescpitionSpan(buffer, maxReadLength); - MakeZclCharString(zclDescpitionSpan, dev->GetDescription().c_str()); - } - else if ((attributeId == PowerSource::Attributes::ClusterRevision::Id) && (maxReadLength == 2)) - { - uint16_t rev = ZCL_POWER_SOURCE_CLUSTER_REVISION; - memcpy(buffer, &rev, sizeof(rev)); - } - else if ((attributeId == PowerSource::Attributes::FeatureMap::Id) && (maxReadLength == 4)) - { - uint32_t featureMap = dev->GetFeatureMap(); - memcpy(buffer, &featureMap, sizeof(featureMap)); - } - else - { - return EMBER_ZCL_STATUS_FAILURE; - } - - return EMBER_ZCL_STATUS_SUCCESS; -} - EmberAfStatus HandleReadTempMeasurementAttribute(DeviceTempSensor * dev, chip::AttributeId attributeId, uint8_t * buffer, uint16_t maxReadLength) { @@ -615,11 +571,6 @@ EmberAfStatus emberAfExternalAttributeReadCallback(EndpointId endpoint, ClusterI { ret = HandleReadOnOffAttribute(static_cast(dev), attributeMetadata->attributeId, buffer, maxReadLength); } - else if (clusterId == chip::app::Clusters::PowerSource::Id) - { - ret = HandleReadPowerSourceAttribute(static_cast(dev), attributeMetadata->attributeId, buffer, - maxReadLength); - } else if (clusterId == TemperatureMeasurement::Id) { ret = HandleReadTempMeasurementAttribute(static_cast(dev), attributeMetadata->attributeId, buffer, @@ -630,6 +581,67 @@ EmberAfStatus emberAfExternalAttributeReadCallback(EndpointId endpoint, ClusterI return ret; } +class BridgedPowerSourceAttrAccess : public AttributeAccessInterface +{ +public: + // Register on all endpoints. + BridgedPowerSourceAttrAccess() : AttributeAccessInterface(Optional::Missing(), PowerSource::Id) {} + + CHIP_ERROR + Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override + { + uint16_t powerSourceDeviceIndex = CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT; + + if ((gDevices[powerSourceDeviceIndex] != nullptr)) + { + DevicePowerSource * dev = static_cast(gDevices[powerSourceDeviceIndex]); + if (aPath.mEndpointId != dev->GetEndpointId()) + { + return CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint); + } + switch (aPath.mAttributeId) + { + case PowerSource::Attributes::BatChargeLevel::Id: + aEncoder.Encode(dev->GetBatChargeLevel()); + break; + case PowerSource::Attributes::Order::Id: + aEncoder.Encode(dev->GetOrder()); + break; + case PowerSource::Attributes::Status::Id: + aEncoder.Encode(dev->GetStatus()); + break; + case PowerSource::Attributes::Description::Id: + aEncoder.Encode(chip::CharSpan(dev->GetDescription().c_str(), dev->GetDescription().size())); + break; + case PowerSource::Attributes::EndpointList::Id: { + std::vector & list = dev->GetEndpointList(); + DataModel::List dm_list(chip::Span(list.data(), list.size())); + aEncoder.Encode(dm_list); + break; + } + case PowerSource::Attributes::ClusterRevision::Id: + aEncoder.Encode(ZCL_POWER_SOURCE_CLUSTER_REVISION); + break; + case PowerSource::Attributes::FeatureMap::Id: + aEncoder.Encode(dev->GetFeatureMap()); + break; + + case PowerSource::Attributes::BatReplacementNeeded::Id: + aEncoder.Encode(false); + break; + case PowerSource::Attributes::BatReplaceability::Id: + aEncoder.Encode(PowerSource::BatReplaceabilityEnum::kNotReplaceable); + break; + default: + return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); + } + } + return CHIP_NO_ERROR; + } +}; + +BridgedPowerSourceAttrAccess gPowerAttrAccess; + EmberAfStatus emberAfExternalAttributeWriteCallback(EndpointId endpoint, ClusterId clusterId, const EmberAfAttributeMetadata * attributeMetadata, uint8_t * buffer) { @@ -732,12 +744,11 @@ void ApplicationInit() {} const EmberAfDeviceType gBridgedOnOffDeviceTypes[] = { { DEVICE_TYPE_LO_ON_OFF_LIGHT, DEVICE_VERSION_DEFAULT }, { DEVICE_TYPE_BRIDGED_NODE, DEVICE_VERSION_DEFAULT } }; -const EmberAfDeviceType gBridgedComposedDeviceTypes[] = { { DEVICE_TYPE_BRIDGED_NODE, DEVICE_VERSION_DEFAULT } }; +const EmberAfDeviceType gBridgedComposedDeviceTypes[] = { { DEVICE_TYPE_BRIDGED_NODE, DEVICE_VERSION_DEFAULT }, + { DEVICE_TYPE_POWER_SOURCE, DEVICE_VERSION_DEFAULT } }; const EmberAfDeviceType gComposedTempSensorDeviceTypes[] = { { DEVICE_TYPE_TEMP_SENSOR, DEVICE_VERSION_DEFAULT } }; -const EmberAfDeviceType gComposedPowerSourceDeviceTypes[] = { { DEVICE_TYPE_POWER_SOURCE, DEVICE_VERSION_DEFAULT } }; - const EmberAfDeviceType gBridgedTempSensorDeviceTypes[] = { { DEVICE_TYPE_TEMP_SENSOR, DEVICE_VERSION_DEFAULT }, { DEVICE_TYPE_BRIDGED_NODE, DEVICE_VERSION_DEFAULT } }; @@ -970,9 +981,6 @@ int main(int argc, char * argv[]) AddDeviceEndpoint(&ComposedTempSensor2, &bridgedTempSensorEndpoint, Span(gComposedTempSensorDeviceTypes), Span(gComposedTempSensor2DataVersions), ComposedDevice.GetEndpointId()); - AddDeviceEndpoint(&ComposedPowerSource, &bridgedPowerSourceEndpoint, - Span(gComposedPowerSourceDeviceTypes), - Span(gComposedPowerSourceDataVersions), ComposedDevice.GetEndpointId()); // Add 4 lights for the Action Clusters tests AddDeviceEndpoint(&ActionLight1, &bridgedLightEndpoint, Span(gBridgedOnOffDeviceTypes), @@ -983,6 +991,17 @@ int main(int argc, char * argv[]) Span(gActionLight3DataVersions), 1); AddDeviceEndpoint(&ActionLight4, &bridgedLightEndpoint, Span(gBridgedOnOffDeviceTypes), Span(gActionLight4DataVersions), 1); + + // Because the power source is on the same endpoint as the composed device, it needs to be explicitly added + gDevices[CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT] = &ComposedPowerSource; + // This provides power for the composed endpoint + std::vector endpointList; + endpointList.push_back(ComposedDevice.GetEndpointId()); + endpointList.push_back(ComposedTempSensor1.GetEndpointId()); + endpointList.push_back(ComposedTempSensor2.GetEndpointId()); + ComposedPowerSource.SetEndpointList(endpointList); + ComposedPowerSource.SetEndpointId(ComposedDevice.GetEndpointId()); + gRooms.push_back(&room1); gRooms.push_back(&room2); gRooms.push_back(&room3); @@ -1004,6 +1023,7 @@ int main(int argc, char * argv[]) // Run CHIP ApplicationInit(); + registerAttributeAccessOverride(&gPowerAttrAccess); chip::DeviceLayer::PlatformMgr().RunEventLoop(); return 0; diff --git a/src/python_testing/TC_DeviceBasicComposition.py b/src/python_testing/TC_DeviceBasicComposition.py index 1c143723b1c0d1..1d3f3fc944514e 100644 --- a/src/python_testing/TC_DeviceBasicComposition.py +++ b/src/python_testing/TC_DeviceBasicComposition.py @@ -251,16 +251,52 @@ def fail_current_test(self, msg: Optional[str] = None): asserts.fail(msg) # ======= START OF ACTUAL TESTS ======= - def test_endpoint_zero_present(self): - logging.info("Validating that the Root Node endpoint is present (EP0)") + def test_TC_SM_1_1(self): + ROOT_NODE_DEVICE_TYPE = 0x16 + self.print_step(1, "Perform a wildcard read of attributes on all endpoints - already done") + self.print_step(2, "Verify that endpoint 0 exists") if 0 not in self.endpoints: self.record_error(self.get_test_name(), location=AttributePathLocation(endpoint_id=0), problem="Did not find Endpoint 0.", spec_location="Endpoint Composition") self.fail_current_test() - def test_descriptor_present_on_each_endpoint(self): - logging.info("Validating each endpoint has a descriptor cluster") + self.print_step(3, "Verify that endpoint 0 descriptor cluster includes the root node device type") + if Clusters.Descriptor not in self.endpoints[0]: + self.record_error(self.get_test_name(), location=AttributePathLocation(endpoint_id=0), + problem="No descriptor cluster on Endpoint 0", spec_location="Root node device type") + self.fail_current_test() + + listed_device_types = [i.deviceType for i in self.endpoints[0] + [Clusters.Descriptor][Clusters.Descriptor.Attributes.DeviceTypeList]] + if ROOT_NODE_DEVICE_TYPE not in listed_device_types: + self.record_error(self.get_test_name(), location=AttributePathLocation(endpoint_id=0), + problem="Root node device type not listed on endpoint 0", spec_location="Root node device type") + self.fail_current_test() + self.print_step(4, "Verify that the root node device type does not appear in any of the non-zero endpoints") + for endpoint_id, endpoint in self.endpoints.items(): + if endpoint_id == 0: + continue + listed_device_types = [i.deviceType for i in endpoint[Clusters.Descriptor] + [Clusters.Descriptor.Attributes.DeviceTypeList]] + if ROOT_NODE_DEVICE_TYPE in listed_device_types: + self.record_error(self.get_test_name(), location=AttributePathLocation(endpoint_id=endpoint_id), + problem=f'Root node device type listed on endpoint {endpoint_id}', spec_location="Root node device type") + self.fail_current_test() + + self.print_step(5, "Verify the existence of all the root node clusters on EP0") + root = self.endpoints[0] + required_clusters = [Clusters.BasicInformation, Clusters.AccessControl, Clusters.GroupKeyManagement, + Clusters.GeneralCommissioning, Clusters.AdministratorCommissioning, Clusters.OperationalCredentials, Clusters.GeneralDiagnostics] + for c in required_clusters: + if c not in root: + self.record_error(self.get_test_name(), location=AttributePathLocation(endpoint_id=0), + problem=f'Root node does not contain required cluster {c}', spec_location="Root node device type") + self.fail_current_test() + + def test_DT_1_1(self): + self.print_step(1, "Perform a wildcard read of attributes on all endpoints - already done") + self.print_step(2, "Verify that each endpoint includes a descriptor cluster") success = True for endpoint_id, endpoint in self.endpoints.items(): has_descriptor = (Clusters.Descriptor in endpoint) @@ -273,8 +309,10 @@ def test_descriptor_present_on_each_endpoint(self): if not success: self.fail_current_test("At least one endpoint was missing the descriptor cluster.") - def test_global_attributes_present_on_each_cluster(self): - logging.info("Validating each cluster has the mandatory global attributes") + def test_IDM_10_1(self): + self.print_step(1, "Perform a wildcard read of attributes on all endpoints - already done") + + self.print_step(2, "Validate all global attributes are present") @dataclass class RequiredMandatoryAttribute: @@ -297,6 +335,7 @@ class RequiredMandatoryAttribute: validator=check_list_of_ints_in_range(0, 0xFFFF_FFFF)), ] + self.print_step(3, "Validate all reported attributes match AttributeList") success = True for endpoint_id, endpoint in self.endpoints_tlv.items(): for cluster_id, cluster in endpoint.items(): @@ -359,13 +398,37 @@ class RequiredMandatoryAttribute: # Warn only for now # TODO: Fail in the future continue + for attribute_id in cluster: + if attribute_id not in attribute_list: + attribute_string = self.cluster_mapper.get_attribute_string(cluster_id, attribute_id) + location = AttributePathLocation(endpoint_id, cluster_id, attribute_id) + self.record_error(self.get_test_name(), location=location, + problem=f'Found attribute {attribute_string} on {location.as_cluster_string(self.cluster_mapper)} not listed in attribute list', spec_location="AttributeList Attribute") + success = False if not success: self.fail_current_test( "At least one cluster was missing a mandatory global attribute or had differences between claimed attributes supported and actual.") - def test_all_attribute_strings_valid(self): - asserts.skip("TODO: Validate every string in the attribute tree is valid UTF-8 and has no nulls") + def test_IDM_11_1(self): + success = True + for endpoint_id, endpoint in self.endpoints_tlv.items(): + for cluster_id, cluster in endpoint.items(): + for attribute_id, attribute in cluster.items(): + if cluster_id not in Clusters.ClusterObjects.ALL_ATTRIBUTES or attribute_id not in Clusters.ClusterObjects.ALL_ATTRIBUTES[cluster_id]: + continue + if Clusters.ClusterObjects.ALL_ATTRIBUTES[cluster_id][attribute_id].attribute_type.Type is not str: + continue + try: + cluster[attribute_id].encode('utf-8', errors='strict') + except UnicodeError: + location = AttributePathLocation(endpoint_id, cluster_id, attribute_id) + attribute_string = self.cluster_mapper.get_attribute_string(cluster_id, attribute_id) + self.record_error(self.get_test_name( + ), location=location, problem=f'Attribute {attribute_string} on {location.as_cluster_string(self.cluster_mapper)} is invalid UTF-8', spec_location="Data types - Character String") + success = False + if not success: + self.fail_current_test("At least one attribute string was not valid UTF-8") def test_all_event_strings_valid(self): asserts.skip("TODO: Validate every string in the read events is valid UTF-8 and has no nulls") @@ -390,6 +453,100 @@ def test_all_endpoints_have_valid_composition(self): def test_topology_is_valid(self): asserts.skip("TODO: Make a test that verifies each endpoint only lists direct descendants, except Root Node and Aggregator endpoints that list all their descendants") + def test_TC_PS_3_1(self): + BRIDGED_NODE_DEVICE_TYPE_ID = 0x13 + success = True + self.print_step(1, "Wildcard read of device - already done") + + self.print_step(2, "Verify that all endpoints listed in the EndpointList are valid") + attribute_id = Clusters.PowerSource.Attributes.EndpointList.attribute_id + cluster_id = Clusters.PowerSource.id + attribute_string = self.cluster_mapper.get_attribute_string(cluster_id, attribute_id) + for endpoint_id, endpoint in self.endpoints.items(): + if Clusters.PowerSource not in endpoint: + continue + if Clusters.PowerSource.Attributes.EndpointList not in endpoint[Clusters.PowerSource]: + 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'Did not find {attribute_string} on {location.as_cluster_string(self.cluster_mapper)}', spec_location="EndpointList Attribute") + success = False + continue + + endpoint_list = endpoint[Clusters.PowerSource][Clusters.PowerSource.Attributes.EndpointList] + non_existent = set(endpoint_list) - set(self.endpoints.keys()) + if non_existent: + 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_string} lists a non-existent endpoint', spec_location="EndpointList Attribute") + success = False + + self.print_step(3, "Verify that all Bridged Node endpoint lists are correct") + device_types = {} + parts_list = {} + for endpoint_id, endpoint in self.endpoints.items(): + if Clusters.PowerSource not in endpoint or Clusters.PowerSource.Attributes.EndpointList not in endpoint[Clusters.PowerSource]: + continue + + def GetPartValidityProblem(endpoint): + if Clusters.Descriptor not in endpoint: + return "Missing cluster descriptor" + if Clusters.Descriptor.Attributes.PartsList not in endpoint[Clusters.Descriptor]: + return "Missing PartList in descriptor cluster" + if Clusters.Descriptor.Attributes.DeviceTypeList not in endpoint[Clusters.Descriptor]: + return "Missing DeviceTypeList in descriptor cluster" + return None + + problem = GetPartValidityProblem(endpoint) + if problem: + location = AttributePathLocation(endpoint_id=endpoint_id, cluster_id=Clusters.Descriptor.id, + attribute_id=Clusters.Descriptor.Attributes.PartsList.id) + self.record_error(self.get_test_name(), location=location, + problem=problem, spec_location="PartsList Attribute") + success = False + continue + + device_types[endpoint_id] = [i.deviceType for i in endpoint[Clusters.Descriptor] + [Clusters.Descriptor.Attributes.DeviceTypeList]] + parts_list[endpoint_id] = endpoint[Clusters.Descriptor][Clusters.Descriptor.Attributes.PartsList] + + bridged_nodes = [id for (id, dev_type) in device_types.items() if BRIDGED_NODE_DEVICE_TYPE_ID in dev_type] + + for endpoint_id in bridged_nodes: + if Clusters.PowerSource not in self.endpoints[endpoint_id]: + continue + # using a list because we do want to preserve duplicates and error on those. + desired_endpoint_list = parts_list[endpoint_id].copy() + desired_endpoint_list.append(endpoint_id) + desired_endpoint_list.sort() + ep_list = self.endpoints[endpoint_id][Clusters.PowerSource][Clusters.PowerSource.Attributes.EndpointList] + ep_list.sort() + if ep_list != desired_endpoint_list: + 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'Power source EndpointList on bridged node endpoint {endpoint_id} is not as expected. Desired: {desired_endpoint_list} Actual: {ep_list}', spec_location="EndpointList Attribute") + success = False + + self.print_step(4, "Verify that all Bridged Node children endpoint lists are correct") + children = [] + # note, this doesn't handle the full tree structure, single layer only + for endpoint_id in bridged_nodes: + children = children + parts_list[endpoint_id] + + for endpoint_id in children: + if Clusters.PowerSource not in self.endpoints[endpoint_id]: + continue + desired_endpoint_list = [endpoint_id] + ep_list = self.endpoints[endpoint_id][Clusters.PowerSource][Clusters.PowerSource.Attributes.EndpointList] + ep_list.sort() + if ep_list != desired_endpoint_list: + 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'Power source EndpointList on bridged child endpoint {endpoint_id} is not as expected. Desired: {desired_endpoint_list} Actual: {ep_list}', spec_location="EndpointList Attribute") + success = False + + if not success: + self.fail_current_test("power source EndpointList attribute is incorrect") + if __name__ == "__main__": default_matter_test_main()