blob: b9a0fbf7fb42507d11cb5e05fa4bff15bf874763 [file] [log] [blame]
/**
* Copyright (c) 2024 Project CHIP Authors
*
* 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 "MTREndpointInfo_Internal.h"
#import "MTRAttributeTLVValueDecoder_Internal.h"
#import "MTRDeviceTypeRevision.h"
#import "MTRLogging_Internal.h"
#import "MTRStructsObjc.h"
#include <app-common/zap-generated/ids/Attributes.h>
#include <app-common/zap-generated/ids/Clusters.h>
#include <deque>
NS_ASSUME_NONNULL_BEGIN
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
enum class EndpointMark : uint8_t {
NotVisited = 0,
Visiting,
Visited,
ParentAssigned = NotVisited, // != Visited
};
MTR_DIRECT_MEMBERS
@implementation MTREndpointInfo {
EndpointId _endpointID;
EndpointMark _mark; // used by populateChildrenForEndpoints:
}
- (instancetype)initWithEndpointID:(NSNumber *)endpointID
deviceTypes:(NSArray<MTRDeviceTypeRevision *> *)deviceTypes
partsList:(NSArray<NSNumber *> *)partsList
{
self = [super init];
_endpointID = endpointID.unsignedShortValue;
_deviceTypes = [deviceTypes copy];
_partsList = [partsList copy];
_children = @[];
_mark = EndpointMark::NotVisited;
return self;
}
static NSString * const sEndpointIDCodingKey = @"id";
static NSString * const sDeviceTypesCodingKey = @"dt";
static NSString * const sPartsListCodingKey = @"pl";
static NSString * const sChildrenCodingKey = @"ch";
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
_endpointID = static_cast<EndpointId>([coder decodeIntegerForKey:sEndpointIDCodingKey]);
_deviceTypes = [coder decodeArrayOfObjectsOfClass:MTRDeviceTypeRevision.class forKey:sDeviceTypesCodingKey];
VerifyOrReturnValue(_deviceTypes != nil, nil);
_partsList = [coder decodeArrayOfObjectsOfClass:NSNumber.class forKey:sPartsListCodingKey];
VerifyOrReturnValue(_partsList != nil, nil);
_children = [coder decodeArrayOfObjectsOfClass:MTREndpointInfo.class forKey:sChildrenCodingKey];
VerifyOrReturnValue(_children != nil, nil);
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeInteger:_endpointID forKey:sEndpointIDCodingKey];
[coder encodeObject:_deviceTypes forKey:sDeviceTypesCodingKey];
[coder encodeObject:_partsList forKey:sPartsListCodingKey];
[coder encodeObject:_children forKey:sChildrenCodingKey];
}
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (id)copyWithZone:(nullable NSZone *)zone
{
return self; // no (externally) mutable state
}
- (NSUInteger)hash
{
return _endpointID;
}
- (BOOL)isEqual:(id)object
{
VerifyOrReturnValue([object class] == [self class], NO);
MTREndpointInfo * other = object;
VerifyOrReturnValue(_endpointID == other->_endpointID, NO);
VerifyOrReturnValue([_deviceTypes isEqual:other->_deviceTypes], NO);
VerifyOrReturnValue([_partsList isEqual:other->_partsList], NO);
// Children are derived from PartsLists, so we don't need to compare them.
// This avoids a lot recursive comparisons when comparing a dictionary of endpoints.
return YES;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ %u>", self.class, _endpointID];
}
- (NSNumber *)endpointID
{
return @(_endpointID);
}
+ (BOOL)populateChildrenForEndpoints:(NSDictionary<NSNumber *, MTREndpointInfo *> *)endpoints
{
// Populate the child list of each endpoint, ensuring no cycles (these are disallowed
// by the spec, but we need to ensure we don't create a retain cycle even under invalid
// input). Conservatively assume all endpoints use the Full-Family Pattern.
// Refer to "Endpoint Composition" in the Matter specification for details.
MTREndpointInfo * root = endpoints[@0];
if (root == nil) {
MTR_LOG_ERROR("Missing root endpoint, not populating endpoint hierarchy");
return NO;
}
// Perform a depth-first search with an explicit stack and create a list of endpoint
// IDs in reverse topological order. Note that endpoints start with _mark == NotVisited.
BOOL valid = YES;
std::deque<EndpointId> deque; // stack followed by sorted list
deque.emplace_front(root->_endpointID);
for (;;) {
EndpointId endpointID = deque.front();
MTREndpointInfo * endpoint = endpoints[@(endpointID)];
if (endpoint->_mark == EndpointMark::NotVisited) {
endpoint->_mark = EndpointMark::Visiting;
for (NSNumber * partNumber in endpoint->_partsList) {
MTREndpointInfo * descendant = endpoints[partNumber];
if (!descendant) {
MTR_LOG_ERROR("Warning: PartsList of endpoint %u references non-existant endpoint %u",
endpointID, partNumber.unsignedShortValue);
valid = NO;
} else if (descendant->_mark == EndpointMark::NotVisited) {
deque.emplace_front(descendant->_endpointID);
} else if (descendant->_mark == EndpointMark::Visiting) {
MTR_LOG_ERROR("Warning: Cyclic endpoint composition involving endpoints %u and %u",
descendant->_endpointID, endpointID);
valid = NO;
}
}
} else if (endpoint->_mark == EndpointMark::Visiting) {
endpoint->_mark = EndpointMark::Visited;
deque.pop_front(); // remove from stack
deque.emplace_back(endpointID); // add to sorted list
if (endpointID == root->_endpointID) {
break; // visited the root, DFS traversal done
}
} else /* endpoint->_mark == EndpointMark::Visited */ {
// Endpoints can be visited multiple times due to Full-Family
// ancestors like the root node, or in scenarios where an
// endpoint is erroneously in the PartsList of two separate
// branches of the tree. There is no easy way to distinguish
// these cases here, so we are not setting valid = NO.
deque.pop_front(); // nothing else to do
}
}
if (deque.size() != endpoints.count) {
MTR_LOG_ERROR("Warning: Not all endpoints are descendants of the root endpoint");
valid = NO;
}
// Now iterate over the endpoints in reverse topological order, i.e. bottom up. This means
// that we will visit children before parents, so the first time we see an endpoint in a
// PartsList we can assign it as a child of the endpoint we're processing, and we can be sure
// that this is the closest parent, not some higher ancestor using the Full-Family Pattern.
NSMutableArray<MTREndpointInfo *> * children = [[NSMutableArray alloc] init];
while (!deque.empty()) {
EndpointId endpointID = deque.front();
MTREndpointInfo * endpoint = endpoints[@(endpointID)];
deque.pop_front();
if (endpoint->_mark == EndpointMark::ParentAssigned) {
continue; // This endpoint is part of a cycle, don't populate its children.
}
[children removeAllObjects];
for (NSNumber * partNumber in endpoint->_partsList) {
MTREndpointInfo * descendant = endpoints[partNumber];
if (descendant != nil && descendant->_mark != EndpointMark::ParentAssigned) {
descendant->_mark = EndpointMark::ParentAssigned;
[children addObject:descendant];
}
}
endpoint->_children = [children copy];
}
root->_mark = EndpointMark::ParentAssigned;
return valid;
}
+ (NSDictionary<NSNumber *, MTREndpointInfo *> *)endpointsFromAttributeCache:(const ClusterStateCache *)cache
{
VerifyOrReturnValue(cache != nullptr, nil);
using namespace Descriptor::Attributes;
NSMutableDictionary<NSNumber *, MTREndpointInfo *> * endpoints = [[NSMutableDictionary alloc] init];
cache->ForEachAttribute(Descriptor::Id, [&](const ConcreteAttributePath & path) -> CHIP_ERROR {
VerifyOrReturnError(path.mAttributeId == DeviceTypeList::Id, CHIP_NO_ERROR);
CHIP_ERROR err = CHIP_NO_ERROR;
NSArray<MTRDescriptorClusterDeviceTypeStruct *> * deviceTypeList = MTRDecodeAttributeValue(path, *cache, &err);
if (!deviceTypeList) {
MTR_LOG_ERROR("Ignoring invalid DeviceTypeList for endpoint %u: %" CHIP_ERROR_FORMAT, path.mEndpointId, err.Format());
// proceed with deviceTypeList == nil, equivalent to an empty list
}
NSMutableArray * deviceTypes = [[NSMutableArray alloc] initWithCapacity:deviceTypeList.count];
for (MTRDescriptorClusterDeviceTypeStruct * deviceTypeStruct in deviceTypeList) {
MTRDeviceTypeRevision * type = [[MTRDeviceTypeRevision alloc] initWithDeviceTypeStruct:deviceTypeStruct];
if (!type) {
MTR_LOG_ERROR("Ignoring invalid device type 0x%x rev %u for endpoint %u",
deviceTypeStruct.deviceType.unsignedIntValue, deviceTypeStruct.revision.unsignedShortValue,
path.mEndpointId);
continue;
}
[deviceTypes addObject:type];
}
ConcreteAttributePath partsListPath(path.mEndpointId, path.mClusterId, PartsList::Id);
NSArray<NSNumber *> * partsList = MTRDecodeAttributeValue(partsListPath, *cache, &err);
if (!partsList) {
MTR_LOG_ERROR("Ignoring invalid PartsList for endpoint %u: %" CHIP_ERROR_FORMAT, path.mEndpointId, err.Format());
partsList = @[];
}
MTREndpointInfo * endpoint = [[MTREndpointInfo alloc] initWithEndpointID:@(path.mEndpointId)
deviceTypes:deviceTypes
partsList:partsList];
endpoints[endpoint.endpointID] = endpoint;
return CHIP_NO_ERROR;
});
if (endpoints.count > 0) {
[self populateChildrenForEndpoints:endpoints];
}
return [endpoints copy];
}
+ (Span<const AttributePathParams>)requiredAttributePaths
{
using namespace Descriptor::Attributes;
static constexpr AttributePathParams kPaths[] = {
AttributePathParams(Descriptor::Id, DeviceTypeList::Id),
AttributePathParams(Descriptor::Id, PartsList::Id),
};
return Span<const AttributePathParams>(kPaths);
}
@end
NS_ASSUME_NONNULL_END