3 * Copyright (c) 2020 Project CHIP Authors
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
20 * Copyright (c) 2020 Silicon Labs
22 * Licensed under the Apache License, Version 2.0 (the "License");
23 * you may not use this file except in compliance with the License.
24 * You may obtain a copy of the License at
26 * http://www.apache.org/licenses/LICENSE-2.0
28 * Unless required by applicable law or agreed to in writing, software
29 * distributed under the License is distributed on an "AS IS" BASIS,
30 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 * See the License for the specific language governing permissions and
32 * limitations under the License.
34 /****************************************************************************
36 * @brief Routines for the Reporting plugin, which
37 *sends asynchronous reports when a ZCL attribute's
39 *******************************************************************************
40 ******************************************************************************/
42 #include "reporting.h"
43 #include <app/util/af-event.h>
44 #include <app/util/af.h>
45 #include <app/util/attribute-storage.h>
46 #include <app/util/binding-table.h>
47 #include <app/util/common.h>
48 #include <support/SafeInt.h>
49 #include <system/SystemLayer.h>
51 #include "gen/attribute-type.h"
52 #include "gen/cluster-id.h"
53 #include "gen/command-id.h"
57 // TODO: Need to figure out what needs to happen wrt HAL tokens here, but for
58 // now define ESZP_HOST to disable it. See
59 // https://github.com/project-chip/connectedhomeip/issues/3275
62 #ifdef ATTRIBUTE_LARGEST
63 #define READ_DATA_SIZE ATTRIBUTE_LARGEST
65 #define READ_DATA_SIZE 8 // max size if attributes aren't present
68 #define NULL_INDEX 0xFF
70 static void conditionallySendReport(EndpointId endpoint, ClusterId clusterId);
71 static void scheduleTick(void);
72 static void removeConfiguration(uint8_t index);
73 static void removeConfigurationAndScheduleTick(uint8_t index);
74 static EmberAfStatus configureReceivedAttribute(const EmberAfClusterCommand * cmd, AttributeId attributeId, uint8_t mask,
76 static void putReportableChangeInResp(const EmberAfPluginReportingEntry * entry, EmberAfAttributeType dataType);
77 static void retrySendReport(EmberOutgoingMessageType type, uint64_t indexOrDestination, EmberApsFrame * apsFrame, uint16_t msgLen,
78 uint8_t * message, EmberStatus status);
79 static uint32_t computeStringHash(uint8_t * data, uint8_t length);
81 EmberEventControl emberAfPluginReportingTickEventControl;
83 EmAfPluginReportVolatileData emAfPluginReportVolatileData[REPORT_TABLE_SIZE];
87 * This callback is called by the Reporting plugin whenever a reporting entry
88 * is configured, including when entries are deleted or updated. The
89 * application can use this callback for scheduling readings or measurements
90 * based on the minimum and maximum reporting interval for the entry. The
91 * application should return EMBER_ZCL_STATUS_SUCCESS if it can support the
92 * configuration or an error status otherwise. Note: attribute reporting is
93 * required for many clusters and attributes, so rejecting a reporting
94 * configuration may violate ZigBee specifications.
96 * @param entry Ver.: always
98 EmberAfStatus emberAfPluginReportingConfiguredCallback(const EmberAfPluginReportingEntry * entry)
100 return EMBER_ZCL_STATUS_SUCCESS;
103 static void retrySendReport(EmberOutgoingMessageType type, uint64_t indexOrDestination, EmberApsFrame * apsFrame, uint16_t msgLen,
104 uint8_t * message, EmberStatus status)
106 // Retry once, and do so by unicasting without a pointer to this callback
107 if (status != EMBER_SUCCESS)
109 emberAfSendUnicast(type, indexOrDestination, apsFrame, msgLen, message);
113 // Implementation based on public domain Fowler/Noll/Vo FNV-1a hash function:
114 // http://isthe.com/chongo/tech/comp/fnv/
115 // https://tools.ietf.org/html/draft-eastlake-fnv-14
116 // https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
118 // Used to save and compare hashes of CHAR and OCTET string values in order to detect
119 // reportable changes. The strings themselves are longer than the storage size.
120 #define FNV1_OFFSET_BASIS (2166136261)
121 #define FNV1_PRIME (16777619)
122 static uint32_t computeStringHash(uint8_t * data, uint8_t length)
124 // FNV-1a, 32-bit hash
125 uint32_t hash = FNV1_OFFSET_BASIS;
126 for (int i = 0; i < length; ++i)
129 hash *= FNV1_PRIME; // Or, hash += (hash<<1) + (hash<<4) + (hash<<7) + (hash<<8) + (hash<<24);
135 #if REPORT_TABLE_SIZE != 0
136 static EmberAfPluginReportingEntry table[REPORT_TABLE_SIZE];
138 void emAfPluginReportingGetEntry(uint8_t index, EmberAfPluginReportingEntry * result)
140 #if REPORT_TABLE_SIZE != 0
141 memmove(result, &table[index], sizeof(EmberAfPluginReportingEntry));
144 void emAfPluginReportingSetEntry(uint8_t index, EmberAfPluginReportingEntry * value)
146 #if REPORT_TABLE_SIZE != 0
147 memmove(&table[index], value, sizeof(EmberAfPluginReportingEntry));
151 void emAfPluginReportingGetEntry(uint8_t index, EmberAfPluginReportingEntry * result)
153 // TODO: Once https://github.com/project-chip/connectedhomeip/issues/2470 is
154 // fixed this manual marking of the entry as unused can probably be removed.
155 result->endpoint = EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID;
156 halCommonGetIndexedToken(result, TOKEN_REPORT_TABLE, index);
158 void emAfPluginReportingSetEntry(uint8_t index, EmberAfPluginReportingEntry * value)
160 halCommonSetIndexedToken(TOKEN_REPORT_TABLE, index, value);
164 void emberAfPluginReportingStackStatusCallback(EmberStatus status)
166 if (status == EMBER_NETWORK_UP)
168 // Load default reporting configurations
169 emberAfPluginReportingLoadReportingConfigDefaults();
175 void emberAfPluginReportingInitCallback(void)
177 // On device initialization, any attributes that have been set up to report
178 // should generate an attribute report.
179 for (uint8_t i = 0; i < REPORT_TABLE_SIZE; i++)
181 EmberAfPluginReportingEntry entry;
182 emAfPluginReportingGetEntry(i, &entry);
183 if (entry.endpoint != EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID &&
184 entry.direction == EMBER_ZCL_REPORTING_DIRECTION_REPORTED)
186 emAfPluginReportVolatileData[i].reportableChange = true;
193 void emberAfPluginReportingTickEventHandler(void)
195 EmberApsFrame * apsFrame = NULL;
196 EmberAfStatus status;
197 EmberAfAttributeType dataType;
198 uint16_t manufacturerCode = 0;
199 uint8_t readData[READ_DATA_SIZE];
202 bool clientToServer = false;
203 EmberBindingTableEntry bindingEntry;
204 // reportSize needs to be able to fit a sum of dataSize and some other stuff
205 // without overflowing.
207 uint8_t index, currentPayloadMaxLength = 0, smallestPayloadMaxLength = 0;
209 for (i = 0; i < REPORT_TABLE_SIZE; i++)
211 EmberAfPluginReportingEntry entry;
212 // Not initializing entry.mask causes errors even if wrapped with GCC diagnostic ignored
213 entry.mask = CLUSTER_MASK_SERVER;
215 emAfPluginReportingGetEntry(i, &entry);
216 // We will only send reports for active reported attributes and only if a
217 // reportable change has occurred and the minimum interval has elapsed or
218 // if the maximum interval is set and has elapsed.
220 elapsedTimeInt32u(emAfPluginReportVolatileData[i].lastReportTimeMs, chip::System::Layer::GetClock_MonotonicMS());
221 if (entry.endpoint == EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID ||
222 entry.direction != EMBER_ZCL_REPORTING_DIRECTION_REPORTED ||
223 (elapsedMs < entry.data.reported.minInterval * MILLISECOND_TICKS_PER_SECOND) ||
224 (!emAfPluginReportVolatileData[i].reportableChange &&
225 (entry.data.reported.maxInterval == 0 ||
226 (elapsedMs < (entry.data.reported.maxInterval * MILLISECOND_TICKS_PER_SECOND)))))
231 status = emAfReadAttribute(entry.endpoint, entry.clusterId, entry.attributeId, entry.mask, entry.manufacturerCode,
232 (uint8_t *) &readData, READ_DATA_SIZE, &dataType);
233 if (status != EMBER_ZCL_STATUS_SUCCESS)
235 emberAfReportingPrintln("ERR: reading cluster 0x%2x attribute 0x%2x: 0x%x", entry.clusterId, entry.attributeId, status);
238 if (emberAfIsLongStringAttributeType(dataType))
240 // LONG string types are rarely used and even more rarely (never?)
241 // reported; ignore and leave ensuing handling of other types unchanged.
242 emberAfReportingPrintln("ERR: reporting of LONG string attribute type not supported: cluster 0x%2x attribute 0x%2x",
243 entry.clusterId, entry.attributeId);
247 // find size of current report
248 dataSize = emberAfAttributeValueSize(dataType, readData);
249 reportSize = static_cast<uint32_t>(sizeof(entry.attributeId) + sizeof(dataType) + dataSize);
251 // If we have already started a report for a different attribute or
252 // destination, or if the current entry is too big for current report, send it and create a new one.
253 if (apsFrame != NULL &&
254 (!(entry.endpoint == apsFrame->sourceEndpoint && entry.clusterId == apsFrame->clusterId &&
255 emberAfClusterIsClient(&entry) == clientToServer && entry.manufacturerCode == manufacturerCode) ||
256 (appResponseLength + reportSize > smallestPayloadMaxLength)))
258 if (appResponseLength + reportSize > smallestPayloadMaxLength)
260 emberAfReportingPrintln("Reporting Entry Full - creating new report");
262 conditionallySendReport(apsFrame->sourceEndpoint, apsFrame->clusterId);
266 // If we haven't made the message header, make it.
267 if (apsFrame == NULL)
269 apsFrame = emberAfGetCommandApsFrame();
270 clientToServer = emberAfClusterIsClient(&entry);
271 // The manufacturer-specfic version of the fill API only creates a
272 // manufacturer-specfic command if the manufacturer code is set. For
273 // non-manufacturer-specfic reports, the manufacturer code is unset, so
274 // we can get away with using this API for both cases.
275 emberAfFillExternalManufacturerSpecificBuffer(
277 ? (ZCL_GLOBAL_COMMAND | ZCL_FRAME_CONTROL_CLIENT_TO_SERVER | EMBER_AF_DEFAULT_RESPONSE_POLICY_REQUESTS)
278 : (ZCL_GLOBAL_COMMAND | ZCL_FRAME_CONTROL_SERVER_TO_CLIENT | EMBER_AF_DEFAULT_RESPONSE_POLICY_REQUESTS)),
279 entry.clusterId, entry.manufacturerCode, ZCL_REPORT_ATTRIBUTES_COMMAND_ID, "");
280 apsFrame->sourceEndpoint = entry.endpoint;
281 apsFrame->options = EMBER_AF_DEFAULT_APS_OPTIONS;
282 manufacturerCode = entry.manufacturerCode;
284 // EMAPPFWKV2-1327: Reporting plugin does not account for reporting too many attributes
285 // in the same ZCL:ReportAttributes message
287 // find smallest maximum payload that the destination can receive for this cluster and source endpoint
288 smallestPayloadMaxLength = MAX_INT8U_VALUE;
289 for (index = 0; index < EMBER_BINDING_TABLE_SIZE; index++)
291 status = (EmberAfStatus) emberGetBinding(index, &bindingEntry);
292 if (status == (EmberAfStatus) EMBER_SUCCESS && bindingEntry.local == entry.endpoint &&
293 bindingEntry.clusterId == entry.clusterId)
295 currentPayloadMaxLength =
296 emberAfMaximumApsPayloadLength(bindingEntry.type, bindingEntry.networkIndex, apsFrame);
297 if (currentPayloadMaxLength < smallestPayloadMaxLength)
299 smallestPayloadMaxLength = currentPayloadMaxLength;
305 // Payload is [attribute id:2] [type:1] [data:N].
306 emberAfPutInt16uInResp(entry.attributeId);
307 emberAfPutInt8uInResp(dataType);
310 if (isThisDataTypeSentLittleEndianOTA(dataType))
313 for (i = 0; i < dataSize; i++)
315 emberAfPutInt8uInResp(readData[dataSize - i - 1]);
320 emberAfPutBlockInResp(readData, dataSize);
323 emberAfPutBlockInResp(readData, dataSize);
326 // Store the last reported time and value so that we can track intervals
327 // and changes. We only track changes for data types that are small enough
328 // for us to compare. For CHAR and OCTET strings, we substitute a 32-bit hash.
329 emAfPluginReportVolatileData[i].reportableChange = false;
330 emAfPluginReportVolatileData[i].lastReportTimeMs = static_cast<uint32_t>(chip::System::Layer::GetClock_MonotonicMS());
331 uint32_t stringHash = 0;
332 uint8_t * copyData = readData;
333 uint16_t copySize = dataSize;
334 if (dataType == ZCL_OCTET_STRING_ATTRIBUTE_TYPE || dataType == ZCL_CHAR_STRING_ATTRIBUTE_TYPE)
336 // dataSize was set above to count the string's length byte, in addition to string length.
337 // Compute hash on string value only. Note that string length fits
338 // in one byte, so dataSize can't be larger than 256 right now.
339 stringHash = computeStringHash(readData + 1, static_cast<uint8_t>(dataSize - 1));
340 copyData = (uint8_t *) &stringHash;
341 copySize = sizeof(stringHash);
343 if (copySize <= sizeof(emAfPluginReportVolatileData[i].lastReportValue))
345 emAfPluginReportVolatileData[i].lastReportValue = 0;
347 memmove(((uint8_t *) &emAfPluginReportVolatileData[i].lastReportValue +
348 sizeof(emAfPluginReportVolatileData[i].lastReportValue) - copySize),
351 memmove(&emAfPluginReportVolatileData[i].lastReportValue, copyData, copySize);
356 if (apsFrame != NULL)
358 conditionallySendReport(apsFrame->sourceEndpoint, apsFrame->clusterId);
363 static void conditionallySendReport(EndpointId endpoint, ClusterId clusterId)
366 if (emberAfIsDeviceEnabled(endpoint) || clusterId == ZCL_IDENTIFY_CLUSTER_ID)
368 status = emberAfSendCommandUnicastToBindingsWithCallback((EmberAfMessageSentFunction)(&retrySendReport));
370 // If the callback table is full, attempt to send the message with no
371 // callback. Note that this could lead to a message failing to transmit
372 // with no notification to the user for any number of reasons (ex: hitting
373 // the message queue limit), but is better than not sending the message at
374 // all because the system hits its callback queue limit.
375 if (status == EMBER_TABLE_FULL)
377 emberAfSendCommandUnicastToBindings();
380 #ifdef EMBER_AF_PLUGIN_REPORTING_ENABLE_GROUP_BOUND_REPORTS
381 emberAfSendCommandMulticastToBindings();
382 #endif // EMBER_AF_PLUGIN_REPORTING_ENABLE_GROUP_BOUND_REPORTS
386 bool emberAfConfigureReportingCommandCallback(const EmberAfClusterCommand * cmd)
388 EmberStatus sendStatus;
389 uint16_t bufIndex = cmd->payloadStartIndex;
390 uint8_t frameControl, mask;
391 bool failures = false;
393 emberAfReportingPrint("%p: ", "CFG_RPT");
394 emberAfReportingDebugExec(emberAfDecodeAndPrintClusterWithMfgCode(cmd->apsFrame->clusterId, cmd->mfgCode));
395 emberAfReportingPrintln("");
396 emberAfReportingFlush();
398 if (cmd->direction == ZCL_DIRECTION_CLIENT_TO_SERVER)
400 frameControl = (ZCL_GLOBAL_COMMAND | ZCL_FRAME_CONTROL_SERVER_TO_CLIENT | EMBER_AF_DEFAULT_RESPONSE_POLICY_REQUESTS);
401 mask = CLUSTER_MASK_SERVER;
405 frameControl = (ZCL_GLOBAL_COMMAND | ZCL_FRAME_CONTROL_CLIENT_TO_SERVER | EMBER_AF_DEFAULT_RESPONSE_POLICY_REQUESTS);
406 mask = CLUSTER_MASK_CLIENT;
409 // The manufacturer-specfic version of the fill API only creates a
410 // manufacturer-specfic command if the manufacturer code is set. For non-
411 // manufacturer-specfic reports, the manufacturer code is unset, so we can
412 // get away with using this API for both cases.
413 emberAfFillExternalManufacturerSpecificBuffer(frameControl, cmd->apsFrame->clusterId, cmd->mfgCode,
414 ZCL_CONFIGURE_REPORTING_RESPONSE_COMMAND_ID, "");
416 // Each record in the command has at least a one-byte direction and a two-
417 // byte attribute id. Additional fields are present depending on the value
418 // of the direction field.
419 while (bufIndex + 3 < cmd->bufLen)
421 AttributeId attributeId;
422 EmberAfReportingDirection direction;
423 EmberAfStatus status;
425 direction = (EmberAfReportingDirection) emberAfGetInt8u(cmd->buffer, bufIndex, cmd->bufLen);
427 attributeId = (AttributeId) emberAfGetInt16u(cmd->buffer, bufIndex, cmd->bufLen);
428 bufIndex = static_cast<uint16_t>(bufIndex + 2);
430 emberAfReportingPrintln(" - direction:%x, attr:%2x", direction, attributeId);
434 case EMBER_ZCL_REPORTING_DIRECTION_REPORTED: {
435 EmberAfAttributeMetadata * metadata;
436 EmberAfAttributeType dataType;
437 uint16_t minInterval, maxInterval;
438 uint32_t reportableChange = 0;
439 EmberAfPluginReportingEntry newEntry;
441 dataType = (EmberAfAttributeType) emberAfGetInt8u(cmd->buffer, bufIndex, cmd->bufLen);
443 minInterval = emberAfGetInt16u(cmd->buffer, bufIndex, cmd->bufLen);
444 bufIndex = static_cast<uint16_t>(bufIndex + 2);
445 maxInterval = emberAfGetInt16u(cmd->buffer, bufIndex, cmd->bufLen);
446 bufIndex = static_cast<uint16_t>(bufIndex + 2);
448 emberAfReportingPrintln(" type:%x, min:%2x, max:%2x", dataType, minInterval, maxInterval);
449 emberAfReportingFlush();
451 if (emberAfGetAttributeAnalogOrDiscreteType(dataType) == EMBER_AF_DATA_TYPE_ANALOG)
453 uint8_t dataSize = emberAfGetDataSize(dataType);
454 uint64_t currentChange = emberAfGetInt(cmd->buffer, bufIndex, cmd->bufLen, dataSize);
455 if (chip::CanCastTo<uint32_t>(currentChange))
457 reportableChange = static_cast<uint32_t>(emberAfGetInt(cmd->buffer, bufIndex, cmd->bufLen, dataSize));
461 status = EMBER_ZCL_STATUS_INVALID_DATA_TYPE;
465 emberAfReportingPrint(" change:");
466 emberAfReportingPrintBuffer(cmd->buffer + bufIndex, dataSize, false);
467 emberAfReportingPrintln("");
469 bufIndex = static_cast<uint16_t>(bufIndex + dataSize);
472 // emberAfPluginReportingConfigureReportedAttribute handles non-
473 // existent attributes, but does not verify the attribute data type, so
474 // we need to check it here.
475 metadata = emberAfLocateAttributeMetadata(cmd->apsFrame->destinationEndpoint, cmd->apsFrame->clusterId, attributeId,
477 if (metadata != NULL && metadata->attributeType != dataType)
479 status = EMBER_ZCL_STATUS_INVALID_DATA_TYPE;
483 // Add a reporting entry for a reported attribute. The reports will
484 // be sent from us to the source of the Configure Reporting command.
485 newEntry.endpoint = cmd->apsFrame->destinationEndpoint;
486 newEntry.clusterId = cmd->apsFrame->clusterId;
487 newEntry.attributeId = attributeId;
488 newEntry.mask = mask;
489 newEntry.manufacturerCode = cmd->mfgCode;
490 newEntry.data.reported.minInterval = minInterval;
491 newEntry.data.reported.maxInterval = maxInterval;
492 newEntry.data.reported.reportableChange = reportableChange;
493 status = emberAfPluginReportingConfigureReportedAttribute(&newEntry);
497 case EMBER_ZCL_REPORTING_DIRECTION_RECEIVED: {
498 uint16_t timeout = emberAfGetInt16u(cmd->buffer, bufIndex, cmd->bufLen);
499 bufIndex = static_cast<uint16_t>(bufIndex + 2);
501 emberAfReportingPrintln(" timeout:%2x", timeout);
503 // Add a reporting entry from a received attribute. The reports
504 // will be sent to us from the source of the Configure Reporting
506 status = configureReceivedAttribute(cmd, attributeId, mask, timeout);
510 // This will abort the processing (see below).
511 status = EMBER_ZCL_STATUS_INVALID_FIELD;
515 // If a report cannot be configured, the status, direction, and
516 // attribute are added to the response. If the failure was due to an
517 // invalid field, we have to abort after this record because we don't
518 // know how to interpret the rest of the data in the request.
519 if (status != EMBER_ZCL_STATUS_SUCCESS)
521 emberAfPutInt8uInResp(status);
522 emberAfPutInt8uInResp(direction);
523 emberAfPutInt16uInResp(attributeId);
525 if (status == EMBER_ZCL_STATUS_INVALID_FIELD)
532 // We just respond with SUCCESS if we made it through without failures.
535 emberAfPutInt8uInResp(EMBER_ZCL_STATUS_SUCCESS);
538 sendStatus = emberAfSendResponse();
539 if (EMBER_SUCCESS != sendStatus)
541 emberAfReportingPrintln("Reporting: failed to send %s response: 0x%x", "configure_reporting", sendStatus);
546 bool emberAfReadReportingConfigurationCommandCallback(const EmberAfClusterCommand * cmd)
548 EmberStatus sendStatus;
549 uint16_t bufIndex = cmd->payloadStartIndex;
550 uint8_t frameControl, mask;
552 emberAfReportingPrint("%p: ", "READ_RPT_CFG");
553 emberAfReportingDebugExec(emberAfDecodeAndPrintClusterWithMfgCode(cmd->apsFrame->clusterId, cmd->mfgCode));
554 emberAfReportingPrintln("");
555 emberAfReportingFlush();
557 if (cmd->direction == ZCL_DIRECTION_CLIENT_TO_SERVER)
559 frameControl = (ZCL_GLOBAL_COMMAND | ZCL_FRAME_CONTROL_SERVER_TO_CLIENT | EMBER_AF_DEFAULT_RESPONSE_POLICY_REQUESTS);
560 mask = CLUSTER_MASK_SERVER;
564 frameControl = (ZCL_GLOBAL_COMMAND | ZCL_FRAME_CONTROL_CLIENT_TO_SERVER | EMBER_AF_DEFAULT_RESPONSE_POLICY_REQUESTS);
565 mask = CLUSTER_MASK_CLIENT;
568 // The manufacturer-specfic version of the fill API only creates a
569 // manufacturer-specfic command if the manufacturer code is set. For non-
570 // manufacturer-specfic reports, the manufacturer code is unset, so we can
571 // get away with using this API for both cases.
572 emberAfFillExternalManufacturerSpecificBuffer(frameControl, cmd->apsFrame->clusterId, cmd->mfgCode,
573 ZCL_READ_REPORTING_CONFIGURATION_RESPONSE_COMMAND_ID, "");
575 // Each record in the command has a one-byte direction and a two-byte
577 while (bufIndex + 3 <= cmd->bufLen)
579 AttributeId attributeId;
580 EmberAfAttributeMetadata * metadata = NULL;
581 EmberAfPluginReportingEntry entry;
582 EmberAfReportingDirection direction;
586 direction = (EmberAfReportingDirection) emberAfGetInt8u(cmd->buffer, bufIndex, cmd->bufLen);
588 attributeId = (AttributeId) emberAfGetInt16u(cmd->buffer, bufIndex, cmd->bufLen);
589 bufIndex = static_cast<uint16_t>(bufIndex + 2);
593 case EMBER_ZCL_REPORTING_DIRECTION_REPORTED:
594 case EMBER_ZCL_REPORTING_DIRECTION_RECEIVED:
595 metadata = emberAfLocateAttributeMetadata(cmd->apsFrame->destinationEndpoint, cmd->apsFrame->clusterId, attributeId,
597 if (metadata == NULL)
599 emberAfPutInt8uInResp(EMBER_ZCL_STATUS_UNSUPPORTED_ATTRIBUTE);
600 emberAfPutInt8uInResp(direction);
601 emberAfPutInt16uInResp(attributeId);
606 emberAfPutInt8uInResp(EMBER_ZCL_STATUS_INVALID_FIELD);
607 emberAfPutInt8uInResp(direction);
608 emberAfPutInt16uInResp(attributeId);
612 // 075123r03 seems to suggest that SUCCESS is returned even if reporting
613 // isn't configured for the requested attribute. The individual fields
614 // of the response for this attribute get populated with defaults.
615 // CCB 1854 removes the ambiguity and requires NOT_FOUND to be returned in
616 // the status field and all fields except direction and attribute identifier
617 // to be omitted if there is no report configuration found.
618 for (i = 0; i < REPORT_TABLE_SIZE; i++)
620 emAfPluginReportingGetEntry(i, &entry);
621 if (entry.endpoint == EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID)
625 if (entry.direction == direction && entry.endpoint == cmd->apsFrame->destinationEndpoint &&
626 entry.clusterId == cmd->apsFrame->clusterId && entry.attributeId == attributeId && entry.mask == mask &&
627 entry.manufacturerCode == cmd->mfgCode &&
628 (entry.direction == EMBER_ZCL_REPORTING_DIRECTION_REPORTED ||
629 (entry.data.received.source == cmd->source && entry.data.received.endpoint == cmd->apsFrame->sourceEndpoint)))
635 // Attribute supported, reportable, no report configuration was found.
638 emberAfPutInt8uInResp(EMBER_ZCL_STATUS_NOT_FOUND);
639 emberAfPutInt8uInResp(direction);
640 emberAfPutInt16uInResp(attributeId);
643 // Attribute supported, reportable, report configuration was found.
644 emberAfPutInt8uInResp(EMBER_ZCL_STATUS_SUCCESS);
645 emberAfPutInt8uInResp(direction);
646 emberAfPutInt16uInResp(attributeId);
649 case EMBER_ZCL_REPORTING_DIRECTION_REPORTED:
650 if (metadata != NULL)
652 emberAfPutInt8uInResp(metadata->attributeType);
653 emberAfPutInt16uInResp(entry.data.reported.minInterval);
654 emberAfPutInt16uInResp(entry.data.reported.maxInterval);
655 if (emberAfGetAttributeAnalogOrDiscreteType(metadata->attributeType) == EMBER_AF_DATA_TYPE_ANALOG)
657 putReportableChangeInResp(&entry, metadata->attributeType);
661 case EMBER_ZCL_REPORTING_DIRECTION_RECEIVED:
662 emberAfPutInt16uInResp(entry.data.received.timeout);
667 sendStatus = emberAfSendResponse();
668 if (EMBER_SUCCESS != sendStatus)
670 emberAfReportingPrintln("Reporting: failed to send %s response: 0x%x", "read_reporting_configuration", sendStatus);
675 EmberStatus emberAfClearReportTableCallback(void)
678 for (i = 0; i < REPORT_TABLE_SIZE; i++)
680 removeConfiguration(i);
682 emberEventControlSetInactive(&emberAfPluginReportingTickEventControl);
683 return EMBER_SUCCESS;
686 EmberStatus emAfPluginReportingRemoveEntry(uint8_t index)
688 EmberStatus status = EMBER_INDEX_OUT_OF_RANGE;
689 if (index < REPORT_TABLE_SIZE)
691 removeConfigurationAndScheduleTick(index);
692 status = EMBER_SUCCESS;
697 void emberAfReportingAttributeChangeCallback(EndpointId endpoint, ClusterId clusterId, AttributeId attributeId, uint8_t mask,
698 uint16_t manufacturerCode, EmberAfAttributeType type, uint8_t * data)
701 for (i = 0; i < REPORT_TABLE_SIZE; i++)
703 EmberAfPluginReportingEntry entry;
704 emAfPluginReportingGetEntry(i, &entry);
705 if (entry.endpoint == EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID)
709 if (entry.direction == EMBER_ZCL_REPORTING_DIRECTION_REPORTED && entry.endpoint == endpoint &&
710 entry.clusterId == clusterId && entry.attributeId == attributeId && entry.mask == mask &&
711 entry.manufacturerCode == manufacturerCode)
713 // For CHAR and OCTET strings, the string value may be too long to fit into the
714 // lastReportValue field (EmberAfDifferenceType), so instead we save the string's
715 // hash, and detect changes in string value based on unequal hash.
716 uint32_t stringHash = 0;
717 uint8_t dataSize = emberAfGetDataSize(type);
718 uint8_t * dataRef = data;
719 if (type == ZCL_OCTET_STRING_ATTRIBUTE_TYPE || type == ZCL_CHAR_STRING_ATTRIBUTE_TYPE)
721 stringHash = computeStringHash(data + 1, emberAfStringLength(data));
722 dataRef = (uint8_t *) &stringHash;
723 dataSize = sizeof(stringHash);
725 // If we are reporting this particular attribute, we only care whether
726 // the new value meets the reportable change criteria. If it does, we
727 // mark the entry as ready to report and reschedule the tick. Whether
728 // the tick will be scheduled for immediate or delayed execution depends
729 // on the minimum reporting interval. This is handled in the scheduler.
730 EmberAfDifferenceType difference =
731 emberAfGetDifference(dataRef, emAfPluginReportVolatileData[i].lastReportValue, dataSize);
732 uint8_t analogOrDiscrete = emberAfGetAttributeAnalogOrDiscreteType(type);
733 if ((analogOrDiscrete == EMBER_AF_DATA_TYPE_DISCRETE && difference != 0) ||
734 (analogOrDiscrete == EMBER_AF_DATA_TYPE_ANALOG && entry.data.reported.reportableChange <= difference))
736 emAfPluginReportVolatileData[i].reportableChange = true;
744 bool emAfPluginReportingDoEntriesMatch(const EmberAfPluginReportingEntry * const entry1,
745 const EmberAfPluginReportingEntry * const entry2)
747 // Verify that the reporting parameters of both entries match.
748 // If the entries are for EMBER_ZCL_REPORTING_DIRECTION_REPORTED, the
749 // reporting configurations do not need to match. If the direction is
750 // EMBER_ZCL_REPORTING_DIRECTION_RECEIVED, then the source and destination
751 // endpoints need to match.
752 if ((entry1->endpoint == entry2->endpoint) && (entry1->clusterId == entry2->clusterId) &&
753 (entry1->attributeId == entry2->attributeId) && (entry1->mask == entry2->mask) &&
754 (entry1->manufacturerCode == entry2->manufacturerCode) && (entry1->direction == entry2->direction) &&
755 ((entry1->direction == EMBER_ZCL_REPORTING_DIRECTION_REPORTED) ||
756 ((entry1->data.received.source == entry2->data.received.source) &&
757 (entry1->data.received.endpoint == entry2->data.received.endpoint))))
764 uint8_t emAfPluginReportingAddEntry(EmberAfPluginReportingEntry * newEntry)
767 EmberAfPluginReportingEntry oldEntry;
769 // If an entry already exists, or exists but with different parameters,
770 // overwrite it with the new entry to prevent pollution of the report table
771 for (i = 0; i < REPORT_TABLE_SIZE; i++)
773 emAfPluginReportingGetEntry(i, &oldEntry);
774 if (emAfPluginReportingDoEntriesMatch(&oldEntry, newEntry))
776 emAfPluginReportingSetEntry(i, newEntry);
781 // If no pre-existing entries were found, copy the new entry into the lowest
782 // indexed free spot in the reporting table
783 for (i = 0; i < REPORT_TABLE_SIZE; i++)
785 emAfPluginReportingGetEntry(i, &oldEntry);
786 if (oldEntry.endpoint == EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID)
788 emAfPluginReportingSetEntry(i, newEntry);
793 // If no free spots were found, return the failure indicator
797 static void scheduleTick(void)
799 uint32_t delayMs = MAX_INT32U_VALUE;
801 for (i = 0; i < REPORT_TABLE_SIZE; i++)
803 EmberAfPluginReportingEntry entry;
804 emAfPluginReportingGetEntry(i, &entry);
806 if (entry.endpoint != EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID &&
807 entry.direction == EMBER_ZCL_REPORTING_DIRECTION_REPORTED)
809 uint32_t minIntervalMs = (entry.data.reported.minInterval * MILLISECOND_TICKS_PER_SECOND);
810 uint32_t maxIntervalMs = (entry.data.reported.maxInterval * MILLISECOND_TICKS_PER_SECOND);
812 elapsedTimeInt32u(emAfPluginReportVolatileData[i].lastReportTimeMs, chip::System::Layer::GetClock_MonotonicMS());
813 uint32_t remainingMs = MAX_INT32U_VALUE;
814 if (emAfPluginReportVolatileData[i].reportableChange)
816 remainingMs = (minIntervalMs < elapsedMs ? 0 : minIntervalMs - elapsedMs);
818 else if (maxIntervalMs)
820 remainingMs = (maxIntervalMs < elapsedMs ? 0 : maxIntervalMs - elapsedMs);
822 if (remainingMs < delayMs)
824 delayMs = remainingMs;
828 if (delayMs != MAX_INT32U_VALUE)
830 emberAfDebugPrintln("sched report event in %d ms", delayMs);
831 emberEventControlSetDelayMS(&emberAfPluginReportingTickEventControl, delayMs);
835 emberAfDebugPrintln("deactivate report event");
836 emberEventControlSetInactive(&emberAfPluginReportingTickEventControl);
840 static void removeConfiguration(uint8_t index)
842 EmberAfPluginReportingEntry entry;
843 emAfPluginReportingGetEntry(index, &entry);
844 entry.endpoint = EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID;
845 emAfPluginReportingSetEntry(index, &entry);
846 emberAfPluginReportingConfiguredCallback(&entry);
849 static void removeConfigurationAndScheduleTick(uint8_t index)
851 removeConfiguration(index);
855 EmberAfStatus emberAfPluginReportingConfigureReportedAttribute(const EmberAfPluginReportingEntry * newEntry)
857 EmberAfAttributeMetadata * metadata;
858 EmberAfPluginReportingEntry entry;
859 EmberAfStatus status;
860 uint8_t i, index = NULL_INDEX;
861 bool initialize = true;
863 // Verify that we support the attribute and that the data type matches.
864 metadata = emberAfLocateAttributeMetadata(newEntry->endpoint, newEntry->clusterId, newEntry->attributeId, newEntry->mask,
865 newEntry->manufacturerCode);
866 if (metadata == NULL)
868 return EMBER_ZCL_STATUS_UNSUPPORTED_ATTRIBUTE;
871 // Verify the minimum and maximum intervals make sense.
872 if (newEntry->data.reported.maxInterval != 0 && (newEntry->data.reported.maxInterval < newEntry->data.reported.minInterval))
874 return EMBER_ZCL_STATUS_INVALID_VALUE;
877 // Check the table for an entry that matches this request and also watch for
878 // empty slots along the way. If a report exists, it will be overwritten
879 // with the new configuration. Otherwise, a new entry will be created and
881 for (i = 0; i < REPORT_TABLE_SIZE; i++)
883 emAfPluginReportingGetEntry(i, &entry);
884 if (entry.direction == EMBER_ZCL_REPORTING_DIRECTION_REPORTED && entry.endpoint == newEntry->endpoint &&
885 entry.clusterId == newEntry->clusterId && entry.attributeId == newEntry->attributeId && entry.mask == newEntry->mask &&
886 entry.manufacturerCode == newEntry->manufacturerCode)
892 else if (entry.endpoint == EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID && index == NULL_INDEX)
898 // If the maximum reporting interval is 0xFFFF, the device shall not issue
899 // reports for the attribute and the configuration information for that
900 // attribute need not be maintained.
901 if (newEntry->data.reported.maxInterval == 0xFFFF)
905 removeConfigurationAndScheduleTick(index);
907 return EMBER_ZCL_STATUS_SUCCESS;
909 // ZCL v6 Section 2.5.7.1.6 Maximum Reporting Interval Field
910 // If this value is set to 0x0000 and the minimum reporting interval field
911 // equals 0xffff, then the device SHALL revert back to its default reporting
912 // configuration. The reportable change field, if present, SHALL be set to
914 // Verify special condition to reset the reporting configuration to defaults
915 // if the minimum == 0xFFFF and maximum == 0x0000
917 if ((newEntry->data.reported.maxInterval == 0x0000) && (newEntry->data.reported.minInterval == 0xFFFF))
919 // Get the configuration from the default configuration table for this
920 memmove(&entry, newEntry, sizeof(EmberAfPluginReportingEntry));
921 if (emberAfPluginReportingGetReportingConfigDefaults(&entry))
923 // Then it must be initialise with the default config - explicity
929 if (index == NULL_INDEX)
931 return EMBER_ZCL_STATUS_INSUFFICIENT_SPACE;
935 entry.direction = EMBER_ZCL_REPORTING_DIRECTION_REPORTED;
936 entry.endpoint = newEntry->endpoint;
937 entry.clusterId = newEntry->clusterId;
938 entry.attributeId = newEntry->attributeId;
939 entry.mask = newEntry->mask;
940 entry.manufacturerCode = newEntry->manufacturerCode;
941 if (index < REPORT_TABLE_SIZE)
943 emAfPluginReportVolatileData[index].lastReportTimeMs =
944 static_cast<uint32_t>(chip::System::Layer::GetClock_MonotonicMS());
945 emAfPluginReportVolatileData[index].lastReportValue = 0;
949 // For new or updated entries, set the intervals and reportable change.
950 // Updated entries will retain all other settings configured previously.
953 entry.data.reported.minInterval = newEntry->data.reported.minInterval;
954 entry.data.reported.maxInterval = newEntry->data.reported.maxInterval;
955 entry.data.reported.reportableChange = newEntry->data.reported.reportableChange;
957 // Give the application a chance to review the configuration that we have
958 // been building up. If the application rejects it, we just do not save the
959 // record. If we were supposed to add a new configuration, it will not be
960 // created. If we were supposed to update an existing configuration, we will
961 // keep the old one and just discard any changes. So, in either case, life
962 // continues unchanged if the application rejects the configuration.
963 status = emberAfPluginReportingConfiguredCallback(&entry);
964 if (status == EMBER_ZCL_STATUS_SUCCESS)
966 emAfPluginReportingSetEntry(index, &entry);
972 static EmberAfStatus configureReceivedAttribute(const EmberAfClusterCommand * cmd, AttributeId attributeId, uint8_t mask,
975 EmberAfPluginReportingEntry entry;
976 EmberAfStatus status;
977 uint8_t i, index = NULL_INDEX;
978 bool initialize = true;
980 // Check the table for an entry that matches this request and also watch for
981 // empty slots along the way. If a report exists, it will be overwritten
982 // with the new configuration. Otherwise, a new entry will be created and
984 for (i = 0; i < REPORT_TABLE_SIZE; i++)
986 emAfPluginReportingGetEntry(i, &entry);
987 if (entry.endpoint == EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID)
991 if (entry.direction == EMBER_ZCL_REPORTING_DIRECTION_RECEIVED && entry.endpoint == cmd->apsFrame->destinationEndpoint &&
992 entry.clusterId == cmd->apsFrame->clusterId && entry.attributeId == attributeId && entry.mask == mask &&
993 entry.manufacturerCode == cmd->mfgCode && entry.data.received.source == cmd->source &&
994 entry.data.received.endpoint == cmd->apsFrame->sourceEndpoint)
1000 else if (entry.endpoint == EMBER_AF_PLUGIN_REPORTING_UNUSED_ENDPOINT_ID && index == NULL_INDEX)
1006 if (index == NULL_INDEX)
1008 return EMBER_ZCL_STATUS_INSUFFICIENT_SPACE;
1010 else if (initialize)
1012 entry.direction = EMBER_ZCL_REPORTING_DIRECTION_RECEIVED;
1013 entry.endpoint = cmd->apsFrame->destinationEndpoint;
1014 entry.clusterId = cmd->apsFrame->clusterId;
1015 entry.attributeId = attributeId;
1017 entry.manufacturerCode = cmd->mfgCode;
1018 entry.data.received.source = cmd->source;
1019 entry.data.received.endpoint = cmd->apsFrame->sourceEndpoint;
1022 // For new or updated entries, set the timeout. Updated entries will retain
1023 // all other settings configured previously.
1024 entry.data.received.timeout = timeout;
1026 // Give the application a chance to review the configuration that we have
1027 // been building up. If the application rejects it, we just do not save the
1028 // record. If we were supposed to add a new configuration, it will not be
1029 // created. If we were supposed to update an existing configuration, we will
1030 // keep the old one and just discard any changes. So, in either case, life
1031 // continues unchanged if the application rejects the configuration. If the
1032 // application accepts the change, the tick does not have to be rescheduled
1033 // here because we don't do anything with received reports.
1034 status = emberAfPluginReportingConfiguredCallback(&entry);
1035 if (status == EMBER_ZCL_STATUS_SUCCESS)
1037 emAfPluginReportingSetEntry(index, &entry);
1042 static void putReportableChangeInResp(const EmberAfPluginReportingEntry * entry, EmberAfAttributeType dataType)
1044 uint8_t bytes = emberAfGetDataSize(dataType);
1046 { // default, 0xFF...UL or 0x80...L
1047 for (; bytes > 0; bytes--)
1050 if (emberAfIsTypeSigned(dataType))
1052 b = (bytes == 1 ? 0x80 : 0x00);
1054 emberAfPutInt8uInResp(b);
1058 { // reportable change value
1059 uint32_t value = entry->data.reported.reportableChange;
1060 for (; bytes > 0; bytes--)
1062 uint8_t b = EMBER_BYTE_0(value);
1063 emberAfPutInt8uInResp(b);
1069 // Conditionally add reporting entry.
1070 // This is required to support setting up default reporting entries for
1071 // reportable attributes.
1072 static bool reportEntryDoesNotExist(const EmberAfPluginReportingEntry * newEntry)
1075 EmberAfPluginReportingEntry entry;
1077 for (i = 0; i < REPORT_TABLE_SIZE; i++)
1079 emAfPluginReportingGetEntry(i, &entry);
1080 if (emAfPluginReportingDoEntriesMatch(&entry, newEntry))
1089 uint8_t emAfPluginReportingConditionallyAddReportingEntry(EmberAfPluginReportingEntry * newEntry)
1091 if (reportEntryDoesNotExist(newEntry))
1093 return emAfPluginReportingAddEntry(newEntry);