blob: 914917a77adcdeea2e412fc40a7100885fef7a16 [file] [log] [blame]
/**
*
* Copyright (c) 2020-2023 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 <Matter/MTRClusterConstants.h>
#import <Matter/MTRDefines.h>
#import "MTRAttributeTLVValueDecoder_Internal.h"
#import "MTRBaseDevice_Internal.h"
#import "MTRBaseSubscriptionCallback.h"
#import "MTRCallbackBridgeBase.h"
#import "MTRCluster.h"
#import "MTRClusterStateCacheContainer_Internal.h"
#import "MTRCluster_Internal.h"
#import "MTRDevice_Internal.h"
#import "MTRError_Internal.h"
#import "MTREventTLVValueDecoder_Internal.h"
#import "MTRFramework.h"
#import "MTRLogging_Internal.h"
#import "MTRMetricKeys.h"
#import "MTRSetupPayload_Internal.h"
#import "NSDataSpanConversion.h"
#import "NSStringSpanConversion.h"
#import "zap-generated/MTRCommandPayloads_Internal.h"
#include "app/ConcreteAttributePath.h"
#include "app/ConcreteCommandPath.h"
#include "app/ConcreteEventPath.h"
#include "app/StatusResponse.h"
#include "lib/core/CHIPError.h"
#include "lib/core/DataModelTypes.h"
#include <app/AttributePathParams.h>
#include <app/BufferedReadCallback.h>
#include <app/ClusterStateCache.h>
#include <app/InteractionModelEngine.h>
#include <app/ReadClient.h>
#include <controller/CommissioningWindowOpener.h>
#include <controller/ReadInteraction.h>
#include <controller/WriteInteraction.h>
#include <crypto/CHIPCryptoPAL.h>
#include <setup_payload/SetupPayload.h>
#include <system/SystemClock.h>
#include <memory>
using namespace chip;
using namespace chip::app;
using namespace chip::Protocols::InteractionModel;
using chip::Optional;
using chip::SessionHandle;
using chip::Messaging::ExchangeManager;
using namespace chip::Tracing::DarwinFramework;
NSString * const MTRAttributePathKey = @"attributePath";
NSString * const MTRCommandPathKey = @"commandPath";
NSString * const MTREventPathKey = @"eventPath";
NSString * const MTRDataKey = @"data";
NSString * const MTRErrorKey = @"error";
NSString * const MTRTypeKey = @"type";
NSString * const MTRValueKey = @"value";
NSString * const MTRContextTagKey = @"contextTag";
NSString * const MTRSignedIntegerValueType = @"SignedInteger";
NSString * const MTRUnsignedIntegerValueType = @"UnsignedInteger";
NSString * const MTRBooleanValueType = @"Boolean";
NSString * const MTRUTF8StringValueType = @"UTF8String";
NSString * const MTROctetStringValueType = @"OctetString";
NSString * const MTRFloatValueType = @"Float";
NSString * const MTRDoubleValueType = @"Double";
NSString * const MTRNullValueType = @"Null";
NSString * const MTRStructureValueType = @"Structure";
NSString * const MTRArrayValueType = @"Array";
NSString * const MTREventNumberKey = @"eventNumber";
NSString * const MTREventPriorityKey = @"eventPriority";
NSString * const MTREventTimeTypeKey = @"eventTimeType";
NSString * const MTREventSystemUpTimeKey = @"eventSystemUpTime";
NSString * const MTREventTimestampDateKey = @"eventTimestampDate";
NSString * const MTREventIsHistoricalKey = @"eventIsHistorical";
class MTRDataValueDictionaryCallbackBridge;
@interface MTRReadClientContainer : NSObject
@property (nonatomic, readwrite) app::ReadClient * readClientPtr;
@property (nonatomic, readwrite) app::AttributePathParams * pathParams;
@property (nonatomic, readwrite) app::EventPathParams * eventPathParams;
@property (nonatomic, readwrite) uint64_t deviceID;
- (void)onDone;
@end
static NSMutableDictionary<NSNumber *, NSMutableArray<MTRReadClientContainer *> *> * readClientContainers;
static NSLock * readClientContainersLock;
static void InitializeReadClientContainers()
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
readClientContainers = [NSMutableDictionary dictionary];
readClientContainersLock = [[NSLock alloc] init];
});
}
static void AddReadClientContainer(uint64_t deviceId, MTRReadClientContainer * container)
{
InitializeReadClientContainers();
NSNumber * key = [NSNumber numberWithUnsignedLongLong:deviceId];
[readClientContainersLock lock];
if (!readClientContainers[key]) {
readClientContainers[key] = [NSMutableArray array];
}
[readClientContainers[key] addObject:container];
[readClientContainersLock unlock];
}
static void ReinstateReadClientList(NSMutableArray<MTRReadClientContainer *> * readClientList, NSNumber * key,
dispatch_queue_t queue, dispatch_block_t _Nullable completion)
{
[readClientContainersLock lock];
auto existingList = readClientContainers[key];
if (existingList) {
[existingList addObjectsFromArray:readClientList];
} else {
readClientContainers[key] = readClientList;
}
[readClientContainersLock unlock];
if (completion) {
dispatch_async(queue, completion);
}
}
static void PurgeReadClientContainers(
MTRDeviceController * controller, uint64_t deviceId, dispatch_queue_t queue, void (^_Nullable completion)(void))
{
InitializeReadClientContainers();
NSMutableArray<MTRReadClientContainer *> * listToDelete;
NSNumber * key = [NSNumber numberWithUnsignedLongLong:deviceId];
[readClientContainersLock lock];
listToDelete = readClientContainers[key];
[readClientContainers removeObjectForKey:key];
[readClientContainersLock unlock];
// Destroy read clients in the work queue
[controller
asyncDispatchToMatterQueue:^() {
for (MTRReadClientContainer * container in listToDelete) {
if (container.readClientPtr) {
Platform::Delete(container.readClientPtr);
container.readClientPtr = nullptr;
}
if (container.pathParams) {
static_assert(std::is_trivially_destructible<AttributePathParams>::value,
"AttributePathParams destructors won't get run");
Platform::MemoryFree(container.pathParams);
container.pathParams = nullptr;
}
if (container.eventPathParams) {
static_assert(
std::is_trivially_destructible<EventPathParams>::value, "EventPathParams destructors won't get run");
Platform::MemoryFree(container.eventPathParams);
container.eventPathParams = nullptr;
}
}
[listToDelete removeAllObjects];
if (completion) {
dispatch_async(queue, completion);
}
}
errorHandler:^(NSError * error) {
// Can't delete things. Just put them back, and hope we
// can delete them later.
ReinstateReadClientList(listToDelete, key, queue, completion);
}];
}
static void PurgeCompletedReadClientContainers(uint64_t deviceId)
{
InitializeReadClientContainers();
NSNumber * key = [NSNumber numberWithUnsignedLongLong:deviceId];
[readClientContainersLock lock];
NSMutableArray<MTRReadClientContainer *> * array = readClientContainers[key];
NSUInteger i = 0;
while (i < [array count]) {
if (array[i].readClientPtr == nullptr) {
[array removeObjectAtIndex:i];
continue;
}
i++;
}
[readClientContainersLock unlock];
}
#ifdef DEBUG
// This function is for unit testing only. This function closes all read clients.
static void CauseReadClientFailure(
MTRDeviceController * controller, uint64_t deviceId, dispatch_queue_t queue, void (^_Nullable completion)(void))
{
InitializeReadClientContainers();
NSMutableArray<MTRReadClientContainer *> * listToFail;
NSNumber * key = [NSNumber numberWithUnsignedLongLong:deviceId];
[readClientContainersLock lock];
listToFail = readClientContainers[key];
[readClientContainers removeObjectForKey:key];
[readClientContainersLock unlock];
[controller
asyncDispatchToMatterQueue:^() {
for (MTRReadClientContainer * container in listToFail) {
// Send auto resubscribe request again by read clients, which must fail.
chip::app::ReadPrepareParams readParams;
if (container.readClientPtr) {
container.readClientPtr->SendAutoResubscribeRequest(std::move(readParams));
}
}
if (completion) {
dispatch_async(queue, completion);
}
}
errorHandler:^(NSError * error) {
// Can't fail things. Just put them back.
ReinstateReadClientList(listToFail, key, queue, completion);
}];
}
#endif
static bool CheckMemberOfType(NSDictionary<NSString *, id> * responseValue, NSString * memberName, Class expectedClass,
NSString * errorMessage, NSError * __autoreleasing * error);
static void LogStringAndReturnError(NSString * errorStr, CHIP_ERROR errorCode, NSError * __autoreleasing * error);
static void LogStringAndReturnError(NSString * errorStr, MTRErrorCode errorCode, NSError * __autoreleasing * error);
@implementation MTRReadClientContainer
- (void)onDone
{
if (_readClientPtr) {
Platform::Delete(_readClientPtr);
_readClientPtr = nullptr;
}
if (_pathParams) {
static_assert(std::is_trivially_destructible<AttributePathParams>::value, "AttributePathParams destructors won't get run");
Platform::MemoryFree(_pathParams);
_pathParams = nullptr;
}
if (_eventPathParams) {
static_assert(std::is_trivially_destructible<EventPathParams>::value, "EventPathParams destructors won't get run");
Platform::MemoryFree(_eventPathParams);
_eventPathParams = nullptr;
}
PurgeCompletedReadClientContainers(_deviceID);
}
- (void)dealloc
{
if (_readClientPtr) {
Platform::Delete(_readClientPtr);
_readClientPtr = nullptr;
}
if (_pathParams) {
static_assert(std::is_trivially_destructible<AttributePathParams>::value, "AttributePathParams destructors won't get run");
Platform::MemoryFree(_pathParams);
_pathParams = nullptr;
}
if (_eventPathParams) {
static_assert(std::is_trivially_destructible<EventPathParams>::value, "EventPathParams destructors won't get run");
Platform::MemoryFree(_eventPathParams);
_eventPathParams = nullptr;
}
}
@end
@implementation MTRBaseDevice
- (instancetype)initWithPASEDevice:(chip::DeviceProxy *)device controller:(MTRDeviceController *)controller
{
if (self = [super init]) {
_isPASEDevice = YES;
_nodeID = device->GetDeviceId();
_deviceController = controller;
}
return self;
}
- (instancetype)initWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
{
if (self = [super init]) {
_isPASEDevice = NO;
_nodeID = nodeID.unsignedLongLongValue;
_deviceController = controller;
}
return self;
}
+ (MTRBaseDevice *)deviceWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
{
// Indirect through the controller to give it a chance to create an
// MTRBaseDeviceOverXPC instead of just an MTRBaseDevice.
return [controller baseDeviceForNodeID:nodeID];
}
- (MTRTransportType)sessionTransportType
{
return [self.deviceController sessionTransportTypeForDevice:self];
}
- (void)invalidateCASESession
{
if (self.isPASEDevice) {
return;
}
[self.deviceController invalidateCASESessionForNode:self.nodeID];
}
namespace {
class SubscriptionCallback final : public MTRBaseSubscriptionCallback {
public:
SubscriptionCallback(DataReportCallback attributeReportCallback, DataReportCallback eventReportCallback,
ErrorCallback errorCallback, MTRDeviceResubscriptionScheduledHandler _Nullable resubscriptionScheduledHandler,
MTRSubscriptionEstablishedHandler _Nullable subscriptionEstablishedHandler, OnDoneHandler _Nullable onDoneHandler)
: MTRBaseSubscriptionCallback(attributeReportCallback, eventReportCallback, errorCallback, resubscriptionScheduledHandler,
subscriptionEstablishedHandler, onDoneHandler)
{
}
void OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus) override;
void OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus) override;
};
} // anonymous namespace
- (void)subscribeWithQueue:(dispatch_queue_t)queue
params:(MTRSubscribeParams *)params
clusterStateCacheContainer:(MTRClusterStateCacheContainer * _Nullable)clusterStateCacheContainer
attributeReportHandler:(MTRDeviceReportHandler _Nullable)attributeReportHandler
eventReportHandler:(MTRDeviceReportHandler _Nullable)eventReportHandler
errorHandler:(void (^)(NSError * error))errorHandler
subscriptionEstablished:(MTRSubscriptionEstablishedHandler _Nullable)subscriptionEstablished
resubscriptionScheduled:(MTRDeviceResubscriptionScheduledHandler _Nullable)resubscriptionScheduled
{
if (self.isPASEDevice) {
// We don't support subscriptions over PASE.
dispatch_async(queue, ^{
errorHandler([MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]);
});
return;
}
// Copy params before going async.
params = [params copy];
[self.deviceController getSessionForNode:self.nodeID
completion:^(ExchangeManager * _Nullable exchangeManager, const Optional<SessionHandle> & session,
NSError * _Nullable error) {
if (error != nil) {
dispatch_async(queue, ^{
errorHandler(error);
});
return;
}
// Wildcard endpoint, cluster, attribute, event.
auto attributePath = std::make_unique<AttributePathParams>();
auto eventPath = std::make_unique<EventPathParams>();
eventPath->mIsUrgentEvent = params.reportEventsUrgently;
ReadPrepareParams readParams(session.Value());
[params toReadPrepareParams:readParams];
readParams.mpAttributePathParamsList = attributePath.get();
readParams.mAttributePathParamsListSize = 1;
readParams.mpEventPathParamsList = eventPath.get();
readParams.mEventPathParamsListSize = 1;
std::unique_ptr<ClusterStateCache> clusterStateCache;
ReadClient::Callback * callbackForReadClient = nullptr;
OnDoneHandler onDoneHandler = nil;
if (clusterStateCacheContainer) {
__weak MTRClusterStateCacheContainer * weakPtr = clusterStateCacheContainer;
onDoneHandler = ^{
// This, like all manipulation of cppClusterStateCache, needs to run on the Matter
// queue.
MTRClusterStateCacheContainer * container = weakPtr;
if (container) {
container.cppClusterStateCache = nullptr;
container.baseDevice = nil;
}
};
}
auto callback = std::make_unique<SubscriptionCallback>(
^(NSArray * value) {
dispatch_async(queue, ^{
if (attributeReportHandler != nil) {
attributeReportHandler(value);
}
});
},
^(NSArray * value) {
dispatch_async(queue, ^{
if (eventReportHandler != nil) {
eventReportHandler(value);
}
});
},
^(NSError * error) {
dispatch_async(queue, ^{
errorHandler(error);
});
},
^(NSError * error, NSNumber * resubscriptionDelay) {
dispatch_async(queue, ^{
if (resubscriptionScheduled != nil) {
resubscriptionScheduled(error, resubscriptionDelay);
}
});
},
^(void) {
dispatch_async(queue, ^{
if (subscriptionEstablished != nil) {
subscriptionEstablished();
}
});
},
onDoneHandler);
if (clusterStateCacheContainer) {
clusterStateCache = std::make_unique<ClusterStateCache>(*callback.get());
callbackForReadClient = &clusterStateCache->GetBufferedCallback();
} else {
callbackForReadClient = &callback->GetBufferedCallback();
}
auto readClient = std::make_unique<ReadClient>(InteractionModelEngine::GetInstance(),
exchangeManager, *callbackForReadClient, ReadClient::InteractionType::Subscribe);
CHIP_ERROR err;
if (!params.resubscribeAutomatically) {
err = readClient->SendRequest(readParams);
} else {
// SendAutoResubscribeRequest cleans up the params, even on failure.
attributePath.release();
eventPath.release();
err = readClient->SendAutoResubscribeRequest(std::move(readParams));
}
if (err != CHIP_NO_ERROR) {
dispatch_async(queue, ^{
errorHandler([MTRError errorForCHIPErrorCode:err]);
});
return;
}
if (clusterStateCacheContainer) {
clusterStateCacheContainer.cppClusterStateCache = clusterStateCache.get();
// ClusterStateCache will be deleted when OnDone is called.
callback->AdoptClusterStateCache(std::move(clusterStateCache));
clusterStateCacheContainer.baseDevice = self;
}
// Callback and ReadClient will be deleted when OnDone is called.
callback->AdoptReadClient(std::move(readClient));
callback.release();
}];
}
static NSDictionary<NSString *, id> * _MakeDataValueDictionary(NSString * type, id _Nullable value, NSNumber * _Nullable dataVersion)
{
if (value && dataVersion) {
return @ { MTRTypeKey : type, MTRValueKey : value, MTRDataVersionKey : dataVersion };
} else if (value) {
return @ { MTRTypeKey : type, MTRValueKey : value };
} else if (dataVersion) {
return @ { MTRTypeKey : type, MTRDataVersionKey : dataVersion };
} else {
return @ { MTRTypeKey : type };
}
}
// Convert TLV data into data-value dictionary as described in MTRDeviceResponseHandler
NSDictionary<NSString *, id> * _Nullable MTRDecodeDataValueDictionaryFromCHIPTLV(chip::TLV::TLVReader * data, NSNumber * dataVersion)
{
chip::TLV::TLVType dataTLVType = data->GetType();
switch (dataTLVType) {
case chip::TLV::kTLVType_SignedInteger: {
int64_t val;
CHIP_ERROR err = data->Get(val);
if (err != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error(%s): TLV signed integer decoding failed", chip::ErrorStr(err));
return nil;
}
return _MakeDataValueDictionary(MTRSignedIntegerValueType, @(val), dataVersion);
}
case chip::TLV::kTLVType_UnsignedInteger: {
uint64_t val;
CHIP_ERROR err = data->Get(val);
if (err != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error(%s): TLV unsigned integer decoding failed", chip::ErrorStr(err));
return nil;
}
return _MakeDataValueDictionary(MTRUnsignedIntegerValueType, @(val), dataVersion);
}
case chip::TLV::kTLVType_Boolean: {
bool val;
CHIP_ERROR err = data->Get(val);
if (err != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error(%s): TLV boolean decoding failed", chip::ErrorStr(err));
return nil;
}
return _MakeDataValueDictionary(MTRBooleanValueType, @(val), dataVersion);
}
case chip::TLV::kTLVType_FloatingPointNumber: {
// Try float first
float floatValue;
CHIP_ERROR err = data->Get(floatValue);
if (err == CHIP_NO_ERROR) {
return _MakeDataValueDictionary(MTRFloatValueType, @(floatValue), dataVersion);
}
double val;
err = data->Get(val);
if (err != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error(%s): TLV floating point decoding failed", chip::ErrorStr(err));
return nil;
}
return _MakeDataValueDictionary(MTRDoubleValueType, @(val), dataVersion);
}
case chip::TLV::kTLVType_UTF8String: {
CharSpan stringValue;
CHIP_ERROR err = data->Get(stringValue);
if (err != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error(%s): TLV UTF8String decoding failed", chip::ErrorStr(err));
return nil;
}
NSString * stringObj = AsString(stringValue);
if (stringObj == nil) {
MTR_LOG_ERROR("Error(%s): TLV UTF8String value is not actually UTF-8", err.AsString());
return nil;
}
return _MakeDataValueDictionary(MTRUTF8StringValueType, stringObj, dataVersion);
}
case chip::TLV::kTLVType_ByteString: {
ByteSpan bytesValue;
CHIP_ERROR err = data->Get(bytesValue);
if (err != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error(%s): TLV ByteString decoding failed", chip::ErrorStr(err));
return nil;
}
return _MakeDataValueDictionary(MTROctetStringValueType, AsData(bytesValue), dataVersion);
}
case chip::TLV::kTLVType_Null: {
return _MakeDataValueDictionary(MTRNullValueType, nil, dataVersion);
}
case chip::TLV::kTLVType_Structure:
case chip::TLV::kTLVType_Array: {
NSString * typeName;
switch (dataTLVType) {
case chip::TLV::kTLVType_Structure:
typeName = MTRStructureValueType;
break;
case chip::TLV::kTLVType_Array:
typeName = MTRArrayValueType;
break;
default:
typeName = @"Unsupported";
break;
}
chip::TLV::TLVType tlvType;
CHIP_ERROR err = data->EnterContainer(tlvType);
if (err != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error(%s): TLV container entering failed", chip::ErrorStr(err));
return nil;
}
NSMutableArray * array = [[NSMutableArray alloc] init];
while ((err = data->Next()) == CHIP_NO_ERROR) {
chip::TLV::Tag tag = data->GetTag();
id value = MTRDecodeDataValueDictionaryFromCHIPTLV(data);
if (value == nullptr) {
MTR_LOG_ERROR("Error when decoding TLV container of type %s", typeName.UTF8String);
return nil;
}
NSMutableDictionary * arrayElement = [NSMutableDictionary dictionary];
[arrayElement setObject:value forKey:MTRDataKey];
if (dataTLVType == chip::TLV::kTLVType_Structure) {
uint64_t tagNum;
if (IsContextTag(tag)) {
tagNum = TagNumFromTag(tag);
} else if (IsProfileTag(tag)) {
uint64_t profile = ProfileIdFromTag(tag);
tagNum = (profile << kProfileIdShift) | TagNumFromTag(tag);
} else {
MTR_LOG_ERROR("Skipping unknown tag type when decoding TLV structure.");
continue;
}
[arrayElement setObject:[NSNumber numberWithUnsignedLongLong:tagNum] forKey:MTRContextTagKey];
}
[array addObject:arrayElement];
}
if (err != CHIP_END_OF_TLV) {
MTR_LOG_ERROR("Error(%s): TLV container decoding failed", chip::ErrorStr(err));
return nil;
}
err = data->ExitContainer(tlvType);
if (err != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error(%s): TLV container exiting failed", chip::ErrorStr(err));
return nil;
}
return _MakeDataValueDictionary(typeName, array, dataVersion);
}
default:
MTR_LOG_ERROR("Error: Unsupported TLV type for conversion: %u", (unsigned) data->GetType());
return nil;
}
}
static CHIP_ERROR MTREncodeTLVFromDataValueDictionary(id object, chip::TLV::TLVWriter & writer, chip::TLV::Tag tag)
{
if (![object isKindOfClass:[NSDictionary class]]) {
MTR_LOG_ERROR("Error: Unsupported object to encode: %@", [object class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
NSString * typeName = ((NSDictionary *) object)[MTRTypeKey];
id value = ((NSDictionary *) object)[MTRValueKey];
if (!typeName) {
MTR_LOG_ERROR("Error: Object to encode is corrupt");
return CHIP_ERROR_INVALID_ARGUMENT;
}
if ([typeName isEqualToString:MTRSignedIntegerValueType]) {
if (![value isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt signed integer type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
return writer.Put(tag, [value longLongValue]);
}
if ([typeName isEqualToString:MTRUnsignedIntegerValueType]) {
if (![value isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt unsigned integer type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
return writer.Put(tag, [value unsignedLongLongValue]);
}
if ([typeName isEqualToString:MTRBooleanValueType]) {
if (![value isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt boolean type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
return writer.Put(tag, static_cast<bool>([value boolValue]));
}
if ([typeName isEqualToString:MTRFloatValueType]) {
if (![value isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt float type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
return writer.Put(tag, [value floatValue]);
}
if ([typeName isEqualToString:MTRDoubleValueType]) {
if (![value isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt double type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
return writer.Put(tag, [value doubleValue]);
}
if ([typeName isEqualToString:MTRNullValueType]) {
return writer.PutNull(tag);
}
if ([typeName isEqualToString:MTRUTF8StringValueType]) {
if (![value isKindOfClass:[NSString class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt UTF8 string type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
return writer.PutString(tag, [value UTF8String]);
}
if ([typeName isEqualToString:MTROctetStringValueType]) {
if (![value isKindOfClass:[NSData class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt octet string type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
return writer.Put(tag, chip::ByteSpan(static_cast<const uint8_t *>([value bytes]), [value length]));
}
if ([typeName isEqualToString:MTRStructureValueType]) {
if (![value isKindOfClass:[NSArray class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt structure type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
TLV::TLVType outer;
ReturnErrorOnFailure(writer.StartContainer(tag, chip::TLV::kTLVType_Structure, outer));
for (id element in value) {
if (![element isKindOfClass:[NSDictionary class]]) {
MTR_LOG_ERROR("Error: Structure element to encode has corrupt type: %@", [element class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
id elementTag = element[MTRContextTagKey];
id elementValue = element[MTRDataKey];
if (!elementTag || !elementValue) {
MTR_LOG_ERROR("Error: Structure element to encode has corrupt value: %@", element);
return CHIP_ERROR_INVALID_ARGUMENT;
}
if (![elementTag isKindOfClass:NSNumber.class]) {
MTR_LOG_ERROR("Error: Structure element to encode has corrupt tag type: %@", [elementTag class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
// Our tag might actually be a profile tag.
uint64_t tagValue = [elementTag unsignedLongLongValue];
TLV::Tag tag;
if (tagValue > UINT8_MAX) {
tag = TLV::ProfileTag(tagValue >> kProfileIdShift,
(tagValue & ((1ull << kProfileIdShift) - 1)));
} else {
tag = TLV::ContextTag(static_cast<uint8_t>(tagValue));
}
ReturnErrorOnFailure(
MTREncodeTLVFromDataValueDictionary(elementValue, writer, tag));
}
ReturnErrorOnFailure(writer.EndContainer(outer));
return CHIP_NO_ERROR;
}
if ([typeName isEqualToString:MTRArrayValueType]) {
if (![value isKindOfClass:[NSArray class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt array type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
TLV::TLVType outer;
ReturnErrorOnFailure(writer.StartContainer(tag, chip::TLV::kTLVType_Array, outer));
for (id element in value) {
if (![element isKindOfClass:[NSDictionary class]]) {
MTR_LOG_ERROR("Error: Array element to encode has corrupt type: %@", [element class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
id elementValue = element[MTRDataKey];
if (!elementValue) {
MTR_LOG_ERROR("Error: Array element to encode has corrupt value: %@", element);
return CHIP_ERROR_INVALID_ARGUMENT;
}
ReturnErrorOnFailure(MTREncodeTLVFromDataValueDictionary(elementValue, writer, chip::TLV::AnonymousTag()));
}
ReturnErrorOnFailure(writer.EndContainer(outer));
return CHIP_NO_ERROR;
}
MTR_LOG_ERROR("Error: Unsupported type to encode: %@", typeName);
return CHIP_ERROR_INVALID_ARGUMENT;
}
NSData * _Nullable MTREncodeTLVFromDataValueDictionary(NSDictionary<NSString *, id> * value, NSError * __autoreleasing * error)
{
// A single data item cannot be bigger than a packet, so just use 1200 bytes
// as the max size of our buffer. This assumes that lists will not be
// passed as-is to this method but will get chunked, with each list item
// passed to this method separately.
uint8_t buffer[1200];
TLV::TLVWriter writer;
writer.Init(buffer);
CHIP_ERROR err = MTREncodeTLVFromDataValueDictionary(value, writer, TLV::AnonymousTag());
if (err != CHIP_NO_ERROR) {
if (error) {
*error = [MTRError errorForCHIPErrorCode:err];
}
return nil;
}
return AsData(ByteSpan(buffer, writer.GetLengthWritten()));
}
// Callback type to pass data value as an NSObject
typedef void (*MTRDataValueDictionaryCallback)(void * context, id value);
// Rename to be generic for decode and encode
class MTRDataValueDictionaryDecodableType {
public:
MTRDataValueDictionaryDecodableType()
: decodedObj(nil)
{
}
MTRDataValueDictionaryDecodableType(id obj)
: decodedObj(obj)
{
}
CHIP_ERROR Decode(chip::TLV::TLVReader & data)
{
decodedObj = MTRDecodeDataValueDictionaryFromCHIPTLV(&data);
if (decodedObj == nil) {
MTR_LOG_ERROR("Error: Failed to get value from TLV data for attribute reading response");
}
return (decodedObj) ? CHIP_NO_ERROR : CHIP_ERROR_DECODE_FAILED;
}
CHIP_ERROR Encode(chip::TLV::TLVWriter & writer, chip::TLV::Tag tag) const
{
return MTREncodeTLVFromDataValueDictionary(decodedObj, writer, tag);
}
static constexpr bool kIsFabricScoped = false;
static bool MustUseTimedInvoke() { return false; }
NSDictionary<NSString *, id> * _Nullable GetDecodedObject() const { return decodedObj; }
private:
NSDictionary<NSString *, id> * _Nullable decodedObj;
};
// Callback bridge for MTRDataValueDictionaryCallback
class MTRDataValueDictionaryCallbackBridge : public MTRCallbackBridge<MTRDataValueDictionaryCallback> {
public:
MTRDataValueDictionaryCallbackBridge(dispatch_queue_t queue, MTRDeviceResponseHandler handler, MTRActionBlock action)
: MTRCallbackBridge<MTRDataValueDictionaryCallback>(queue, handler, action, OnSuccessFn) {};
static void OnSuccessFn(void * context, id value) { DispatchSuccess(context, value); }
};
template <typename DecodableValueType>
class BufferedReadClientCallback final : public app::ReadClient::Callback {
public:
using OnSuccessAttributeCallbackType
= std::function<void(const ConcreteDataAttributePath & aPath, const DecodableValueType & aData)>;
using OnSuccessEventCallbackType = std::function<void(const EventHeader & aEventHeader, const DecodableValueType & aData)>;
using OnErrorCallbackType = std::function<void(
const app::ConcreteAttributePath * attributePath, const app::ConcreteEventPath * eventPath, CHIP_ERROR aError)>;
using OnDoneCallbackType = std::function<void(BufferedReadClientCallback * callback)>;
using OnSubscriptionEstablishedCallbackType = std::function<void()>;
using OnDeviceResubscriptionScheduledCallbackType = std::function<void(NSError * error, NSNumber * resubscriptionDelay)>;
BufferedReadClientCallback(app::AttributePathParams * aAttributePathParamsList, size_t aAttributePathParamsSize,
app::EventPathParams * aEventPathParamsList, size_t aEventPathParamsSize,
OnSuccessAttributeCallbackType aOnAttributeSuccess, OnSuccessEventCallbackType aOnEventSuccess,
OnErrorCallbackType aOnError, OnDoneCallbackType aOnDone,
OnSubscriptionEstablishedCallbackType aOnSubscriptionEstablished = nullptr,
OnDeviceResubscriptionScheduledCallbackType aOnDeviceResubscriptionScheduled = nullptr)
: mAttributePathParamsList(aAttributePathParamsList)
, mAttributePathParamsSize(aAttributePathParamsSize)
, mEventPathParamsList(aEventPathParamsList)
, mEventPathParamsSize(aEventPathParamsSize)
, mOnAttributeSuccess(aOnAttributeSuccess)
, mOnEventSuccess(aOnEventSuccess)
, mOnError(aOnError)
, mOnDone(aOnDone)
, mOnSubscriptionEstablished(aOnSubscriptionEstablished)
, mOnDeviceResubscriptionScheduled(aOnDeviceResubscriptionScheduled)
, mBufferedReadAdapter(*this)
{
}
~BufferedReadClientCallback()
{
// Ensure we release the ReadClient before we tear down anything else,
// so it can call our OnDeallocatePaths properly.
mReadClient = nullptr;
}
app::BufferedReadCallback & GetBufferedCallback() { return mBufferedReadAdapter; }
void AdoptReadClient(Platform::UniquePtr<app::ReadClient> aReadClient) { mReadClient = std::move(aReadClient); }
private:
void OnAttributeData(
const app::ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const app::StatusIB & aStatus) override
{
CHIP_ERROR err = CHIP_NO_ERROR;
DecodableValueType value;
VerifyOrExit(mOnAttributeSuccess != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrExit(mAttributePathParamsList != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
//
// We shouldn't be getting list item operations in the provided path since that should be handled by the buffered read
// callback. If we do, that's a bug.
//
VerifyOrDie(!aPath.IsListItemOperation());
VerifyOrExit(
std::find_if(mAttributePathParamsList, mAttributePathParamsList + mAttributePathParamsSize,
[aPath](app::AttributePathParams & pathParam) -> bool { return pathParam.IsAttributePathSupersetOf(aPath); })
!= mAttributePathParamsList + mAttributePathParamsSize,
err = CHIP_ERROR_SCHEMA_MISMATCH);
VerifyOrExit(aStatus.IsSuccess(), err = aStatus.ToChipError());
VerifyOrExit(apData != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
SuccessOrExit(err = app::DataModel::Decode(*apData, value));
mOnAttributeSuccess(aPath, value);
exit:
if (err != CHIP_NO_ERROR) {
mOnError(&aPath, nullptr, err);
}
}
void OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus) override
{
CHIP_ERROR err = CHIP_NO_ERROR;
DecodableValueType value;
VerifyOrExit(mOnEventSuccess != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrExit(mEventPathParamsList != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrExit(std::find_if(mEventPathParamsList, mEventPathParamsList + mEventPathParamsSize,
[aEventHeader](app::EventPathParams & pathParam) -> bool {
return pathParam.IsEventPathSupersetOf(aEventHeader.mPath);
})
!= mEventPathParamsList + mEventPathParamsSize,
err = CHIP_ERROR_SCHEMA_MISMATCH);
VerifyOrExit(apStatus == nullptr, err = apStatus->ToChipError());
VerifyOrExit(apData != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT);
SuccessOrExit(err = app::DataModel::Decode(*apData, value));
mOnEventSuccess(aEventHeader, value);
exit:
if (err != CHIP_NO_ERROR) {
mOnError(nullptr, &aEventHeader.mPath, err);
}
}
void OnError(CHIP_ERROR aError) override { mOnError(nullptr, nullptr, aError); }
void OnDone(ReadClient *) override { mOnDone(this); }
void OnSubscriptionEstablished(SubscriptionId aSubscriptionId) override
{
if (mOnSubscriptionEstablished) {
mOnSubscriptionEstablished();
}
}
CHIP_ERROR OnResubscriptionNeeded(ReadClient * apReadClient, CHIP_ERROR aTerminationCause) override
{
CHIP_ERROR err = ReadClient::Callback::OnResubscriptionNeeded(apReadClient, aTerminationCause);
ReturnErrorOnFailure(err);
if (mOnDeviceResubscriptionScheduled != nullptr) {
auto callback = mOnDeviceResubscriptionScheduled;
auto error = [MTRError errorForCHIPErrorCode:aTerminationCause];
auto delayMs = @(apReadClient->ComputeTimeTillNextSubscription());
callback(error, delayMs);
}
return CHIP_NO_ERROR;
}
void OnDeallocatePaths(chip::app::ReadPrepareParams && aReadPrepareParams) override {}
OnSuccessAttributeCallbackType mOnAttributeSuccess;
OnSuccessEventCallbackType mOnEventSuccess;
OnErrorCallbackType mOnError;
OnDoneCallbackType mOnDone;
OnSubscriptionEstablishedCallbackType mOnSubscriptionEstablished;
OnDeviceResubscriptionScheduledCallbackType mOnDeviceResubscriptionScheduled;
app::BufferedReadCallback mBufferedReadAdapter;
Platform::UniquePtr<app::ReadClient> mReadClient;
app::AttributePathParams * mAttributePathParamsList;
app::EventPathParams * mEventPathParamsList;
size_t mAttributePathParamsSize;
size_t mEventPathParamsSize;
};
- (void)readAttributesWithEndpointID:(NSNumber * _Nullable)endpointID
clusterID:(NSNumber * _Nullable)clusterID
attributeID:(NSNumber * _Nullable)attributeID
params:(MTRReadParams * _Nullable)params
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
NSArray<MTRAttributeRequestPath *> * attributePaths = [NSArray
arrayWithObject:[MTRAttributeRequestPath requestPathWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID]];
[self readAttributePaths:attributePaths eventPaths:nil params:params queue:queue completion:completion];
}
- (void)_readKnownAttributeWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
attributeID:(NSNumber *)attributeID
params:(MTRReadParams * _Nullable)params
queue:(dispatch_queue_t)queue
completion:(void (^)(id _Nullable value, NSError * _Nullable error))completion
{
auto * attributePath = [MTRAttributePath attributePathWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID];
auto innerCompletion = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
if (error != nil) {
completion(nil, error);
return;
}
// Preserving the old behavior: we don't fail on multiple reports, but
// just report the first one.
if (values.count == 0) {
completion(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeSchemaMismatch userInfo:nil]);
return;
}
NSDictionary<NSString *, id> * value = values[0];
NSError * initError;
auto * report = [[MTRAttributeReport alloc] initWithResponseValue:value error:&initError];
if (initError != nil) {
completion(nil, initError);
return;
}
if (![report.path isEqual:attributePath]) {
// For some reason the server returned data for the wrong
// attribute, even though it happened to decode to our type.
completion(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeSchemaMismatch userInfo:nil]);
return;
}
completion(report.value, report.error);
};
[self readAttributesWithEndpointID:endpointID
clusterID:clusterID
attributeID:attributeID
params:params
queue:queue
completion:innerCompletion];
}
- (void)readAttributePaths:(NSArray<MTRAttributeRequestPath *> * _Nullable)attributePaths
eventPaths:(NSArray<MTREventRequestPath *> * _Nullable)eventPaths
params:(MTRReadParams * _Nullable)params
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
[self readAttributePaths:attributePaths eventPaths:eventPaths params:params includeDataVersion:NO queue:queue completion:completion];
}
- (void)readAttributePaths:(NSArray<MTRAttributeRequestPath *> * _Nullable)attributePaths
eventPaths:(NSArray<MTREventRequestPath *> * _Nullable)eventPaths
params:(MTRReadParams * _Nullable)params
includeDataVersion:(BOOL)includeDataVersion
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
if ((attributePaths == nil || [attributePaths count] == 0) && (eventPaths == nil || [eventPaths count] == 0)) {
// No paths, just return an empty array.
dispatch_async(queue, ^{
completion(@[], nil);
});
return;
}
NSArray<MTRAttributeRequestPath *> * attributes = nil;
if (attributePaths != nil) {
attributes = [[NSArray alloc] initWithArray:attributePaths copyItems:YES];
}
NSArray<MTREventRequestPath *> * events = nil;
if (eventPaths != nil) {
events = [[NSArray alloc] initWithArray:eventPaths copyItems:YES];
}
params = (params == nil) ? nil : [params copy];
auto * bridge = new MTRDataValueDictionaryCallbackBridge(queue, completion,
^(ExchangeManager & exchangeManager, const SessionHandle & session, MTRDataValueDictionaryCallback successCb,
MTRErrorCallback failureCb, MTRCallbackBridgeBase * bridge) {
// interactionStatus tracks whether the whole read interaction has failed.
//
// Make sure interactionStatus survives even if this block scope is destroyed.
auto interactionStatus = std::make_shared<CHIP_ERROR>(CHIP_NO_ERROR);
auto resultArray = [[NSMutableArray alloc] init];
auto onAttributeSuccessCb
= [resultArray, includeDataVersion](const ConcreteDataAttributePath & aAttributePath, const MTRDataValueDictionaryDecodableType & aData) {
// TODO: move this logic into MTRDataValueDictionaryDecodableType
if (includeDataVersion && aAttributePath.mDataVersion.HasValue()) {
NSDictionary * dataValue = aData.GetDecodedObject();
[resultArray addObject:@{
MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:aAttributePath],
MTRDataKey : _MakeDataValueDictionary(dataValue[MTRTypeKey], dataValue[MTRValueKey], @(aAttributePath.mDataVersion.Value()))
}];
} else {
[resultArray addObject:@ {
MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:aAttributePath],
MTRDataKey : aData.GetDecodedObject()
}];
}
};
auto onEventSuccessCb
= [resultArray](const EventHeader & aEventHeader, const MTRDataValueDictionaryDecodableType & aData) {
[resultArray addObject:[MTRBaseDevice eventReportForHeader:aEventHeader andData:aData.GetDecodedObject()]];
};
auto onFailureCb = [resultArray, interactionStatus](const app::ConcreteAttributePath * aAttributePath,
const app::ConcreteEventPath * aEventPath, CHIP_ERROR aError) {
if (aAttributePath != nullptr) {
[resultArray addObject:@ {
MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:*aAttributePath],
MTRErrorKey : [MTRError errorForCHIPErrorCode:aError]
}];
} else if (aEventPath != nullptr) {
[resultArray addObject:@ {
MTREventPathKey : [[MTREventPath alloc] initWithPath:*aEventPath],
MTRErrorKey : [MTRError errorForCHIPErrorCode:aError]
}];
} else {
// This will only happen once per read interaction, and
// after that there will be no more calls to onFailureCb or
// onSuccessCb.
*interactionStatus = aError;
}
};
Platform::ScopedMemoryBuffer<AttributePathParams> attributePathParamsList;
Platform::ScopedMemoryBuffer<EventPathParams> eventPathParamsList;
if (attributes != nil) {
size_t count = 0;
VerifyOrReturnError(attributePathParamsList.Calloc([attributes count]), CHIP_ERROR_NO_MEMORY);
for (MTRAttributeRequestPath * attribute in attributes) {
[attribute convertToAttributePathParams:attributePathParamsList[count++]];
}
}
if (events != nil) {
size_t count = 0;
VerifyOrReturnError(eventPathParamsList.Calloc([events count]), CHIP_ERROR_NO_MEMORY);
for (MTREventRequestPath * event in events) {
[event convertToEventPathParams:eventPathParamsList[count++]];
}
}
app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance();
CHIP_ERROR err = CHIP_NO_ERROR;
chip::app::ReadPrepareParams readParams(session);
[params toReadPrepareParams:readParams];
readParams.mpAttributePathParamsList = attributePathParamsList.Get();
readParams.mAttributePathParamsListSize = [attributes count];
readParams.mpEventPathParamsList = eventPathParamsList.Get();
readParams.mEventPathParamsListSize = [events count];
AttributePathParams * attributePathParamsListToFree = attributePathParamsList.Get();
EventPathParams * eventPathParamsListToFree = eventPathParamsList.Get();
auto onDone
= [resultArray, interactionStatus, bridge, successCb, failureCb, attributePathParamsListToFree,
eventPathParamsListToFree](BufferedReadClientCallback<MTRDataValueDictionaryDecodableType> * callback) {
if (*interactionStatus != CHIP_NO_ERROR) {
// Failure
failureCb(bridge, *interactionStatus);
} else {
// Success
successCb(bridge, resultArray);
}
if (attributePathParamsListToFree != nullptr) {
Platform::MemoryFree(attributePathParamsListToFree);
}
if (eventPathParamsListToFree != nullptr) {
Platform::MemoryFree(eventPathParamsListToFree);
}
chip::Platform::Delete(callback);
};
auto callback = chip::Platform::MakeUnique<BufferedReadClientCallback<MTRDataValueDictionaryDecodableType>>(
attributePathParamsList.Get(), readParams.mAttributePathParamsListSize, eventPathParamsList.Get(),
readParams.mEventPathParamsListSize, onAttributeSuccessCb, onEventSuccessCb, onFailureCb, onDone, nullptr);
VerifyOrReturnError(callback != nullptr, CHIP_ERROR_NO_MEMORY);
auto readClient = chip::Platform::MakeUnique<app::ReadClient>(
engine, &exchangeManager, callback->GetBufferedCallback(), chip::app::ReadClient::InteractionType::Read);
VerifyOrReturnError(readClient != nullptr, CHIP_ERROR_NO_MEMORY);
err = readClient->SendRequest(readParams);
if (err != CHIP_NO_ERROR) {
return err;
}
//
// At this point, we'll get a callback through the OnDone callback above regardless of success or failure
// of the read operation to permit us to free up the callback object. So, release ownership of the callback
// object now to prevent it from being reclaimed at the end of this scoped block.
//
callback->AdoptReadClient(std::move(readClient));
callback.release();
IgnoreUnusedVariable(attributePathParamsList.Release());
IgnoreUnusedVariable(eventPathParamsList.Release());
return err;
});
std::move(*bridge).DispatchAction(self);
}
- (void)writeAttributeWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
attributeID:(NSNumber *)attributeID
value:(id)value
timedWriteTimeout:(NSNumber * _Nullable)timeoutMs
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
auto * bridge = new MTRDataValueDictionaryCallbackBridge(queue, completion,
^(ExchangeManager & exchangeManager, const SessionHandle & session, MTRDataValueDictionaryCallback successCb,
MTRErrorCallback failureCb, MTRCallbackBridgeBase * bridge) {
// Controller::WriteAttribute guarantees that there will be exactly one call to either the success callback or the
// failure callback, for a non-group session.
auto onSuccessCb = [successCb, bridge](const app::ConcreteAttributePath & attribPath) {
auto resultArray = @[ @ { MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:attribPath] } ];
successCb(bridge, resultArray);
};
auto onFailureCb = [failureCb, bridge](
const app::ConcreteAttributePath * attribPath, CHIP_ERROR aError) { failureCb(bridge, aError); };
return chip::Controller::WriteAttribute<MTRDataValueDictionaryDecodableType>(session,
static_cast<chip::EndpointId>([endpointID unsignedShortValue]),
static_cast<chip::ClusterId>([clusterID unsignedLongValue]),
static_cast<chip::AttributeId>([attributeID unsignedLongValue]), MTRDataValueDictionaryDecodableType(value),
onSuccessCb, onFailureCb, (timeoutMs == nil) ? NullOptional : Optional<uint16_t>([timeoutMs unsignedShortValue]));
});
std::move(*bridge).DispatchAction(self);
}
class NSObjectCommandCallback final : public app::CommandSender::Callback {
public:
using OnSuccessCallbackType
= std::function<void(const app::ConcreteCommandPath &, const app::StatusIB &, const MTRDataValueDictionaryDecodableType &)>;
using OnErrorCallbackType = std::function<void(CHIP_ERROR aError)>;
using OnDoneCallbackType = std::function<void(app::CommandSender * commandSender)>;
/*
* Constructor that takes in success, failure and onDone callbacks.
*
* The latter can be provided later through the SetOnDoneCallback below in cases where the
* TypedCommandCallback object needs to be created first before it can be passed in as a closure
* into a hypothetical OnDoneCallback function.
*/
NSObjectCommandCallback(chip::ClusterId clusterId, chip::CommandId commandId, OnSuccessCallbackType aOnSuccess,
OnErrorCallbackType aOnError, OnDoneCallbackType aOnDone = {})
: mOnSuccess(aOnSuccess)
, mOnError(aOnError)
, mOnDone(aOnDone)
, mClusterId(clusterId)
, mCommandId(commandId)
{
}
void SetOnDoneCallback(OnDoneCallbackType callback) { mOnDone = callback; }
private:
void OnResponse(app::CommandSender * apCommandSender, const app::ConcreteCommandPath & aCommandPath,
const app::StatusIB & aStatus, TLV::TLVReader * aReader) override;
void OnError(const app::CommandSender * apCommandSender, CHIP_ERROR aError) override
{
if (mCalledCallback) {
return;
}
mCalledCallback = true;
mOnError(aError);
}
void OnDone(app::CommandSender * apCommandSender) override
{
if (!mCalledCallback) {
// This can happen if the server sends a response with an empty
// InvokeResponses list. Since we are not sending wildcard command
// paths, that's not a valid response and we should treat it as an
// error. Use the error we would have gotten if we in fact expected
// a nonempty list.
OnError(apCommandSender, CHIP_END_OF_TLV);
}
mOnDone(apCommandSender);
}
OnSuccessCallbackType mOnSuccess;
OnErrorCallbackType mOnError;
OnDoneCallbackType mOnDone;
chip::ClusterId mClusterId;
// Id of the command we send.
chip::CommandId mCommandId;
bool mCalledCallback = false;
};
void NSObjectCommandCallback::OnResponse(app::CommandSender * apCommandSender, const app::ConcreteCommandPath & aCommandPath,
const app::StatusIB & aStatus, TLV::TLVReader * aReader)
{
if (mCalledCallback) {
return;
}
mCalledCallback = true;
MTRDataValueDictionaryDecodableType response;
CHIP_ERROR err = CHIP_NO_ERROR;
//
// Validate that the data response we received matches what we expect in terms of its cluster and command IDs.
//
VerifyOrExit(aCommandPath.mClusterId == mClusterId, err = CHIP_ERROR_SCHEMA_MISMATCH);
// If aReader is null, we got a status response and the command id in the
// path should match our command id. If aReader is not null, we got a data
// response, which will have its own command id, which we don't know.
VerifyOrExit(aCommandPath.mCommandId == mCommandId || aReader != nullptr, err = CHIP_ERROR_SCHEMA_MISMATCH);
if (aReader != nullptr) {
err = app::DataModel::Decode(*aReader, response);
SuccessOrExit(err);
}
mOnSuccess(aCommandPath, aStatus, response);
exit:
if (err != CHIP_NO_ERROR) {
mOnError(err);
}
}
- (void)invokeCommandWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
commandID:(NSNumber *)commandID
commandFields:(id)commandFields
timedInvokeTimeout:(NSNumber * _Nullable)timeoutMs
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
// We don't have a way to communicate a non-default invoke timeout
// here for now.
// TODO: https://github.com/project-chip/connectedhomeip/issues/24563
[self _invokeCommandWithEndpointID:endpointID
clusterID:clusterID
commandID:commandID
commandFields:commandFields
timedInvokeTimeout:timeoutMs
serverSideProcessingTimeout:nil
queue:queue
completion:completion];
}
- (void)_invokeCommandWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
commandID:(NSNumber *)commandID
commandFields:(id)commandFields
timedInvokeTimeout:(NSNumber * _Nullable)timeoutMs
serverSideProcessingTimeout:(NSNumber * _Nullable)serverSideProcessingTimeout
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
endpointID = (endpointID == nil) ? nil : [endpointID copy];
clusterID = (clusterID == nil) ? nil : [clusterID copy];
commandID = (commandID == nil) ? nil : [commandID copy];
// TODO: This is not going to deep-copy the NSArray instances in
// commandFields. We need to do something smarter here.
commandFields = (commandFields == nil) ? nil : [commandFields copy];
serverSideProcessingTimeout = [serverSideProcessingTimeout copy];
if (serverSideProcessingTimeout != nil) {
serverSideProcessingTimeout = MTRClampedNumber(serverSideProcessingTimeout, @(0), @(UINT16_MAX));
}
timeoutMs = [timeoutMs copy];
if (timeoutMs != nil) {
timeoutMs = MTRClampedNumber(timeoutMs, @(1), @(UINT16_MAX));
}
auto * bridge = new MTRDataValueDictionaryCallbackBridge(queue, completion,
^(ExchangeManager & exchangeManager, const SessionHandle & session, MTRDataValueDictionaryCallback successCb,
MTRErrorCallback failureCb, MTRCallbackBridgeBase * bridge) {
NSData * attestationChallenge;
if ([clusterID isEqualToNumber:@(MTRClusterIDTypeOperationalCredentialsID)] &&
[commandID isEqualToNumber:@(MTRCommandIDTypeClusterOperationalCredentialsCommandAttestationRequestID)] && session->IsSecureSession()) {
// An AttestationResponse command needs to have an attestationChallenge
// to make sense of the results. If we are doing an
// AttestationRequest, store the challenge now.
attestationChallenge = AsData(session->AsSecureSession()->GetCryptoContext().GetAttestationChallenge());
}
// NSObjectCommandCallback guarantees that there will be exactly one call to either the success callback or the failure
// callback.
auto onSuccessCb = [successCb, bridge, attestationChallenge](const app::ConcreteCommandPath & commandPath, const app::StatusIB & status,
const MTRDataValueDictionaryDecodableType & responseData) {
auto resultArray = [[NSMutableArray alloc] init];
if (responseData.GetDecodedObject()) {
auto response = responseData.GetDecodedObject();
if (attestationChallenge != nil) {
// Add the attestationChallenge to our data.
NSArray<NSDictionary<NSString *, id> *> * value = response[MTRValueKey];
NSMutableArray<NSDictionary<NSString *, id> *> * newValue = [[NSMutableArray alloc] initWithCapacity:(value.count + 1)];
[newValue addObjectsFromArray:value];
[newValue addObject:@{
MTRContextTagKey : @(kAttestationChallengeTagValue),
MTRDataKey : @ {
MTRTypeKey : MTROctetStringValueType,
MTRValueKey : attestationChallenge,
},
}];
auto * newResponse = [NSMutableDictionary dictionaryWithCapacity:(response.count + 1)];
[newResponse addEntriesFromDictionary:response];
newResponse[MTRValueKey] = newValue;
response = newResponse;
}
[resultArray addObject:@ {
MTRCommandPathKey : [[MTRCommandPath alloc] initWithPath:commandPath],
MTRDataKey : response,
}];
} else {
[resultArray addObject:@ { MTRCommandPathKey : [[MTRCommandPath alloc] initWithPath:commandPath] }];
}
successCb(bridge, resultArray);
};
auto onFailureCb = [failureCb, bridge](CHIP_ERROR aError) { failureCb(bridge, aError); };
app::CommandPathParams commandPath = { static_cast<chip::EndpointId>([endpointID unsignedShortValue]), 0,
static_cast<chip::ClusterId>([clusterID unsignedLongValue]),
static_cast<chip::CommandId>([commandID unsignedLongValue]), (app::CommandPathFlags::kEndpointIdValid) };
auto decoder = chip::Platform::MakeUnique<NSObjectCommandCallback>(
commandPath.mClusterId, commandPath.mCommandId, onSuccessCb, onFailureCb);
VerifyOrReturnError(decoder != nullptr, CHIP_ERROR_NO_MEMORY);
auto rawDecoderPtr = decoder.get();
auto onDoneCb = [rawDecoderPtr](app::CommandSender * commandSender) {
chip::Platform::Delete(commandSender);
chip::Platform::Delete(rawDecoderPtr);
};
decoder->SetOnDoneCallback(onDoneCb);
bool isTimedRequest = (timeoutMs != nil);
auto commandSender = chip::Platform::MakeUnique<app::CommandSender>(decoder.get(), &exchangeManager, isTimedRequest);
VerifyOrReturnError(commandSender != nullptr, CHIP_ERROR_NO_MEMORY);
ReturnErrorOnFailure(commandSender->AddRequestData(commandPath, MTRDataValueDictionaryDecodableType(commandFields),
(timeoutMs == nil) ? NullOptional : Optional<uint16_t>([timeoutMs unsignedShortValue])));
Optional<System::Clock::Timeout> invokeTimeout;
if (serverSideProcessingTimeout != nil) {
// Clamp to a number of seconds that will not overflow 32-bit
// int when converted to ms.
auto serverTimeoutInSeconds = System::Clock::Seconds16(serverSideProcessingTimeout.unsignedShortValue);
invokeTimeout.SetValue(session->ComputeRoundTripTimeout(serverTimeoutInSeconds));
}
ReturnErrorOnFailure(commandSender->SendCommandRequest(session, invokeTimeout));
decoder.release();
commandSender.release();
return CHIP_NO_ERROR;
});
std::move(*bridge).DispatchAction(self);
}
- (void)_invokeKnownCommandWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
commandID:(NSNumber *)commandID
commandPayload:(id)commandPayload
timedInvokeTimeout:(NSNumber * _Nullable)timeout
serverSideProcessingTimeout:(NSNumber * _Nullable)serverSideProcessingTimeout
responseClass:(Class _Nullable)responseClass
queue:(dispatch_queue_t)queue
completion:(void (^)(id _Nullable response, NSError * _Nullable error))completion
{
NSError * encodingError;
auto * commandFields = [commandPayload _encodeAsDataValue:&encodingError];
if (commandFields == nil) {
dispatch_async(queue, ^{
completion(nil, encodingError);
});
return;
}
auto responseHandler = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
id _Nullable response = nil;
if (error == nil) {
if (values.count != 1) {
error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeSchemaMismatch userInfo:nil];
} else if (responseClass != nil) {
response = [[responseClass alloc] initWithResponseValue:values[0] error:&error];
}
}
completion(response, error);
};
[self _invokeCommandWithEndpointID:endpointID
clusterID:clusterID
commandID:commandID
commandFields:commandFields
timedInvokeTimeout:timeout
serverSideProcessingTimeout:serverSideProcessingTimeout
queue:queue
completion:responseHandler];
}
- (void)subscribeToAttributesWithEndpointID:(NSNumber * _Nullable)endpointID
clusterID:(NSNumber * _Nullable)clusterID
attributeID:(NSNumber * _Nullable)attributeID
params:(MTRSubscribeParams * _Nullable)params
queue:(dispatch_queue_t)queue
reportHandler:(MTRDeviceResponseHandler)reportHandler
subscriptionEstablished:(MTRSubscriptionEstablishedHandler)subscriptionEstablished
{
NSArray<MTRAttributeRequestPath *> * attributePaths = [NSArray
arrayWithObject:[MTRAttributeRequestPath requestPathWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID]];
[self subscribeToAttributePaths:attributePaths
eventPaths:nil
params:params
queue:queue
reportHandler:reportHandler
subscriptionEstablished:subscriptionEstablished
resubscriptionScheduled:nil];
}
- (void)_subscribeToKnownAttributeWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
attributeID:(NSNumber *)attributeID
params:(MTRSubscribeParams *)params
queue:(dispatch_queue_t)queue
reportHandler:(void (^)(id _Nullable value, NSError * _Nullable error))reportHandler
subscriptionEstablished:(MTRSubscriptionEstablishedHandler _Nullable)subscriptionEstablished
{
auto * attributePath = [MTRAttributePath attributePathWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID];
auto innerReportHandler = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
if (error != nil) {
reportHandler(nil, error);
return;
}
for (NSDictionary<NSString *, id> * value in values) {
NSError * initError;
auto * report = [[MTRAttributeReport alloc] initWithResponseValue:value error:&initError];
if (initError != nil) {
reportHandler(nil, initError);
continue;
}
if (![report.path isEqual:attributePath]) {
// For some reason the server returned data for the wrong
// attribute, even though it happened to decode to our type.
reportHandler(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeSchemaMismatch userInfo:nil]);
continue;
}
reportHandler(report.value, report.error);
}
};
[self subscribeToAttributesWithEndpointID:endpointID
clusterID:clusterID
attributeID:attributeID
params:params
queue:queue
reportHandler:innerReportHandler
subscriptionEstablished:subscriptionEstablished];
}
- (void)subscribeToAttributePaths:(NSArray<MTRAttributeRequestPath *> * _Nullable)attributePaths
eventPaths:(NSArray<MTREventRequestPath *> * _Nullable)eventPaths
params:(MTRSubscribeParams * _Nullable)params
queue:(dispatch_queue_t)queue
reportHandler:(MTRDeviceResponseHandler)reportHandler
subscriptionEstablished:(MTRSubscriptionEstablishedHandler _Nullable)subscriptionEstablished
resubscriptionScheduled:(MTRDeviceResubscriptionScheduledHandler _Nullable)resubscriptionScheduled
{
if ((attributePaths == nil || [attributePaths count] == 0) && (eventPaths == nil || [eventPaths count] == 0)) {
// Per spec a server would respond InvalidAction to this, so just go
// ahead and do that.
dispatch_async(queue, ^{
reportHandler(nil, [MTRError errorForIMStatus:StatusIB(Status::InvalidAction)]);
});
return;
}
if (self.isPASEDevice) {
// We don't support subscriptions over PASE.
dispatch_async(queue, ^{
reportHandler(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]);
});
return;
}
// Copy params before going async.
NSArray<MTRAttributeRequestPath *> * attributes = nil;
if (attributePaths != nil) {
attributes = [[NSArray alloc] initWithArray:attributePaths copyItems:YES];
}
NSArray<MTREventRequestPath *> * events = nil;
if (eventPaths != nil) {
events = [[NSArray alloc] initWithArray:eventPaths copyItems:YES];
}
params = (params == nil) ? nil : [params copy];
[self.deviceController
getSessionForNode:self.nodeID
completion:^(ExchangeManager * _Nullable exchangeManager, const Optional<SessionHandle> & session,
NSError * _Nullable error) {
if (error != nil) {
dispatch_async(queue, ^{
reportHandler(nil, error);
});
return;
}
auto onAttributeReportCb = [queue, reportHandler](const ConcreteAttributePath & attributePath,
const MTRDataValueDictionaryDecodableType & data) {
id valueObject = data.GetDecodedObject();
ConcreteAttributePath pathCopy(attributePath);
dispatch_async(queue, ^{
reportHandler(@[ @ {
MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:pathCopy],
MTRDataKey : valueObject
} ],
nil);
});
};
auto onEventReportCb = [queue, reportHandler](
const EventHeader & eventHeader, const MTRDataValueDictionaryDecodableType & data) {
NSDictionary * report = [MTRBaseDevice eventReportForHeader:eventHeader andData:data.GetDecodedObject()];
dispatch_async(queue, ^{
reportHandler(@[ report ], nil);
});
};
auto onFailureCb = [queue, reportHandler](const app::ConcreteAttributePath * attributePath,
const app::ConcreteEventPath * eventPath, CHIP_ERROR error) {
if (attributePath != nullptr) {
ConcreteAttributePath pathCopy(*attributePath);
dispatch_async(queue, ^{
reportHandler(@[ @ {
MTRAttributePathKey : [[MTRAttributePath alloc] initWithPath:pathCopy],
MTRErrorKey : [MTRError errorForCHIPErrorCode:error]
} ],
nil);
});
} else if (eventPath != nullptr) {
ConcreteEventPath pathCopy(*eventPath);
dispatch_async(queue, ^{
reportHandler(@[ @ {
MTREventPathKey : [[MTREventPath alloc] initWithPath:pathCopy],
MTRErrorKey : [MTRError errorForCHIPErrorCode:error]
} ],
nil);
});
} else {
dispatch_async(queue, ^{
reportHandler(nil, [MTRError errorForCHIPErrorCode:error]);
});
}
};
auto onEstablishedCb = [queue, subscriptionEstablished]() {
if (subscriptionEstablished) {
dispatch_async(queue, subscriptionEstablished);
}
};
auto onResubscriptionScheduledCb
= [queue, resubscriptionScheduled](NSError * error, NSNumber * resubscriptionDelay) {
if (resubscriptionScheduled) {
dispatch_async(queue, ^{
resubscriptionScheduled(error, resubscriptionDelay);
});
}
};
MTRReadClientContainer * container = [[MTRReadClientContainer alloc] init];
container.deviceID = self.nodeID;
size_t attributePathSize = 0;
if (attributes != nil) {
container.pathParams = static_cast<AttributePathParams *>(
Platform::MemoryCalloc([attributes count], sizeof(AttributePathParams)));
if (container.pathParams == nullptr) {
dispatch_async(queue, ^{
reportHandler(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_NO_MEMORY]);
});
return;
}
for (MTRAttributeRequestPath * attribute in attributes) {
[attribute convertToAttributePathParams:container.pathParams[attributePathSize++]];
}
}
size_t eventPathSize = 0;
if (events != nil) {
container.eventPathParams
= static_cast<EventPathParams *>(Platform::MemoryCalloc([events count], sizeof(EventPathParams)));
if (container.eventPathParams == nullptr) {
dispatch_async(queue, ^{
reportHandler(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_NO_MEMORY]);
});
return;
}
for (MTREventRequestPath * event in events) {
[event convertToEventPathParams:container.eventPathParams[eventPathSize++]];
}
}
app::InteractionModelEngine * engine = app::InteractionModelEngine::GetInstance();
CHIP_ERROR err = CHIP_NO_ERROR;
chip::app::ReadPrepareParams readParams(session.Value());
[params toReadPrepareParams:readParams];
readParams.mpAttributePathParamsList = container.pathParams;
readParams.mAttributePathParamsListSize = attributePathSize;
readParams.mpEventPathParamsList = container.eventPathParams;
readParams.mEventPathParamsListSize = eventPathSize;
auto onDone = [container](BufferedReadClientCallback<MTRDataValueDictionaryDecodableType> * callback) {
[container onDone];
// Make sure we delete callback last, because doing that actually destroys our
// lambda, so we can't access captured values after that.
chip::Platform::Delete(callback);
};
auto callback = chip::Platform::MakeUnique<BufferedReadClientCallback<MTRDataValueDictionaryDecodableType>>(
container.pathParams, attributePathSize, container.eventPathParams, eventPathSize, onAttributeReportCb,
onEventReportCb, onFailureCb, onDone, onEstablishedCb, onResubscriptionScheduledCb);
auto readClient = Platform::New<app::ReadClient>(
engine, exchangeManager, callback->GetBufferedCallback(), chip::app::ReadClient::InteractionType::Subscribe);
if (!params.resubscribeAutomatically) {
err = readClient->SendRequest(readParams);
} else {
err = readClient->SendAutoResubscribeRequest(std::move(readParams));
}
if (err != CHIP_NO_ERROR) {
dispatch_async(queue, ^{
reportHandler(nil, [MTRError errorForCHIPErrorCode:err]);
});
Platform::Delete(readClient);
if (container.pathParams != nullptr) {
Platform::MemoryFree(container.pathParams);
}
if (container.eventPathParams != nullptr) {
Platform::MemoryFree(container.eventPathParams);
}
container.pathParams = nullptr;
container.eventPathParams = nullptr;
return;
}
// Read clients will be purged when deregistered.
container.readClientPtr = readClient;
AddReadClientContainer(container.deviceID, container);
callback.release();
}];
}
- (void)deregisterReportHandlersWithQueue:(dispatch_queue_t)queue completion:(dispatch_block_t)completion
{
// This method must only be used for MTRDeviceOverXPC. However, for unit testing purpose, the method purges all read clients.
MTR_LOG_DEBUG("Unexpected call to deregister report handlers");
PurgeReadClientContainers(self.deviceController, self.nodeID, queue, completion);
}
namespace {
class OpenCommissioningWindowHelper {
typedef void (^ResultCallback)(CHIP_ERROR status, const SetupPayload &);
public:
static CHIP_ERROR OpenCommissioningWindow(Controller::DeviceController * controller, NodeId nodeID,
System::Clock::Seconds16 timeout, uint16_t discriminator, const Optional<uint32_t> & setupPIN, ResultCallback callback);
private:
OpenCommissioningWindowHelper(Controller::DeviceController * controller, ResultCallback callback);
static void OnOpenCommissioningWindowResponse(void * context, NodeId deviceId, CHIP_ERROR status, chip::SetupPayload payload);
Controller::CommissioningWindowOpener mOpener;
Callback::Callback<Controller::OnOpenCommissioningWindow> mOnOpenCommissioningWindowCallback;
ResultCallback mResultCallback;
};
OpenCommissioningWindowHelper::OpenCommissioningWindowHelper(Controller::DeviceController * controller, ResultCallback callback)
: mOpener(controller)
, mOnOpenCommissioningWindowCallback(OnOpenCommissioningWindowResponse, this)
, mResultCallback(callback)
{
}
CHIP_ERROR OpenCommissioningWindowHelper::OpenCommissioningWindow(Controller::DeviceController * controller, NodeId nodeID,
System::Clock::Seconds16 timeout, uint16_t discriminator, const Optional<uint32_t> & setupPIN, ResultCallback callback)
{
auto * self = new (std::nothrow) OpenCommissioningWindowHelper(controller, callback);
if (self == nullptr) {
return CHIP_ERROR_NO_MEMORY;
}
SetupPayload unused;
CHIP_ERROR err = self->mOpener.OpenCommissioningWindow(nodeID, timeout, Crypto::kSpake2p_Min_PBKDF_Iterations, discriminator,
setupPIN, NullOptional, &self->mOnOpenCommissioningWindowCallback, unused);
if (err != CHIP_NO_ERROR) {
delete self;
}
// Else will clean up when the callback is called.
return err;
}
void OpenCommissioningWindowHelper::OnOpenCommissioningWindowResponse(
void * context, NodeId deviceId, CHIP_ERROR status, chip::SetupPayload payload)
{
auto * self = static_cast<OpenCommissioningWindowHelper *>(context);
self->mResultCallback(status, payload);
delete self;
}
#pragma mark - Utility for time conversion
NSTimeInterval MTRTimeIntervalForEventTimestampValue(uint64_t timeValue)
{
// Note: The event timestamp value as written in the spec is in microseconds, but the released 1.0 SDK implemented it in
// milliseconds. The following issue was filed to address the inconsistency:
// https://github.com/CHIP-Specifications/connectedhomeip-spec/issues/6236
// For consistency with the released behavior, calculations here will be done in milliseconds.
// First convert the event timestamp value (in milliseconds) to NSTimeInterval - to minimize potential loss of precision
// of uint64 => NSTimeInterval (double), convert whole seconds and remainder separately and then combine
uint64_t eventTimestampValueSeconds = timeValue / chip::kMillisecondsPerSecond;
uint64_t eventTimestampValueRemainderMilliseconds = timeValue % chip::kMillisecondsPerSecond;
NSTimeInterval eventTimestampValueRemainder
= NSTimeInterval(eventTimestampValueRemainderMilliseconds) / chip::kMillisecondsPerSecond;
NSTimeInterval eventTimestampValue = eventTimestampValueSeconds + eventTimestampValueRemainder;
return eventTimestampValue;
}
#pragma mark - Utility for event priority conversion
BOOL MTRPriorityLevelIsValid(chip::app::PriorityLevel priorityLevel)
{
return (priorityLevel >= chip::app::PriorityLevel::Debug) && (priorityLevel <= chip::app::PriorityLevel::Critical);
}
MTREventPriority MTREventPriorityForValidPriorityLevel(chip::app::PriorityLevel priorityLevel)
{
switch (priorityLevel) {
case chip::app::PriorityLevel::Debug:
return MTREventPriorityDebug;
case chip::app::PriorityLevel::Info:
return MTREventPriorityInfo;
default:
return MTREventPriorityCritical;
}
}
} // anonymous namespace
- (void)_openCommissioningWindowWithSetupPasscode:(nullable NSNumber *)setupPasscode
discriminator:(NSNumber *)discriminator
duration:(NSNumber *)duration
queue:(dispatch_queue_t)queue
completion:(MTRDeviceOpenCommissioningWindowHandler)completion
{
MATTER_LOG_METRIC_BEGIN(kMetricOpenPairingWindow);
if (self.isPASEDevice) {
MTR_LOG_ERROR("Can't open a commissioning window over PASE");
dispatch_async(queue, ^{
MATTER_LOG_METRIC_END(kMetricOpenPairingWindow, CHIP_ERROR_INCORRECT_STATE);
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]);
});
return;
}
unsigned long long durationVal = [duration unsignedLongLongValue];
if (!CanCastTo<uint16_t>(durationVal)) {
MTR_LOG_ERROR("Error: Duration %llu is too large.", durationVal);
dispatch_async(queue, ^{
MATTER_LOG_METRIC_END(kMetricOpenPairingWindow, CHIP_ERROR_INVALID_INTEGER_VALUE);
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE]);
});
return;
}
unsigned long long discriminatorVal = [discriminator unsignedLongLongValue];
if (discriminatorVal > 0xFFF) {
MTR_LOG_ERROR("Error: Discriminator %llu is too large. Max value %d", discriminatorVal, 0xFFF);
dispatch_async(queue, ^{
MATTER_LOG_METRIC_END(kMetricOpenPairingWindow, CHIP_ERROR_INVALID_INTEGER_VALUE);
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE]);
});
return;
}
Optional<uint32_t> passcode;
if (setupPasscode != nil) {
unsigned long long passcodeVal = [setupPasscode unsignedLongLongValue];
if (!CanCastTo<uint32_t>(passcodeVal) || !SetupPayload::IsValidSetupPIN(static_cast<uint32_t>(passcodeVal))) {
MTR_LOG_ERROR("Error: Setup passcode %llu is not valid", passcodeVal);
dispatch_async(queue, ^{
MATTER_LOG_METRIC_END(kMetricOpenPairingWindow, CHIP_ERROR_INVALID_INTEGER_VALUE);
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_INTEGER_VALUE]);
});
return;
}
passcode.Emplace(static_cast<uint32_t>(passcodeVal));
}
[self.deviceController
asyncGetCommissionerOnMatterQueue:^(Controller::DeviceCommissioner * commissioner) {
auto resultCallback = ^(CHIP_ERROR status, const SetupPayload & payload) {
if (status != CHIP_NO_ERROR) {
dispatch_async(queue, ^{
MATTER_LOG_METRIC_END(kMetricOpenPairingWindow, status);
completion(nil, [MTRError errorForCHIPErrorCode:status]);
});
return;
}
auto * payloadObj = [[MTRSetupPayload alloc] initWithSetupPayload:payload];
if (payloadObj == nil) {
dispatch_async(queue, ^{
MATTER_LOG_METRIC_END(kMetricOpenPairingWindow, CHIP_ERROR_NO_MEMORY);
completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_NO_MEMORY]);
});
return;
}
dispatch_async(queue, ^{
MATTER_LOG_METRIC_END(kMetricOpenPairingWindow, CHIP_NO_ERROR);
completion(payloadObj, nil);
});
};
SetupPayload setupPayload;
auto errorCode = OpenCommissioningWindowHelper::OpenCommissioningWindow(commissioner, self.nodeID,
chip::System::Clock::Seconds16(static_cast<uint16_t>(durationVal)), static_cast<uint16_t>(discriminatorVal),
passcode, resultCallback);
if (errorCode != CHIP_NO_ERROR) {
dispatch_async(queue, ^{
MATTER_LOG_METRIC_END(kMetricOpenPairingWindow, errorCode);
completion(nil, [MTRError errorForCHIPErrorCode:errorCode]);
});
return;
}
// resultCallback will handle things now.
}
errorHandler:^(NSError * error) {
dispatch_async(queue, ^{
MATTER_LOG_METRIC_END(kMetricOpenPairingWindow, [MTRError errorToCHIPErrorCode:error]);
completion(nil, error);
});
}];
}
- (void)openCommissioningWindowWithSetupPasscode:(NSNumber *)setupPasscode
discriminator:(NSNumber *)discriminator
duration:(NSNumber *)duration
queue:(dispatch_queue_t)queue
completion:(MTRDeviceOpenCommissioningWindowHandler)completion
{
[self _openCommissioningWindowWithSetupPasscode:setupPasscode
discriminator:discriminator
duration:duration
queue:queue
completion:completion];
}
- (void)openCommissioningWindowWithDiscriminator:(NSNumber *)discriminator
duration:(NSNumber *)duration
queue:(dispatch_queue_t)queue
completion:(MTRDeviceOpenCommissioningWindowHandler)completion
{
[self _openCommissioningWindowWithSetupPasscode:nil
discriminator:discriminator
duration:duration
queue:queue
completion:completion];
}
#ifdef DEBUG
// This method is for unit testing only
- (void)failSubscribers:(dispatch_queue_t)queue completion:(void (^)(void))completion
{
MTR_LOG_DEBUG("Causing failure in subscribers on purpose");
CauseReadClientFailure(self.deviceController, self.nodeID, queue, completion);
}
// The following method is for unit testing purpose only
+ (id)CHIPEncodeAndDecodeNSObject:(id)object
{
MTRDataValueDictionaryDecodableType originalData(object);
chip::TLV::TLVWriter writer;
uint8_t buffer[1024];
writer.Init(buffer, sizeof(buffer));
CHIP_ERROR error = originalData.Encode(writer, chip::TLV::CommonTag(1));
if (error != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error: Data encoding failed: %s", error.AsString());
return nil;
}
error = writer.Finalize();
if (error != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error: TLV writer finalizing failed: %s", error.AsString());
return nil;
}
chip::TLV::TLVReader reader;
reader.Init(buffer, writer.GetLengthWritten());
error = reader.Next();
if (error != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error: TLV reader failed to fetch next element: %s", error.AsString());
return nil;
}
__auto_type tag = reader.GetTag();
if (tag != chip::TLV::CommonTag(1)) {
MTR_LOG_ERROR("Error: TLV reader did not read the tag correctly: %x.%u", chip::TLV::ProfileIdFromTag(tag),
chip::TLV::TagNumFromTag(tag));
return nil;
}
MTRDataValueDictionaryDecodableType decodedData;
error = decodedData.Decode(reader);
if (error != CHIP_NO_ERROR) {
MTR_LOG_ERROR("Error: Data decoding failed: %s", error.AsString());
return nil;
}
return decodedData.GetDecodedObject();
}
#endif
- (void)readEventsWithEndpointID:(NSNumber * _Nullable)endpointID
clusterID:(NSNumber * _Nullable)clusterID
eventID:(NSNumber * _Nullable)eventID
params:(MTRReadParams * _Nullable)params
queue:(dispatch_queue_t)queue
completion:(MTRDeviceResponseHandler)completion
{
NSArray<MTREventRequestPath *> * eventPaths = [NSArray arrayWithObject:[MTREventRequestPath requestPathWithEndpointID:endpointID
clusterID:clusterID
eventID:eventID]];
[self readAttributePaths:nil eventPaths:eventPaths params:params queue:queue completion:completion];
}
- (void)subscribeToEventsWithEndpointID:(NSNumber * _Nullable)endpointID
clusterID:(NSNumber * _Nullable)clusterID
eventID:(NSNumber * _Nullable)eventID
params:(MTRSubscribeParams * _Nullable)params
queue:(dispatch_queue_t)queue
reportHandler:(MTRDeviceResponseHandler)reportHandler
subscriptionEstablished:(MTRSubscriptionEstablishedHandler)subscriptionEstablished
{
NSArray<MTREventRequestPath *> * eventPaths = [NSArray arrayWithObject:[MTREventRequestPath requestPathWithEndpointID:endpointID
clusterID:clusterID
eventID:eventID]];
[self subscribeToAttributePaths:nil
eventPaths:eventPaths
params:params
queue:queue
reportHandler:reportHandler
subscriptionEstablished:subscriptionEstablished
resubscriptionScheduled:nil];
}
+ (NSDictionary *)eventReportForHeader:(const chip::app::EventHeader &)header andData:(id _Nullable)data
{
MTREventPath * eventPath = [[MTREventPath alloc] initWithPath:header.mPath];
if (data == nil) {
MTR_LOG_ERROR("%@ could not decode event data", eventPath);
return @{ MTREventPathKey : eventPath, MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT] };
}
// Construct the right type, and key/value depending on the type
NSNumber * eventTimeType;
NSString * timestampKey;
id timestampValue;
if (header.mTimestamp.mType == Timestamp::Type::kSystem) {
eventTimeType = @(MTREventTimeTypeSystemUpTime);
timestampKey = MTREventSystemUpTimeKey;
timestampValue = @(MTRTimeIntervalForEventTimestampValue(header.mTimestamp.mValue));
} else if (header.mTimestamp.mType == Timestamp::Type::kEpoch) {
eventTimeType = @(MTREventTimeTypeTimestampDate);
timestampKey = MTREventTimestampDateKey;
timestampValue = [NSDate dateWithTimeIntervalSince1970:MTRTimeIntervalForEventTimestampValue(header.mTimestamp.mValue)];
} else {
MTR_LOG_ERROR("%@ Unsupported event timestamp type %u - ignoring", eventPath, (unsigned int) header.mTimestamp.mType);
return @{ MTREventPathKey : eventPath, MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE] };
}
if (!MTRPriorityLevelIsValid(header.mPriorityLevel)) {
MTR_LOG_ERROR("%@ Unsupported event priority %u - ignoring", eventPath, (unsigned int) header.mPriorityLevel);
return @{ MTREventPathKey : eventPath, MTRErrorKey : [MTRError errorForCHIPErrorCode:CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE] };
}
return @{
MTREventPathKey : eventPath,
MTRDataKey : data,
MTREventNumberKey : @(header.mEventNumber),
MTREventPriorityKey : @(MTREventPriorityForValidPriorityLevel(header.mPriorityLevel)),
MTREventTimeTypeKey : eventTimeType,
timestampKey : timestampValue
};
}
+ (System::PacketBufferHandle)_responseDataForCommand:(NSDictionary<NSString *, id> *)responseValue
clusterID:(chip::ClusterId)clusterID
commandID:(chip::CommandId)commandID
error:(NSError * __autoreleasing *)error
{
if (!CheckMemberOfType(responseValue, MTRCommandPathKey, [MTRCommandPath class],
@"response-value command path is not an MTRCommandPath.", error)) {
return System::PacketBufferHandle();
}
MTRCommandPath * path = responseValue[MTRCommandPathKey];
if (![path.cluster isEqualToNumber:@(clusterID)]) {
LogStringAndReturnError([NSString stringWithFormat:@"Expected cluster id %@ but got %@", path.cluster, @(clusterID)],
MTRErrorCodeSchemaMismatch, error);
return System::PacketBufferHandle();
}
if (![path.command isEqualToNumber:@(commandID)]) {
LogStringAndReturnError([NSString stringWithFormat:@"Expected command id %@ but got %@", path.command, @(commandID)],
MTRErrorCodeSchemaMismatch, error);
return System::PacketBufferHandle();
}
if (!CheckMemberOfType(
responseValue, MTRDataKey, [NSDictionary class], @"response-value data is not a data-value dictionary.", error)) {
return System::PacketBufferHandle();
}
NSDictionary * data = responseValue[MTRDataKey];
auto buffer = System::PacketBufferHandle::New(System::PacketBuffer::kMaxSizeWithoutReserve, 0);
if (buffer.IsNull()) {
LogStringAndReturnError(@"Unable to allocate encoding buffer", CHIP_ERROR_NO_MEMORY, error);
return System::PacketBufferHandle();
}
System::PacketBufferTLVWriter writer;
// Commands never need chained buffers, since they cannot be chunked.
writer.Init(std::move(buffer), /* useChainedBuffers = */ false);
CHIP_ERROR errorCode = MTREncodeTLVFromDataValueDictionary(data, writer, TLV::AnonymousTag());
if (errorCode != CHIP_NO_ERROR) {
LogStringAndReturnError(@"Unable to encode data-value to TLV", errorCode, error);
return System::PacketBufferHandle();
}
errorCode = writer.Finalize(&buffer);
if (errorCode != CHIP_NO_ERROR) {
LogStringAndReturnError(@"Unable to encode data-value to TLV", errorCode, error);
return System::PacketBufferHandle();
}
return buffer;
}
- (void)downloadLogOfType:(MTRDiagnosticLogType)type
timeout:(NSTimeInterval)timeout
queue:(dispatch_queue_t)queue
completion:(void (^)(NSURL * _Nullable url, NSError * _Nullable error))completion
{
[_deviceController downloadLogFromNodeWithID:@(_nodeID)
type:type
timeout:timeout
queue:queue
completion:completion];
}
@end
@implementation MTRBaseDevice (Deprecated)
- (void)subscribeWithQueue:(dispatch_queue_t)queue
minInterval:(uint16_t)minInterval
maxInterval:(uint16_t)maxInterval
params:(MTRSubscribeParams * _Nullable)params
cacheContainer:(MTRAttributeCacheContainer * _Nullable)attributeCacheContainer
attributeReportHandler:(MTRDeviceReportHandler _Nullable)attributeReportHandler
eventReportHandler:(MTRDeviceReportHandler _Nullable)eventReportHandler
errorHandler:(MTRDeviceErrorHandler)errorHandler
subscriptionEstablished:(dispatch_block_t _Nullable)subscriptionEstablishedHandler
resubscriptionScheduled:(MTRDeviceResubscriptionScheduledHandler _Nullable)resubscriptionScheduledHandler
{
MTRSubscribeParams * _Nullable subscribeParams = [params copy];
if (subscribeParams == nil) {
subscribeParams = [[MTRSubscribeParams alloc] initWithMinInterval:@(minInterval) maxInterval:@(maxInterval)];
} else {
subscribeParams.minInterval = @(minInterval);
subscribeParams.maxInterval = @(maxInterval);
}
[self subscribeWithQueue:queue
params:subscribeParams
clusterStateCacheContainer:attributeCacheContainer.realContainer
attributeReportHandler:attributeReportHandler
eventReportHandler:eventReportHandler
errorHandler:errorHandler
subscriptionEstablished:subscriptionEstablishedHandler
resubscriptionScheduled:resubscriptionScheduledHandler];
}
- (void)readAttributeWithEndpointId:(NSNumber * _Nullable)endpointId
clusterId:(NSNumber * _Nullable)clusterId
attributeId:(NSNumber * _Nullable)attributeId
params:(MTRReadParams * _Nullable)params
clientQueue:(dispatch_queue_t)clientQueue
completion:(MTRDeviceResponseHandler)completion
{
[self readAttributesWithEndpointID:endpointId
clusterID:clusterId
attributeID:attributeId
params:params
queue:clientQueue
completion:completion];
}
- (void)writeAttributeWithEndpointId:(NSNumber *)endpointId
clusterId:(NSNumber *)clusterId
attributeId:(NSNumber *)attributeId
value:(id)value
timedWriteTimeout:(NSNumber * _Nullable)timeoutMs
clientQueue:(dispatch_queue_t)clientQueue
completion:(MTRDeviceResponseHandler)completion
{
[self writeAttributeWithEndpointID:endpointId
clusterID:clusterId
attributeID:attributeId
value:value
timedWriteTimeout:timeoutMs
queue:clientQueue
completion:completion];
}
- (void)invokeCommandWithEndpointId:(NSNumber *)endpointId
clusterId:(NSNumber *)clusterId
commandId:(NSNumber *)commandId
commandFields:(id)commandFields
timedInvokeTimeout:(NSNumber * _Nullable)timeoutMs
clientQueue:(dispatch_queue_t)clientQueue
completion:(MTRDeviceResponseHandler)completion
{
[self invokeCommandWithEndpointID:endpointId
clusterID:clusterId
commandID:commandId
commandFields:commandFields
timedInvokeTimeout:timeoutMs
queue:clientQueue
completion:completion];
}
- (void)subscribeAttributeWithEndpointId:(NSNumber * _Nullable)endpointId
clusterId:(NSNumber * _Nullable)clusterId
attributeId:(NSNumber * _Nullable)attributeId
minInterval:(NSNumber *)minInterval
maxInterval:(NSNumber *)maxInterval
params:(MTRSubscribeParams * _Nullable)params
clientQueue:(dispatch_queue_t)clientQueue
reportHandler:(MTRDeviceResponseHandler)reportHandler
subscriptionEstablished:(dispatch_block_t _Nullable)subscriptionEstablishedHandler
{
MTRSubscribeParams * _Nullable subscribeParams = [params copy];
if (subscribeParams == nil) {
subscribeParams = [[MTRSubscribeParams alloc] initWithMinInterval:minInterval maxInterval:maxInterval];
} else {
subscribeParams.minInterval = minInterval;
subscribeParams.maxInterval = maxInterval;
}
[self subscribeToAttributesWithEndpointID:endpointId
clusterID:clusterId
attributeID:attributeId
params:subscribeParams
queue:clientQueue
reportHandler:reportHandler
subscriptionEstablished:subscriptionEstablishedHandler];
}
- (void)deregisterReportHandlersWithClientQueue:(dispatch_queue_t)queue completion:(dispatch_block_t)completion
{
[self deregisterReportHandlersWithQueue:queue completion:completion];
}
@end
@implementation MTRAttributeRequestPath
- (instancetype)initWithEndpointID:(NSNumber * _Nullable)endpointID
clusterID:(NSNumber * _Nullable)clusterID
attributeID:(NSNumber * _Nullable)attributeID
{
_endpoint = [endpointID copy];
_cluster = [clusterID copy];
_attribute = [attributeID copy];
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<MTRAttributeRequestPath endpoint %u cluster %u attribute %u>",
(uint16_t) _endpoint.unsignedShortValue, (uint32_t) _cluster.unsignedLongValue,
(uint32_t) _attribute.unsignedLongValue];
}
+ (MTRAttributeRequestPath *)requestPathWithEndpointID:(NSNumber * _Nullable)endpointID
clusterID:(NSNumber * _Nullable)clusterID
attributeID:(NSNumber * _Nullable)attributeID
{
return [[MTRAttributeRequestPath alloc] initWithEndpointID:endpointID clusterID:clusterID attributeID:attributeID];
}
- (BOOL)isEqualToAttributeRequestPath:(MTRAttributeRequestPath *)path
{
return [_endpoint isEqualToNumber:path.endpoint] && [_cluster isEqualToNumber:path.cluster] &&
[_attribute isEqualToNumber:path.attribute];
}
- (BOOL)isEqual:(id)object
{
if ([object class] != [self class]) {
return NO;
}
return [self isEqualToAttributeRequestPath:object];
}
- (NSUInteger)hash
{
return _endpoint.unsignedShortValue ^ _cluster.unsignedLongValue ^ _attribute.unsignedLongValue;
}
- (id)copyWithZone:(NSZone *)zone
{
return [MTRAttributeRequestPath requestPathWithEndpointID:_endpoint clusterID:_cluster attributeID:_attribute];
}
- (void)convertToAttributePathParams:(chip::app::AttributePathParams &)params
{
if (_endpoint != nil) {
params.mEndpointId = static_cast<chip::EndpointId>(_endpoint.unsignedShortValue);
} else {
params.SetWildcardEndpointId();
}
if (_cluster != nil) {
params.mClusterId = static_cast<chip::ClusterId>(_cluster.unsignedLongValue);
} else {
params.SetWildcardClusterId();
}
if (_attribute != nil) {
params.mAttributeId = static_cast<chip::AttributeId>(_attribute.unsignedLongValue);
} else {
params.SetWildcardAttributeId();
}
}
@end
@implementation MTREventRequestPath
- (instancetype)initWithEndpointID:(NSNumber * _Nullable)endpointID
clusterID:(NSNumber * _Nullable)clusterID
eventID:(NSNumber * _Nullable)eventID
{
_endpoint = [endpointID copy];
_cluster = [clusterID copy];
_event = [eventID copy];
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<MTREventRequestPath endpoint %u cluster %u event %u>",
(uint16_t) _endpoint.unsignedShortValue, (uint32_t) _cluster.unsignedLongValue,
(uint32_t) _event.unsignedLongValue];
}
+ (MTREventRequestPath *)requestPathWithEndpointID:(NSNumber * _Nullable)endpointID
clusterID:(NSNumber * _Nullable)clusterID
eventID:(NSNumber * _Nullable)eventID
{
return [[MTREventRequestPath alloc] initWithEndpointID:endpointID clusterID:clusterID eventID:eventID];
}
- (BOOL)isEqualToEventRequestPath:(MTREventRequestPath *)path
{
return
[_endpoint isEqualToNumber:path.endpoint] && [_cluster isEqualToNumber:path.cluster] && [_event isEqualToNumber:path.event];
}
- (BOOL)isEqual:(id)object
{
if ([object class] != [self class]) {
return NO;
}
return [self isEqualToEventRequestPath:object];
}
- (NSUInteger)hash
{
return _endpoint.unsignedShortValue ^ _cluster.unsignedLongValue ^ _event.unsignedLongValue;
}
- (id)copyWithZone:(NSZone *)zone
{
return [MTREventRequestPath requestPathWithEndpointID:_endpoint clusterID:_cluster eventID:_event];
}
- (void)convertToEventPathParams:(chip::app::EventPathParams &)params
{
if (_endpoint != nil) {
params.mEndpointId = static_cast<chip::EndpointId>(_endpoint.unsignedShortValue);
} else {
params.SetWildcardEndpointId();
}
if (_cluster != nil) {
params.mClusterId = static_cast<chip::ClusterId>(_cluster.unsignedLongValue);
} else {
params.SetWildcardClusterId();
}
if (_event != nil) {
params.mEventId = static_cast<chip::EventId>(_event.unsignedLongValue);
} else {
params.SetWildcardEventId();
}
}
@end
@implementation MTRClusterPath
- (instancetype)initWithPath:(const ConcreteClusterPath &)path
{
if (self = [super init]) {
_endpoint = @(path.mEndpointId);
_cluster = @(path.mClusterId);
}
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<MTRClusterPath endpoint %u cluster %u>", (uint16_t) _endpoint.unsignedShortValue,
(uint32_t) _cluster.unsignedLongValue];
}
+ (MTRClusterPath *)clusterPathWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID
{
ConcreteClusterPath path(static_cast<chip::EndpointId>([endpointID unsignedShortValue]),
static_cast<chip::ClusterId>([clusterID unsignedLongValue]));
return [[MTRClusterPath alloc] initWithPath:path];
}
- (BOOL)isEqualToClusterPath:(MTRClusterPath *)clusterPath
{
return [_endpoint isEqualToNumber:clusterPath.endpoint] && [_cluster isEqualToNumber:clusterPath.cluster];
}
- (BOOL)isEqual:(id)object
{
if ([object class] != [self class]) {
return NO;
}
return [self isEqualToClusterPath:object];
}
- (NSUInteger)hash
{
return _endpoint.unsignedShortValue ^ _cluster.unsignedLongValue;
}
- (id)copyWithZone:(NSZone *)zone
{
return [MTRClusterPath clusterPathWithEndpointID:_endpoint clusterID:_cluster];
}
static NSString * const sEndpointKey = @"endpointKey";
static NSString * const sClusterKey = @"clusterKey";
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (nullable instancetype)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if (self == nil) {
return nil;
}
_endpoint = [decoder decodeObjectOfClass:[NSNumber class] forKey:sEndpointKey];
if (_endpoint && ![_endpoint isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("MTRClusterPath decoded %@ for endpoint, not NSNumber.", _endpoint);
return nil;
}
_cluster = [decoder decodeObjectOfClass:[NSNumber class] forKey:sClusterKey];
if (_cluster && ![_cluster isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("MTRClusterPath decoded %@ for cluster, not NSNumber.", _cluster);
return nil;
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:_endpoint forKey:sEndpointKey];
[coder encodeObject:_cluster forKey:sClusterKey];
}
@end
@implementation MTRAttributePath
- (instancetype)initWithPath:(const ConcreteDataAttributePath &)path
{
if (self = [super initWithPath:path]) {
_attribute = @(path.mAttributeId);
}
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<MTRAttributePath endpoint %u cluster %u attribute %u>",
(uint16_t) self.endpoint.unsignedShortValue, (uint32_t) self.cluster.unsignedLongValue,
(uint32_t) _attribute.unsignedLongValue];
}
+ (MTRAttributePath *)attributePathWithEndpointID:(NSNumber *)endpointID
clusterID:(NSNumber *)clusterID
attributeID:(NSNumber *)attributeID
{
ConcreteDataAttributePath path(static_cast<chip::EndpointId>([endpointID unsignedShortValue]),
static_cast<chip::ClusterId>([clusterID unsignedLongValue]),
static_cast<chip::AttributeId>([attributeID unsignedLongValue]));
return [[MTRAttributePath alloc] initWithPath:path];
}
- (BOOL)isEqualToAttributePath:(MTRAttributePath *)attributePath
{
return [self isEqualToClusterPath:attributePath] && [_attribute isEqualToNumber:attributePath.attribute];
}
- (BOOL)isEqual:(id)object
{
if ([object class] != [self class]) {
return NO;
}
return [self isEqualToAttributePath:object];
}
- (NSUInteger)hash
{
return self.endpoint.unsignedShortValue ^ self.cluster.unsignedLongValue ^ _attribute.unsignedLongValue;
}
- (id)copyWithZone:(NSZone *)zone
{
return [MTRAttributePath attributePathWithEndpointID:self.endpoint clusterID:self.cluster attributeID:_attribute];
}
- (ConcreteAttributePath)_asConcretePath
{
return ConcreteAttributePath([self.endpoint unsignedShortValue], static_cast<ClusterId>([self.cluster unsignedLongValue]),
static_cast<AttributeId>([self.attribute unsignedLongValue]));
}
static NSString * const sAttributeKey = @"attributeKey";
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (nullable instancetype)initWithCoder:(NSCoder *)decoder
{
self = [super initWithCoder:decoder];
if (self == nil) {
return nil;
}
_attribute = [decoder decodeObjectOfClass:[NSNumber class] forKey:sAttributeKey];
if (_attribute && ![_attribute isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("MTRAttributePath decoded %@ for attribute, not NSNumber.", _attribute);
return nil;
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:_attribute forKey:sAttributeKey];
}
@end
@implementation MTRAttributePath (Deprecated)
+ (instancetype)attributePathWithEndpointId:(NSNumber *)endpointId
clusterId:(NSNumber *)clusterId
attributeId:(NSNumber *)attributeId
{
return [self attributePathWithEndpointID:endpointId clusterID:clusterId attributeID:attributeId];
}
@end
@implementation MTREventPath
- (instancetype)initWithPath:(const ConcreteEventPath &)path
{
if (self = [super initWithPath:path]) {
_event = @(path.mEventId);
}
return self;
}
- (NSString *)description
{
return
[NSString stringWithFormat:@"<MTREventPath endpoint %u cluster %u event %u>", (uint16_t) self.endpoint.unsignedShortValue,
(uint32_t) self.cluster.unsignedLongValue, (uint32_t) _event.unsignedLongValue];
}
+ (MTREventPath *)eventPathWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID eventID:(NSNumber *)eventID
{
ConcreteEventPath path(static_cast<chip::EndpointId>([endpointID unsignedShortValue]),
static_cast<chip::ClusterId>([clusterID unsignedLongValue]), static_cast<chip::EventId>([eventID unsignedLongValue]));
return [[MTREventPath alloc] initWithPath:path];
}
- (BOOL)isEqualToEventPath:(MTREventPath *)eventPath
{
return [self isEqualToClusterPath:eventPath] && [_event isEqualToNumber:eventPath.event];
}
- (BOOL)isEqual:(id)object
{
if ([object class] != [self class]) {
return NO;
}
return [self isEqualToEventPath:object];
}
- (NSUInteger)hash
{
return self.endpoint.unsignedShortValue ^ self.cluster.unsignedLongValue ^ _event.unsignedLongValue;
}
- (id)copyWithZone:(NSZone *)zone
{
return [MTREventPath eventPathWithEndpointID:self.endpoint clusterID:self.cluster eventID:_event];
}
- (ConcreteEventPath)_asConcretePath
{
return ConcreteEventPath([self.endpoint unsignedShortValue], static_cast<ClusterId>([self.cluster unsignedLongValue]),
static_cast<EventId>([self.event unsignedLongValue]));
}
@end
@implementation MTREventPath (Deprecated)
+ (instancetype)eventPathWithEndpointId:(NSNumber *)endpointId clusterId:(NSNumber *)clusterId eventId:(NSNumber *)eventId
{
return [self eventPathWithEndpointID:endpointId clusterID:clusterId eventID:eventId];
}
@end
@implementation MTRCommandPath
- (instancetype)initWithPath:(const ConcreteCommandPath &)path
{
if (self = [super initWithPath:path]) {
_command = @(path.mCommandId);
}
return self;
}
- (NSString *)description
{
return
[NSString stringWithFormat:@"<MTRCommandPath endpoint %u cluster %lu command %lu>", self.endpoint.unsignedShortValue,
self.cluster.unsignedLongValue, _command.unsignedLongValue];
}
+ (MTRCommandPath *)commandPathWithEndpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID commandID:(NSNumber *)commandID
{
ConcreteCommandPath path(static_cast<chip::EndpointId>([endpointID unsignedShortValue]),
static_cast<chip::ClusterId>([clusterID unsignedLongValue]), static_cast<chip::CommandId>([commandID unsignedLongValue]));
return [[MTRCommandPath alloc] initWithPath:path];
}
- (BOOL)isEqualToCommandPath:(MTRCommandPath *)commandPath
{
return [self isEqualToClusterPath:commandPath] && [_command isEqualToNumber:commandPath.command];
}
- (BOOL)isEqual:(id)object
{
if ([object class] != [self class]) {
return NO;
}
return [self isEqualToCommandPath:object];
}
- (NSUInteger)hash
{
return self.endpoint.unsignedShortValue ^ self.cluster.unsignedLongValue ^ _command.unsignedLongValue;
}
- (id)copyWithZone:(NSZone *)zone
{
return [MTRCommandPath commandPathWithEndpointID:self.endpoint clusterID:self.cluster commandID:_command];
}
@end
@implementation MTRCommandPath (Deprecated)
+ (instancetype)commandPathWithEndpointId:(NSNumber *)endpointId clusterId:(NSNumber *)clusterId commandId:(NSNumber *)commandId
{
return [self commandPathWithEndpointID:endpointId clusterID:clusterId commandID:commandId];
}
@end
static void LogStringAndReturnError(NSString * errorStr, MTRErrorCode errorCode, NSError * __autoreleasing * error)
{
MTR_LOG_ERROR("%s", errorStr.UTF8String);
if (!error) {
return;
}
NSDictionary * userInfo = @ { NSLocalizedFailureReasonErrorKey : NSLocalizedString(errorStr, nil) };
*error = [NSError errorWithDomain:MTRErrorDomain code:errorCode userInfo:userInfo];
}
static void LogStringAndReturnError(NSString * errorStr, CHIP_ERROR errorCode, NSError * __autoreleasing * error)
{
MTR_LOG_ERROR("%s: %s", errorStr.UTF8String, errorCode.AsString());
if (!error) {
return;
}
*error = [MTRError errorForCHIPErrorCode:errorCode];
}
static bool CheckMemberOfType(NSDictionary<NSString *, id> * responseValue, NSString * memberName, Class expectedClass,
NSString * errorMessage, NSError * __autoreleasing * error)
{
id _Nullable value = responseValue[memberName];
if (value == nil) {
LogStringAndReturnError([NSString stringWithFormat:@"%s is null when not expected to be", memberName.UTF8String],
MTRErrorCodeInvalidArgument, error);
return false;
}
if (![value isKindOfClass:expectedClass]) {
LogStringAndReturnError(errorMessage, MTRErrorCodeInvalidArgument, error);
return false;
}
return true;
}
// Allocates a buffer, encodes the data-value as TLV, and points the TLV::Reader
// to the data. Returns false if any of that fails, in which case error gets
// set.
//
// Data model decoding requires a contiguous buffer (because lists walk all the
// data multiple times and TLVPacketBufferBackingStore doesn't have a way to
// checkpoint and restore its state), but we can encode into chained packet
// buffers and then decide whether we need a contiguous realloc.
static bool EncodeDataValueToTLV(System::PacketBufferHandle & buffer, Platform::ScopedMemoryBuffer<uint8_t> & flatBuffer,
NSDictionary * data, TLV::TLVReader & reader, NSError * __autoreleasing * error)
{
buffer = System::PacketBufferHandle::New(System::PacketBuffer::kMaxSizeWithoutReserve, 0);
if (buffer.IsNull()) {
LogStringAndReturnError(@"Unable to allocate encoding buffer", CHIP_ERROR_NO_MEMORY, error);
return false;
}
System::PacketBufferTLVWriter writer;
writer.Init(std::move(buffer), /* useChainedBuffers = */ true);
CHIP_ERROR errorCode = MTREncodeTLVFromDataValueDictionary(data, writer, TLV::AnonymousTag());
if (errorCode != CHIP_NO_ERROR) {
LogStringAndReturnError(@"Unable to encode data-value to TLV", errorCode, error);
return false;
}
errorCode = writer.Finalize(&buffer);
if (errorCode != CHIP_NO_ERROR) {
LogStringAndReturnError(@"Unable to encode data-value to TLV", errorCode, error);
return false;
}
if (buffer->HasChainedBuffer()) {
// We need to reallocate into a single contiguous buffer.
size_t remainingData = buffer->TotalLength();
if (!flatBuffer.Calloc(remainingData)) {
LogStringAndReturnError(@"Unable to allocate decoding buffer", CHIP_ERROR_NO_MEMORY, error);
return false;
}
size_t copiedData = 0;
while (!buffer.IsNull()) {
if (buffer->DataLength() > remainingData) {
// Should never happen, but let's be extra careful about buffer
// overruns.
LogStringAndReturnError(@"Encoding buffer size is bigger than it claimed", CHIP_ERROR_INCORRECT_STATE, error);
return false;
}
memcpy(flatBuffer.Get() + copiedData, buffer->Start(), buffer->DataLength());
copiedData += buffer->DataLength();
remainingData -= buffer->DataLength();
buffer.Advance();
}
if (remainingData != 0) {
LogStringAndReturnError(
@"Did not copy all data from Encoding buffer for some reason", CHIP_ERROR_INCORRECT_STATE, error);
return false;
}
reader.Init(flatBuffer.Get(), copiedData);
} else {
reader.Init(buffer->Start(), buffer->DataLength());
}
errorCode = reader.Next(TLV::AnonymousTag());
if (errorCode != CHIP_NO_ERROR) {
LogStringAndReturnError(@"data-value TLV encoding did not create a TLV element", errorCode, error);
return false;
}
return true;
}
@implementation MTRAttributeReport
+ (void)initialize
{
// One of our init methods ends up doing Platform::MemoryAlloc.
MTRFrameworkInit();
}
- (instancetype)initWithPath:(const ConcreteDataAttributePath &)path value:(id _Nullable)value error:(NSError * _Nullable)error
{
if (self = [super init]) {
_path = [[MTRAttributePath alloc] initWithPath:path];
_value = value;
_error = error;
}
return self;
}
- (nullable instancetype)initWithResponseValue:(NSDictionary<NSString *, id> *)responseValue
error:(NSError * __autoreleasing *)error
{
if (!(self = [super init])) {
return nil;
}
// In theory, the types of all the things in the dictionary will be correct
// if our consumer passes in an actual response-value dictionary, but
// double-check just to be sure
if (!CheckMemberOfType(responseValue, MTRAttributePathKey, [MTRAttributePath class],
@"response-value attribute path is not an MTRAttributePath.", error)) {
return nil;
}
MTRAttributePath * path = responseValue[MTRAttributePathKey];
id _Nullable value = responseValue[MTRErrorKey];
if (value != nil) {
if (!CheckMemberOfType(responseValue, MTRErrorKey, [NSError class], @"response-value error is not an NSError.", error)) {
return nil;
}
_path = path;
_value = nil;
_error = value;
return self;
}
if (!CheckMemberOfType(
responseValue, MTRDataKey, [NSDictionary class], @"response-value data is not a data-value dictionary.", error)) {
return nil;
}
NSDictionary * data = responseValue[MTRDataKey];
// Encode the data to TLV and then decode from that, to reuse existing code.
System::PacketBufferHandle buffer;
Platform::ScopedMemoryBuffer<uint8_t> flatBuffer;
TLV::TLVReader reader;
if (!EncodeDataValueToTLV(buffer, flatBuffer, data, reader, error)) {
return nil;
}
auto attributePath = [path _asConcretePath];
CHIP_ERROR errorCode = CHIP_ERROR_INTERNAL;
id decodedValue = MTRDecodeAttributeValue(attributePath, reader, &errorCode);
if (errorCode == CHIP_NO_ERROR) {
_path = path;
_value = decodedValue;
_error = nil;
return self;
}
if (errorCode == CHIP_ERROR_IM_MALFORMED_ATTRIBUTE_PATH_IB) {
LogStringAndReturnError(@"No known schema for decoding attribute value.", MTRErrorCodeUnknownSchema, error);
return nil;
}
// Treat all other errors as schema errors.
LogStringAndReturnError(@"Attribute decoding failed schema check.", MTRErrorCodeSchemaMismatch, error);
return nil;
}
- (id)copyWithZone:(NSZone *)zone
{
return [[MTRAttributeReport alloc] initWithPath:[self.path _asConcretePath] value:self.value error:self.error];
}
@end
@implementation MTREventReport {
NSNumber * _timestampValue;
}
+ (void)initialize
{
// One of our init methods ends up doing Platform::MemoryAlloc.
MTRFrameworkInit();
}
- (instancetype)initWithPath:(const chip::app::ConcreteEventPath &)path
eventNumber:(NSNumber *)eventNumber
priority:(PriorityLevel)priority
timestamp:(const Timestamp &)timestamp
value:(id)value
{
if (self = [super init]) {
_path = [[MTREventPath alloc] initWithPath:path];
_eventNumber = eventNumber;
if (!MTRPriorityLevelIsValid(priority)) {
return nil;
}
_priority = @(MTREventPriorityForValidPriorityLevel(priority));
_timestampValue = @(timestamp.mValue);
if (timestamp.IsSystem()) {
_eventTimeType = MTREventTimeTypeSystemUpTime;
_systemUpTime = MTRTimeIntervalForEventTimestampValue(timestamp.mValue);
} else if (timestamp.IsEpoch()) {
_eventTimeType = MTREventTimeTypeTimestampDate;
_timestampDate = [NSDate dateWithTimeIntervalSince1970:MTRTimeIntervalForEventTimestampValue(timestamp.mValue)];
} else {
return nil;
}
_value = value;
_error = nil;
}
return self;
}
- (instancetype)initWithPath:(const chip::app::ConcreteEventPath &)path error:(NSError *)error
{
if (self = [super init]) {
_path = [[MTREventPath alloc] initWithPath:path];
// Use some sort of initialized values for our members, even though
// those values are meaningless in this case.
_eventNumber = @(0);
_priority = @(MTREventPriorityDebug);
_eventTimeType = MTREventTimeTypeSystemUpTime;
_systemUpTime = 0;
_timestampDate = nil;
_value = nil;
_error = error;
}
return self;
}
- (nullable instancetype)initWithResponseValue:(NSDictionary<NSString *, id> *)responseValue
error:(NSError * __autoreleasing *)error
{
if (!(self = [super init])) {
return nil;
}
// In theory, the types of all the things in the dictionary will be correct
// if our consumer passes in an actual response-value dictionary, but
// double-check just to be sure
if (!CheckMemberOfType(
responseValue, MTREventPathKey, [MTREventPath class], @"response-value event path is not an MTREventPath.", error)) {
return nil;
}
MTREventPath * path = responseValue[MTREventPathKey];
id _Nullable value = responseValue[MTRErrorKey];
if (value != nil) {
if (!CheckMemberOfType(responseValue, MTRErrorKey, [NSError class], @"response-value error is not an NSError.", error)) {
return nil;
}
return [self initWithPath:[path _asConcretePath] error:value];
}
if (!CheckMemberOfType(
responseValue, MTRDataKey, [NSDictionary class], @"response-value data is not a data-value dictionary.", error)) {
return nil;
}
NSDictionary * data = responseValue[MTRDataKey];
// Encode the data to TLV and then decode from that, to reuse existing code.
System::PacketBufferHandle buffer;
Platform::ScopedMemoryBuffer<uint8_t> flatBuffer;
TLV::TLVReader reader;
if (!EncodeDataValueToTLV(buffer, flatBuffer, data, reader, error)) {
return nil;
}
auto eventPath = [path _asConcretePath];
CHIP_ERROR errorCode = CHIP_ERROR_INTERNAL;
id decodedValue = MTRDecodeEventPayload(eventPath, reader, &errorCode);
if (errorCode == CHIP_NO_ERROR) {
// Validate our other members.
if (!CheckMemberOfType(
responseValue, MTREventNumberKey, [NSNumber class], @"response-value event number is not an NSNumber", error)) {
return nil;
}
_eventNumber = responseValue[MTREventNumberKey];
if (!CheckMemberOfType(
responseValue, MTREventPriorityKey, [NSNumber class], @"response-value event priority is not an NSNumber", error)) {
return nil;
}
_priority = responseValue[MTREventPriorityKey];
if (!CheckMemberOfType(responseValue, MTREventTimeTypeKey, [NSNumber class],
@"response-value event time type is not an NSNumber", error)) {
return nil;
}
NSNumber * wrappedTimeType = responseValue[MTREventTimeTypeKey];
if (wrappedTimeType.unsignedIntegerValue == MTREventTimeTypeSystemUpTime) {
if (!CheckMemberOfType(responseValue, MTREventSystemUpTimeKey, [NSNumber class],
@"response-value event system uptime time is not an NSNumber", error)) {
return nil;
}
NSNumber * wrappedSystemTime = responseValue[MTREventSystemUpTimeKey];
_systemUpTime = wrappedSystemTime.doubleValue;
} else if (wrappedTimeType.unsignedIntegerValue == MTREventTimeTypeTimestampDate) {
if (!CheckMemberOfType(responseValue, MTREventTimestampDateKey, [NSDate class],
@"response-value event timestampe is not an NSDate", error)) {
return nil;
}
_timestampDate = responseValue[MTREventTimestampDateKey];
} else {
LogStringAndReturnError([NSString stringWithFormat:@"Invalid event time type: %lu", wrappedTimeType.unsignedLongValue],
MTRErrorCodeInvalidArgument, error);
return nil;
}
_eventTimeType = static_cast<MTREventTimeType>(wrappedTimeType.unsignedIntegerValue);
_path = path;
_value = decodedValue;
_error = nil;
return self;
}
if (errorCode == CHIP_ERROR_IM_MALFORMED_EVENT_PATH_IB) {
LogStringAndReturnError(@"No known schema for decoding event payload.", MTRErrorCodeUnknownSchema, error);
return nil;
}
// Treat all other errors as schema errors.
LogStringAndReturnError(@"Event payload decoding failed schema check.", MTRErrorCodeSchemaMismatch, error);
return nil;
}
@end
@implementation MTREventReport (Deprecated)
- (NSNumber *)timestamp
{
return _timestampValue;
}
@end
namespace {
void SubscriptionCallback::OnEventData(const EventHeader & aEventHeader, TLV::TLVReader * apData, const StatusIB * apStatus)
{
id _Nullable value = nil;
NSError * _Nullable error = nil;
if (apStatus != nullptr) {
error = [MTRError errorForIMStatus:*apStatus];
} else if (apData == nullptr) {
error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT];
} else {
CHIP_ERROR err;
value = MTRDecodeEventPayload(aEventHeader.mPath, *apData, &err);
if (err == CHIP_ERROR_IM_MALFORMED_EVENT_PATH_IB) {
// We don't know this event; just skip it.
return;
}
if (err != CHIP_NO_ERROR) {
value = nil;
error = [MTRError errorForCHIPErrorCode:err];
}
}
if (mEventReports == nil) {
// Never got a OnReportBegin? Not much to do other than tear things down.
ReportError(CHIP_ERROR_INCORRECT_STATE);
return;
}
if (error != nil) {
[mEventReports addObject:[[MTREventReport alloc] initWithPath:aEventHeader.mPath error:error]];
} else {
[mEventReports addObject:[[MTREventReport alloc] initWithPath:aEventHeader.mPath
eventNumber:@(aEventHeader.mEventNumber)
priority:aEventHeader.mPriorityLevel
timestamp:aEventHeader.mTimestamp
value:value]];
}
}
void SubscriptionCallback::OnAttributeData(
const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus)
{
if (aPath.IsListItemOperation()) {
ReportError(CHIP_ERROR_INCORRECT_STATE);
return;
}
id _Nullable value = nil;
NSError * _Nullable error = nil;
if (aStatus.mStatus != Status::Success) {
error = [MTRError errorForIMStatus:aStatus];
} else if (apData == nullptr) {
error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT];
} else {
CHIP_ERROR err;
value = MTRDecodeAttributeValue(aPath, *apData, &err);
if (err == CHIP_ERROR_IM_MALFORMED_ATTRIBUTE_PATH_IB) {
// We don't know this attribute; just skip it.
return;
}
if (err != CHIP_NO_ERROR) {
value = nil;
error = [MTRError errorForCHIPErrorCode:err];
}
}
if (mAttributeReports == nil) {
// Never got a OnReportBegin? Not much to do other than tear things down.
ReportError(CHIP_ERROR_INCORRECT_STATE);
return;
}
[mAttributeReports addObject:[[MTRAttributeReport alloc] initWithPath:aPath value:value error:error]];
}
} // anonymous namespace