blob: e64d56cd2b1c72c18752e4b9c450b5b67a7b5f38 [file] [log] [blame]
/*
*
* 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 <functional>
#include <stdbool.h>
#include <type_traits>
#include <app/data-model/Nullable.h>
#include <lib/support/BitFlags.h>
#include <system/SystemClock.h>
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<QuieterReportingPolicyEnum>;
namespace detail {
using Timestamp = System::Clock::Milliseconds64;
template <typename T>
using Nullable = DataModel::Nullable<T>;
/**
* 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<uint8_t> 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 <typename T, std::enable_if_t<std::is_arithmetic<T>::value, bool> = true>
class QuieterReportingAttribute
{
public:
explicit QuieterReportingAttribute(const Nullable<T> & 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<T> & lastDirtyValue;
// New value passed in `SetValue()`, to compare against lastDirtyValue for sufficient change if needed.
const Nullable<T> & newValue;
};
using SufficientChangePredicate = std::function<bool(const SufficientChangePredicateCandidate &)>;
/**
* @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<T> 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 <timestamp, value> and new <timestamp value> 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<T> & 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<T> & newValue, Timestamp now)
{
return SetValue(newValue, now, [](const SufficientChangePredicateCandidate &) -> bool { return false; });
}
protected:
// Current value of the attribute.
DataModel::Nullable<T> mValue;
// Last value that was marked as dirty (to use in comparisons for change, e.g. by SufficientChangePredicate).
DataModel::Nullable<T> 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