/* * * Copyright (c) 2024 Project CHIP Authors * All rights reserved. * * 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. */ #pragma once #include #include #include #include #include #include namespace chip { namespace app { enum class QuieterReportingPolicyEnum { kMarkDirtyOnChangeToFromZero = (1u << 0), kMarkDirtyOnDecrement = (1u << 1), kMarkDirtyOnIncrement = (1u << 2), }; enum class AttributeDirtyState { kNoReportNeeded = 0, kMustReport = 1, }; using QuieterReportingPolicyFlags = BitFlags; namespace detail { using Timestamp = System::Clock::Milliseconds64; template using Nullable = DataModel::Nullable; /** * This class helps track reporting state of an attribute to properly keep track of whether * it needs to be marked as dirty or not for purposes of reporting using * "7.7.9 Quieter Reporting Quality" (Q quality) * * The class can be configured via `policy()` to have some/all of the common reasons * for reporting (e.g. increment only, decrement only, change to/from zero). * * Changes of null to non-null or non-null to null are always considered dirty. * * It is possible to force mark the attribute as dirty (see `ForceDirty()`) such as * for conditions like "When there is any increase or decrease in the estimated time * remaining that was due to progressing insight of the server's control logic". * * Class maintains a `current value` and a timestamped `dirty` state. The `SetValue()` * method must be used to update the `current value` and will return AttributeDirtyState::kMustReport * if the attribute should be marked dirty/ * * - `SetValue()` has internal rules for null/non-null changes and policy-based rules * - `SetValue()` with a `SufficientChangePredicate` uses the internal rules in addition to * the predicate to determine dirty state * * See [QuieterReportingPolicyEnum] for policy flags on when a value is considered dirty * beyond non/non-null changes. * * Common quieter reporting usecases that can be supported by this class are: * - If attribute has changed due to a change in the X or Y attributes * - Use SufficientChangePredicate version * - When it changes from 0 to any other value and vice versa * - Use `kMarkDirtyOnChangeToFromZero` internal policy. * - When it changes from null to any other value and vice versa * - Built-in rule. * - When it increases * - Use `kMarkDirtyOnIncrement` internal policy. * - When it decreases * - Use `kMarkDirtyOnDecrement` internal policy. * - When there is any increase or decrease in the estimated time remaining that was * due to progressing insight of the server's control logic * - Use SufficientChangePredicate version with an always-true predicate. * - When it changes at a rate significantly different from one unit per second. * - Use SufficientChangePredicate version. * Example usage in-situ: * * Class has: * QuieterReportingAttribute mAttrib; * * Code at time of setting new value has: * * uint8_t newValue = driver.GetNewValue(); * auto now = SystemClock().GetMonotonicTimestamp(); * if (mAttrib.SetValue(newValue, now) == AttributeDirtyState::kMustReport) * { * MatterReportingAttributeChangeCallback(path_for_attribute); * } * * @tparam T - the type of underlying numerical value that will be held by the class. */ template ::value, bool> = true> class QuieterReportingAttribute { public: explicit QuieterReportingAttribute(const Nullable & initialValue) : mValue(initialValue), mLastDirtyValue(initialValue) {} // constructor that works with arrays of QuieterReportingAttribute explicit QuieterReportingAttribute() : mValue(DataModel::NullNullable), mLastDirtyValue(DataModel::NullNullable) {} struct SufficientChangePredicateCandidate { // Timestamp of last time attribute was marked dirty. Timestamp lastDirtyTimestamp; // New (`now`) timestamp passed in `SetValue()`. Timestamp nowTimestamp; // Value last marked as dirty. const Nullable & lastDirtyValue; // New value passed in `SetValue()`, to compare against lastDirtyValue for sufficient change if needed. const Nullable & newValue; }; using SufficientChangePredicate = std::function; /** * @brief Factory to generate a functor for "attribute was last reported" at least `minimumDurationMillis` ago. * * @param minimumDurationMillis - number of millis needed since last marked as dirty before we mark dirty again. * @return a functor usable for the `changedPredicate` arg of `SetValue()` */ static SufficientChangePredicate GetPredicateForSufficientTimeSinceLastDirty(System::Clock::Milliseconds64 minimumDurationMillis) { return [minimumDurationMillis](const SufficientChangePredicateCandidate & candidate) -> bool { return (candidate.lastDirtyValue != candidate.newValue) && ((candidate.nowTimestamp - candidate.lastDirtyTimestamp) >= minimumDurationMillis); }; } Nullable value() const { return mValue; } QuieterReportingPolicyFlags & policy() { return mPolicyFlags; } const QuieterReportingPolicyFlags & policy() const { return mPolicyFlags; } /** * Set the updated value of the attribute, computing whether it needs to be reported according to `changedPredicate` and * policies. * * - Any change of nullability between `newValue` and the old value will be considered dirty. * - The policies from `QuieterReportingPolicyEnum` and set via `SetPolicy()` are self-explanatory by name. * - The changedPredicate will be called with last dirty and new and may override * the dirty state altogether when it returns true. Use sparingly and default to a functor returning false. * The changedPredicate is only called on change. * * Internal recording will be done about last dirty value and last dirty timestamp based on the policies having applied. * * @param newValue - new value to set for the attribute * @param now - system monotonic timestamp at the time of the call * @param changedPredicate - functor to possibly override dirty state * @return AttributeDirtyState::kMustReport if attribute must be marked dirty right away, or * AttributeDirtyState::kNoReportNeeded otherwise. */ AttributeDirtyState SetValue(const DataModel::Nullable & newValue, Timestamp now, SufficientChangePredicate changedPredicate) { bool isChangeOfNull = newValue.IsNull() != mValue.IsNull(); bool areBothValuesNonNull = !newValue.IsNull() && !mValue.IsNull(); bool areBothValuesDifferent = areBothValuesNonNull && (newValue.Value() != mValue.Value()); bool changeToFromZero = areBothValuesNonNull && areBothValuesDifferent && (newValue.Value() == 0 || mValue.Value() == 0); bool isIncrement = areBothValuesNonNull && (newValue.Value() > mValue.Value()); bool isDecrement = areBothValuesNonNull && (newValue.Value() < mValue.Value()); bool isNewlyDirty = isChangeOfNull; isNewlyDirty = isNewlyDirty || (mPolicyFlags.Has(QuieterReportingPolicyEnum::kMarkDirtyOnChangeToFromZero) && changeToFromZero); isNewlyDirty = isNewlyDirty || (mPolicyFlags.Has(QuieterReportingPolicyEnum::kMarkDirtyOnDecrement) && isDecrement); isNewlyDirty = isNewlyDirty || (mPolicyFlags.Has(QuieterReportingPolicyEnum::kMarkDirtyOnIncrement) && isIncrement); // Only execute predicate on value change from last marked dirty. if (newValue != mLastDirtyValue) { SufficientChangePredicateCandidate candidate{ mLastDirtyTimestampMillis, // lastDirtyTimestamp now, // nowTimestamp mLastDirtyValue, // lastDirtyValue newValue // newValue }; isNewlyDirty = isNewlyDirty || changedPredicate(candidate); } mValue = newValue; if (isNewlyDirty) { mLastDirtyValue = newValue; mLastDirtyTimestampMillis = now; } return isNewlyDirty ? AttributeDirtyState::kMustReport : AttributeDirtyState::kNoReportNeeded; } /** * Same as the other `SetValue`, but assumes a changedPredicate that never overrides to dirty. * * This is the easy/common case. * * @param newValue - new value to set for the attribute * @param now - system monotonic timestamp at the time of the call * @return AttributeDirtyState::kMustReport if attribute must be marked dirty right away, or * AttributeDirtyState::kNoReportNeeded otherwise. */ AttributeDirtyState SetValue(const DataModel::Nullable & newValue, Timestamp now) { return SetValue(newValue, now, [](const SufficientChangePredicateCandidate &) -> bool { return false; }); } protected: // Current value of the attribute. DataModel::Nullable mValue; // Last value that was marked as dirty (to use in comparisons for change, e.g. by SufficientChangePredicate). DataModel::Nullable mLastDirtyValue; // Enabled internal change detection policies. QuieterReportingPolicyFlags mPolicyFlags{ 0 }; // Timestamp associated with the last time the attribute was marked dirty (to use in comparisons for change). chip::System::Clock::Timestamp mLastDirtyTimestampMillis{}; }; } // namespace detail using detail::QuieterReportingAttribute; } // namespace app } // namespace chip