blob: c176d6f6b17bbb57e84f86d6064dcae42ad06957 [file] [log] [blame]
/**
*
* Copyright (c) 2020 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.
*/
// clusters specific header
#include "level-control.h"
#include <algorithm>
// this file contains all the common includes for clusters in the util
#include <app-common/zap-generated/attributes/Accessors.h>
#include <app-common/zap-generated/cluster-objects.h>
#include <app/CommandHandler.h>
#include <app/ConcreteCommandPath.h>
#include <app/cluster-building-blocks/QuieterReporting.h>
#include <app/util/attribute-storage.h>
#include <app/util/config.h>
#include <app/util/util.h>
#include <app/reporting/reporting.h>
#include <lib/core/Optional.h>
#include <platform/CHIPDeviceConfig.h>
#include <platform/CHIPDeviceLayer.h>
#include <platform/PlatformManager.h>
#include <tracing/macros.h>
#ifdef MATTER_DM_PLUGIN_SCENES_MANAGEMENT
#include <app/clusters/scenes-server/scenes-server.h>
#endif // MATTER_DM_PLUGIN_SCENES_MANAGEMENT
#ifdef MATTER_DM_PLUGIN_ON_OFF
#include <app/clusters/on-off-server/on-off-server.h>
#endif // MATTER_DM_PLUGIN_ON_OFF
#ifdef MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP
#include <app/clusters/color-control-server/color-control-server.h>
#endif // MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP
#include <assert.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::LevelControl;
using chip::Protocols::InteractionModel::Status;
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL
static bool areStartUpLevelControlServerAttributesNonVolatile(EndpointId endpoint);
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL
#if (MATTER_DM_PLUGIN_LEVEL_CONTROL_RATE == 0)
#define FASTEST_TRANSITION_TIME_MS 0
#else
#define FASTEST_TRANSITION_TIME_MS (MILLISECOND_TICKS_PER_SECOND / MATTER_DM_PLUGIN_LEVEL_CONTROL_RATE)
#endif // MATTER_DM_PLUGIN_LEVEL_CONTROL_RATE
#define LEVEL_CONTROL_LIGHTING_MIN_LEVEL 0x01
#define LEVEL_CONTROL_LIGHTING_MAX_LEVEL 0xFE
#define INVALID_STORED_LEVEL 0xFFFF
#define STARTUP_CURRENT_LEVEL_USE_DEVICE_MINIMUM 0x00
#define STARTUP_CURRENT_LEVEL_USE_PREVIOUS_LEVEL 0xFF
static constexpr size_t kLevelControlStateTableSize =
MATTER_DM_LEVEL_CONTROL_CLUSTER_SERVER_ENDPOINT_COUNT + CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT;
static_assert(kLevelControlStateTableSize <= kEmberInvalidEndpointIndex, "LevelControl state table size error");
struct CallbackScheduleState
{
System::Clock::Timestamp idealTimestamp; // The ideal time-stamp for the next callback to be scheduled.
System::Clock::Milliseconds32 runTime; // The duration of the previous scheduled callback function.
// e.g. running time of emberAfLevelControlClusterServerTickCallback
// when called consecutively
};
struct EmberAfLevelControlState
{
CommandId commandId;
uint8_t moveToLevel;
bool increasing;
uint8_t onLevel;
uint8_t minLevel;
uint8_t maxLevel;
uint16_t storedLevel;
uint32_t eventDurationMs;
uint32_t transitionTimeMs;
uint32_t elapsedTimeMs;
CallbackScheduleState callbackSchedule;
QuieterReportingAttribute<uint8_t> quietCurrentLevel{ DataModel::NullNullable };
QuieterReportingAttribute<uint16_t> quietRemainingTime{ DataModel::MakeNullable<uint16_t>(0) };
};
static EmberAfLevelControlState stateTable[kLevelControlStateTableSize];
static EmberAfLevelControlState * getState(EndpointId endpoint);
static Status moveToLevelHandler(EndpointId endpoint, CommandId commandId, uint8_t level,
DataModel::Nullable<uint16_t> transitionTimeDs, chip::Optional<BitMask<OptionsBitmap>> optionsMask,
chip::Optional<BitMask<OptionsBitmap>> optionsOverride, uint16_t storedLevel);
static void moveHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, MoveModeEnum moveMode,
DataModel::Nullable<uint8_t> rate, chip::Optional<BitMask<OptionsBitmap>> optionsMask,
chip::Optional<BitMask<OptionsBitmap>> optionsOverride);
static void stepHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, StepModeEnum stepMode,
uint8_t stepSize, DataModel::Nullable<uint16_t> transitionTimeDs,
chip::Optional<BitMask<OptionsBitmap>> optionsMask, chip::Optional<BitMask<OptionsBitmap>> optionsOverride);
static void stopHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
chip::Optional<BitMask<OptionsBitmap>> optionsMask, chip::Optional<BitMask<OptionsBitmap>> optionsOverride);
static void setOnOffValue(EndpointId endpoint, bool onOff);
static void writeRemainingTime(EndpointId endpoint, uint16_t remainingTimeMs, bool isNewTransition = false);
static bool shouldExecuteIfOff(EndpointId endpoint, CommandId commandId, chip::Optional<chip::BitMask<OptionsBitmap>> optionsMask,
chip::Optional<chip::BitMask<OptionsBitmap>> optionsOverride);
static Status SetCurrentLevelQuietReport(EndpointId endpoint, EmberAfLevelControlState * state,
DataModel::Nullable<uint8_t> newValue, bool isEndOfTransition);
#if defined(MATTER_DM_PLUGIN_SCENES_MANAGEMENT) && CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS
class DefaultLevelControlSceneHandler : public scenes::DefaultSceneHandlerImpl
{
public:
// As per spec, 2 attributes are scenable in the level control cluster
static constexpr uint8_t kLevelMaxScenableAttributes = 2;
DefaultLevelControlSceneHandler() = default;
~DefaultLevelControlSceneHandler() override {}
// Default function for LevelControl cluster, only puts the LevelControl cluster ID in the span if supported on the caller
// endpoint
virtual void GetSupportedClusters(EndpointId endpoint, Span<ClusterId> & clusterBuffer) override
{
if (emberAfContainsServer(endpoint, LevelControl::Id) && clusterBuffer.size() >= 1)
{
clusterBuffer[0] = LevelControl::Id;
clusterBuffer.reduce_size(1);
}
else
{
clusterBuffer.reduce_size(0);
}
}
// Default function for LevelControl cluster, only checks if LevelControl is enabled on the endpoint
bool SupportsCluster(EndpointId endpoint, ClusterId cluster) override
{
return (cluster == LevelControl::Id) && (emberAfContainsServer(endpoint, LevelControl::Id));
}
/// @brief Serialize the Cluster's EFS value
/// @param endpoint target endpoint
/// @param cluster target cluster
/// @param serializedBytes data to serialize into EFS
/// @return CHIP_NO_ERROR if successfully serialized the data, CHIP_ERROR_INVALID_ARGUMENT otherwise
CHIP_ERROR SerializeSave(EndpointId endpoint, ClusterId cluster, MutableByteSpan & serializedBytes) override
{
using AttributeValuePair = ScenesManagement::Structs::AttributeValuePairStruct::Type;
DataModel::Nullable<uint8_t> level;
VerifyOrReturnError(Status::Success == Attributes::CurrentLevel::Get(endpoint, level), CHIP_ERROR_READ_FAILED);
AttributeValuePair pairs[kLevelMaxScenableAttributes];
uint8_t maxLevel;
VerifyOrReturnError(Status::Success == Attributes::MaxLevel::Get(endpoint, &maxLevel), CHIP_ERROR_READ_FAILED);
pairs[0].attributeID = Attributes::CurrentLevel::Id;
if (!level.IsNull())
{
pairs[0].valueUnsigned8.SetValue(level.Value());
}
else
{
pairs[0].valueUnsigned8.SetValue(NumericAttributeTraits<uint8_t>::kNullValue);
}
size_t attributeCount = 1;
if (LevelControlHasFeature(endpoint, LevelControl::Feature::kFrequency))
{
uint16_t frequency;
VerifyOrReturnError(Status::Success == Attributes::CurrentFrequency::Get(endpoint, &frequency), CHIP_ERROR_READ_FAILED);
pairs[attributeCount].attributeID = Attributes::CurrentFrequency::Id;
pairs[attributeCount].valueUnsigned16.SetValue(frequency);
attributeCount++;
}
DataModel::List<AttributeValuePair> attributeValueList(pairs, attributeCount);
return EncodeAttributeValueList(attributeValueList, serializedBytes);
}
/// @brief Default EFS interaction when applying scene to the OnOff Cluster
/// @param endpoint target endpoint
/// @param cluster target cluster
/// @param serializedBytes Data from nvm
/// @param timeMs transition time in ms
/// @return CHIP_NO_ERROR if value as expected, CHIP_ERROR_INVALID_ARGUMENT otherwise
CHIP_ERROR ApplyScene(EndpointId endpoint, ClusterId cluster, const ByteSpan & serializedBytes,
scenes::TransitionTimeMs timeMs) override
{
DataModel::DecodableList<ScenesManagement::Structs::AttributeValuePairStruct::DecodableType> attributeValueList;
ReturnErrorOnFailure(DecodeAttributeValueList(serializedBytes, attributeValueList));
size_t attributeCount = 0;
ReturnErrorOnFailure(attributeValueList.ComputeSize(&attributeCount));
VerifyOrReturnError(attributeCount <= kLevelMaxScenableAttributes, CHIP_ERROR_BUFFER_TOO_SMALL);
auto pair_iterator = attributeValueList.begin();
// The level control cluster should have a maximum of 2 attributes
uint8_t level = 0;
// TODO : Uncomment when frequency is supported by the level control cluster
// uint16_t frequency;
while (pair_iterator.Next())
{
auto & decodePair = pair_iterator.GetValue();
// If attribute ID was encoded, checks which attribute from LC cluster is there
switch (decodePair.attributeID)
{
case Attributes::CurrentLevel::Id:
VerifyOrReturnError(decodePair.valueUnsigned8.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
level = decodePair.valueUnsigned8.Value();
break;
case Attributes::CurrentFrequency::Id:
// TODO : Uncomment when frequency is supported by the level control cluster
// VerifyOrReturnError(decodePair.valueUnsigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
// frequency = decodePair.valueUnsigned16.Value();
break;
default:
return CHIP_ERROR_INVALID_ARGUMENT;
}
}
ReturnErrorOnFailure(pair_iterator.GetStatus());
// TODO : Implement action on frequency when frequency not provisional anymore
// if(LevelControlHasFeature(endpoint, LevelControl::Feature::kFrequency)){}
EmberAfLevelControlState * state = getState(endpoint);
if (level < state->minLevel || level > state->maxLevel)
{
NumericAttributeTraits<uint8_t>::SetNull(level);
}
if (!NumericAttributeTraits<uint8_t>::IsNullValue(level))
{
moveToLevelHandler(
endpoint, Commands::MoveToLevel::Id, level, DataModel::MakeNullable(static_cast<uint16_t>(timeMs / 100)),
chip::Optional<BitMask<OptionsBitmap>>(1), chip::Optional<BitMask<OptionsBitmap>>(1), INVALID_STORED_LEVEL);
}
return CHIP_NO_ERROR;
}
};
static DefaultLevelControlSceneHandler sLevelControlSceneHandler;
#endif // defined(MATTER_DM_PLUGIN_SCENES_MANAGEMENT) && CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS
#if !defined(IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS) && defined(MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP)
static void reallyUpdateCoupledColorTemp(EndpointId endpoint);
#define updateCoupledColorTemp(endpoint) reallyUpdateCoupledColorTemp(endpoint)
#else
#define updateCoupledColorTemp(endpoint)
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS && MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP
void emberAfLevelControlClusterServerTickCallback(EndpointId endpoint);
static void timerCallback(System::Layer *, void * callbackContext)
{
emberAfLevelControlClusterServerTickCallback(static_cast<EndpointId>(reinterpret_cast<uintptr_t>(callbackContext)));
}
static uint32_t computeCallbackWaitTimeMs(CallbackScheduleState & callbackSchedule, uint32_t delayMs)
{
auto delay = System::Clock::Milliseconds32(delayMs);
auto waitTime = delay;
const auto currentTime = System::SystemClock().GetMonotonicTimestamp();
// Subsequent call
if (callbackSchedule.runTime.count())
{
// Check whether the previous scheduled callback was late and whether its running time
// is smaller than the desired delay
// If the running time of the scheduled callback is greater than the desired delay
// then do nothing; do not flood the event loop if the device is not fast enough
if ((currentTime > callbackSchedule.idealTimestamp) && (callbackSchedule.runTime < delay))
{
System::Clock::Timestamp latency = currentTime - callbackSchedule.idealTimestamp;
if (latency >= delay)
{
waitTime = System::Clock::Milliseconds32(0);
}
else
{
waitTime -= latency;
}
}
}
// First-time call
else
{
// initialize idealTimestamp
callbackSchedule.idealTimestamp = currentTime;
}
callbackSchedule.idealTimestamp += System::Clock::Milliseconds32(delayMs);
callbackSchedule.runTime = System::Clock::Milliseconds32(0);
return waitTime.count();
}
static void scheduleTimerCallbackMs(EndpointId endpoint, uint32_t delayMs)
{
CHIP_ERROR err = DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Milliseconds32(delayMs), timerCallback,
reinterpret_cast<void *>(static_cast<uintptr_t>(endpoint)));
if (err != CHIP_NO_ERROR)
{
ChipLogError(Zcl, "Level Control Server failed to schedule event: %" CHIP_ERROR_FORMAT, err.Format());
}
}
static void cancelEndpointTimerCallback(EndpointId endpoint)
{
DeviceLayer::SystemLayer().CancelTimer(timerCallback, reinterpret_cast<void *>(static_cast<uintptr_t>(endpoint)));
}
static EmberAfLevelControlState * getState(EndpointId endpoint)
{
uint16_t ep =
emberAfGetClusterServerEndpointIndex(endpoint, LevelControl::Id, MATTER_DM_LEVEL_CONTROL_CLUSTER_SERVER_ENDPOINT_COUNT);
return (ep >= kLevelControlStateTableSize ? nullptr : &stateTable[ep]);
}
#if !defined(IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS) && defined(MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP)
static void reallyUpdateCoupledColorTemp(EndpointId endpoint)
{
LevelControl::Attributes::Options::TypeInfo::Type options;
Status status = Attributes::Options::Get(endpoint, &options);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "Unable to read Options attribute: 0x%X", to_underlying(status));
return;
}
if (emberAfContainsAttribute(endpoint, ColorControl::Id, ColorControl::Attributes::ColorTemperatureMireds::Id))
{
if (options.Has(OptionsBitmap::kCoupleColorTempToLevel))
{
emberAfPluginLevelControlCoupledColorTempChangeCallback(endpoint);
}
}
}
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS && MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP
/*
* @brief
* This function is used to update the current level attribute
* while respecting its defined quiet reporting quality:
* The attribute will be reported:
* - At most once per second, or
* - At the end of the movement/transition, or
* - When it changes from null to any other value and vice versa.
*
* @param endpoint: endpoint on which the currentLevel attribute must be updated.
* @param state: LevelControlState struct of this given endpoint.
* @param newValue: Value to update the attribute with
* @param isEndOfTransition: Boolean that indicate whether the update is occuring at the end of a level transition
* @return Success in setting the attribute value or the IM error code for the failure.
*/
static Status SetCurrentLevelQuietReport(EndpointId endpoint, EmberAfLevelControlState * state,
DataModel::Nullable<uint8_t> newValue, bool isEndOfTransition)
{
AttributeDirtyState dirtyState;
auto now = System::SystemClock().GetMonotonicTimestamp();
if (isEndOfTransition)
{
// At the start or end of the movement/transition we must report
auto predicate = [](const decltype(state->quietCurrentLevel)::SufficientChangePredicateCandidate &) -> bool {
return true;
};
dirtyState = state->quietCurrentLevel.SetValue(newValue, now, predicate);
}
else
{
// During transtions, reports should be at most once per second
System::Clock::Milliseconds64 reportInterval =
std::max(System::Clock::Milliseconds64(1000), System::Clock::Milliseconds64(state->transitionTimeMs / 4));
auto predicate = state->quietCurrentLevel.GetPredicateForSufficientTimeSinceLastDirty(reportInterval);
dirtyState = state->quietCurrentLevel.SetValue(newValue, now, predicate);
}
MarkAttributeDirty markDirty = MarkAttributeDirty::kNo;
if (dirtyState == AttributeDirtyState::kMustReport)
{
markDirty = MarkAttributeDirty::kYes;
}
return Attributes::CurrentLevel::Set(endpoint, state->quietCurrentLevel.value(), markDirty);
}
void emberAfLevelControlClusterServerTickCallback(EndpointId endpoint)
{
EmberAfLevelControlState * state = getState(endpoint);
Status status;
DataModel::Nullable<uint8_t> currentLevel;
const auto callbackStartTimestamp = System::SystemClock().GetMonotonicTimestamp();
bool isTransitionStart = false;
bool isTransitionEnd = false;
if (state == nullptr)
{
return;
}
isTransitionStart = (state->elapsedTimeMs == 0);
state->elapsedTimeMs += state->eventDurationMs;
// Read the attribute; print error message and return if it can't be read
status = LevelControl::Attributes::CurrentLevel::Get(endpoint, currentLevel);
if (status != Status::Success || currentLevel.IsNull())
{
ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status));
state->callbackSchedule.runTime = System::Clock::Milliseconds32(0);
writeRemainingTime(endpoint, 0);
return;
}
ChipLogDetail(Zcl, "Event: move from %d", currentLevel.Value());
// adjust by the proper amount, either up or down
if (state->transitionTimeMs == 0)
{
// Immediate, not over a time interval.
currentLevel.SetNonNull(state->moveToLevel);
}
else if (state->increasing)
{
assert(currentLevel.Value() < state->maxLevel);
assert(currentLevel.Value() < state->moveToLevel);
currentLevel.SetNonNull(static_cast<uint8_t>(currentLevel.Value() + 1));
}
else
{
assert(state->minLevel < currentLevel.Value());
assert(state->moveToLevel < currentLevel.Value());
currentLevel.SetNonNull(static_cast<uint8_t>(currentLevel.Value() - 1));
}
ChipLogDetail(Zcl, " to %d ", currentLevel.Value());
ChipLogDetail(Zcl, "(diff %c1)", state->increasing ? '+' : '-');
// Are we at the requested level?
isTransitionEnd = (currentLevel.Value() == state->moveToLevel);
status = SetCurrentLevelQuietReport(endpoint, state, currentLevel, isTransitionEnd);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "ERR: writing current level %x", to_underlying(status));
state->callbackSchedule.runTime = System::Clock::Milliseconds32(0);
writeRemainingTime(endpoint, 0);
return;
}
updateCoupledColorTemp(endpoint);
if (isTransitionEnd)
{
if (state->commandId == Commands::MoveToLevelWithOnOff::Id || state->commandId == Commands::MoveWithOnOff::Id ||
state->commandId == Commands::StepWithOnOff::Id)
{
setOnOffValue(endpoint, (currentLevel.Value() != state->minLevel));
}
if (state->storedLevel != INVALID_STORED_LEVEL)
{
uint8_t storedLevel8u = (uint8_t) state->storedLevel;
status = Attributes::CurrentLevel::Set(endpoint, storedLevel8u);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "ERR: writing current level %x", to_underlying(status));
}
else
{
updateCoupledColorTemp(endpoint);
}
}
state->callbackSchedule.runTime = System::Clock::Milliseconds32(0);
writeRemainingTime(endpoint, 0);
}
else
{
state->callbackSchedule.runTime = System::SystemClock().GetMonotonicTimestamp() - callbackStartTimestamp;
writeRemainingTime(endpoint, static_cast<uint16_t>(state->transitionTimeMs - state->elapsedTimeMs), isTransitionStart);
scheduleTimerCallbackMs(endpoint, computeCallbackWaitTimeMs(state->callbackSchedule, state->eventDurationMs));
}
}
static void writeRemainingTime(EndpointId endpoint, uint16_t remainingTimeMs, bool isNewTransition)
{
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_LEVEL_CONTROL_REMAINING_TIME
if (emberAfContainsAttribute(endpoint, LevelControl::Id, LevelControl::Attributes::RemainingTime::Id))
{
// Convert milliseconds to tenths of a second, rounding any fractional value
// up to the nearest whole value. This means:
//
// 0 ms = 0.00 ds = 0 ds
// 1 ms = 0.01 ds = 1 ds
// ...
// 100 ms = 1.00 ds = 1 ds
// 101 ms = 1.01 ds = 2 ds
// ...
// 200 ms = 2.00 ds = 2 ds
// 201 ms = 2.01 ds = 3 ds
// ...
//
// This is done to ensure that the attribute, in tenths of a second, only
// goes to zero when the remaining time in milliseconds is actually zero.
auto markDirty = MarkAttributeDirty::kNo;
auto state = getState(endpoint);
auto now = System::SystemClock().GetMonotonicTimestamp();
uint16_t remainingTimeDs = static_cast<uint16_t>((remainingTimeMs + 99) / 100);
uint16_t lastRemainingTime = state->quietRemainingTime.value().ValueOr(0);
// RemainingTime Quiet report conditions:
// - When it changes to 0, or
// - When it changes from 0 to any value higher than 10, or
// - When it changes, with a delta larger than 10, caused by the invoke of a command.
auto predicate = [isNewTransition, lastRemainingTime](
const decltype(state->quietRemainingTime)::SufficientChangePredicateCandidate & candidate) -> bool {
constexpr uint16_t reportDelta = 10;
bool isDirty = false;
if (candidate.newValue.Value() == 0 ||
(candidate.lastDirtyValue.Value() == 0 && candidate.newValue.Value() > reportDelta))
{
isDirty = true;
}
else if (isNewTransition &&
(candidate.newValue.Value() > static_cast<uint32_t>(lastRemainingTime + reportDelta) ||
static_cast<uint32_t>(candidate.newValue.Value() + reportDelta) < lastRemainingTime ||
candidate.newValue.Value() > static_cast<uint32_t>(candidate.lastDirtyValue.Value() + reportDelta)))
{
isDirty = true;
}
return isDirty;
};
if (state->quietRemainingTime.SetValue(remainingTimeDs, now, predicate) == AttributeDirtyState::kMustReport)
{
markDirty = MarkAttributeDirty::kYes;
}
Attributes::RemainingTime::Set(endpoint, state->quietRemainingTime.value().Value(), markDirty);
}
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_LEVEL_CONTROL_REMAINING_TIME
}
static void setOnOffValue(EndpointId endpoint, bool onOff)
{
#ifdef MATTER_DM_PLUGIN_ON_OFF
if (emberAfContainsServer(endpoint, OnOff::Id))
{
ChipLogProgress(Zcl, "Setting on/off to %s due to level change", onOff ? "ON" : "OFF");
OnOffServer::Instance().setOnOffValue(endpoint, (onOff ? OnOff::Commands::On::Id : OnOff::Commands::Off::Id), true);
}
#endif // MATTER_DM_PLUGIN_ON_OFF
}
static bool shouldExecuteIfOff(EndpointId endpoint, CommandId commandId, chip::Optional<chip::BitMask<OptionsBitmap>> optionsMask,
chip::Optional<chip::BitMask<OptionsBitmap>> optionsOverride)
{
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS
if (emberAfContainsAttribute(endpoint, LevelControl::Id, Attributes::Options::Id))
{
// From 3.10.2.2.8.1 of ZCL7 document 14-0127-20j-zcl-ch-3-general.docx:
// "Command execution SHALL NOT continue beyond the Options processing if
// all of these criteria are true:
// - The command is one of the ‘without On/Off’ commands: Move, Move to
// Level, Stop, or Step.
// - The On/Off cluster exists on the same endpoint as this cluster.
// - The OnOff attribute of the On/Off cluster, on this endpoint, is 0x00
// (FALSE).
// - The value of the ExecuteIfOff bit is 0."
if (commandId > Commands::Stop::Id)
{
return true;
}
if (!emberAfContainsServer(endpoint, OnOff::Id))
{
return true;
}
LevelControl::Attributes::Options::TypeInfo::Type options;
Status status = Attributes::Options::Get(endpoint, &options);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "Unable to read Options attribute: 0x%X", to_underlying(status));
// If we can't read the attribute, then we should just assume that it has its
// default value.
}
bool on;
status = OnOff::Attributes::OnOff::Get(endpoint, &on);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "Unable to read OnOff attribute: 0x%X", to_underlying(status));
return true;
}
// The device is on - hence ExecuteIfOff does not matter
if (on)
{
return true;
}
// The OptionsMask & OptionsOverride fields SHALL both be present or both
// omitted in the command. A temporary Options bitmap SHALL be created from
// the Options attribute, using the OptionsMask & OptionsOverride fields, if
// present. Each bit of the temporary Options bitmap SHALL be determined as
// follows:
// Each bit in the Options attribute SHALL determine the corresponding bit in
// the temporary Options bitmap, unless the OptionsMask field is present and
// has the corresponding bit set to 1, in which case the corresponding bit in
// the OptionsOverride field SHALL determine the corresponding bit in the
// temporary Options bitmap.
// The resulting temporary Options bitmap SHALL then be processed as defined
// in section 3.10.2.2.3.
// ---------- The following order is important in decision making -------
// -----------more readable ----------
//
if (!optionsMask.HasValue() || !optionsOverride.HasValue())
{
// in case optionMask or optionOverride is not set, use of option
// attribute to decide execution of the command
return options.Has(OptionsBitmap::kExecuteIfOff);
}
// ---------- The above is to distinguish if the payload is present or not
if (optionsMask.Value().Has(OptionsBitmap::kExecuteIfOff))
{
// Mask is present and set in the command payload, this indicates
// use the over ride as temporary option
return optionsOverride.Value().Has(OptionsBitmap::kExecuteIfOff);
}
// if we are here - use the option bits
return options.Has(OptionsBitmap::kExecuteIfOff);
}
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS
// By default, we return true to continue supporting backwards compatibility.
return true;
}
bool emberAfLevelControlClusterMoveToLevelCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
const Commands::MoveToLevel::DecodableType & commandData)
{
MATTER_TRACE_SCOPE("MoveToLevel", "LevelControl");
commandObj->AddStatus(commandPath, LevelControlServer::MoveToLevel(commandPath.mEndpointId, commandData));
return true;
}
namespace LevelControlServer {
Status MoveToLevel(EndpointId endpointId, const Commands::MoveToLevel::DecodableType & commandData)
{
auto & level = commandData.level;
auto & transitionTime = commandData.transitionTime;
auto & optionsMask = commandData.optionsMask;
auto & optionsOverride = commandData.optionsOverride;
if (transitionTime.IsNull())
{
ChipLogProgress(Zcl, "%s MOVE_TO_LEVEL %x null %x %x", "RX level-control:", level, optionsMask.Raw(),
optionsOverride.Raw());
}
else
{
ChipLogProgress(Zcl, "%s MOVE_TO_LEVEL %x %2x %x %x", "RX level-control:", level, transitionTime.Value(), optionsMask.Raw(),
optionsOverride.Raw());
}
return moveToLevelHandler(endpointId, Commands::MoveToLevel::Id, level, transitionTime,
Optional<BitMask<OptionsBitmap>>(optionsMask), Optional<BitMask<OptionsBitmap>>(optionsOverride),
INVALID_STORED_LEVEL); // Don't revert to the stored level
}
#ifdef MATTER_DM_PLUGIN_SCENES_MANAGEMENT
chip::scenes::SceneHandler * GetSceneHandler()
{
#if CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS
return &sLevelControlSceneHandler;
#else
return nullptr;
#endif // CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS
}
#endif // ifdef MATTER_DM_PLUGIN_SCENES_MANAGEMENT
} // namespace LevelControlServer
bool emberAfLevelControlClusterMoveToLevelWithOnOffCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
const Commands::MoveToLevelWithOnOff::DecodableType & commandData)
{
MATTER_TRACE_SCOPE("MoveToLevelWithOnOff", "LevelControl");
auto & level = commandData.level;
auto & transitionTime = commandData.transitionTime;
auto & optionsMask = commandData.optionsMask;
auto & optionsOverride = commandData.optionsOverride;
if (transitionTime.IsNull())
{
ChipLogProgress(Zcl, "%s MOVE_TO_LEVEL_WITH_ON_OFF %x null %x %x", "RX level-control:", level, optionsMask.Raw(),
optionsOverride.Raw());
}
else
{
ChipLogProgress(Zcl, "%s MOVE_TO_LEVEL_WITH_ON_OFF %x %2x %x %x", "RX level-control:", level, transitionTime.Value(),
optionsMask.Raw(), optionsOverride.Raw());
}
Status status =
moveToLevelHandler(commandPath.mEndpointId, Commands::MoveToLevelWithOnOff::Id, level, transitionTime,
Optional<BitMask<OptionsBitmap>>(optionsMask), Optional<BitMask<OptionsBitmap>>(optionsOverride),
INVALID_STORED_LEVEL); // Don't revert to the stored level
commandObj->AddStatus(commandPath, status);
return true;
}
bool emberAfLevelControlClusterMoveCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
const Commands::Move::DecodableType & commandData)
{
MATTER_TRACE_SCOPE("Move", "LevelControl");
auto & moveMode = commandData.moveMode;
auto & rate = commandData.rate;
auto & optionsMask = commandData.optionsMask;
auto & optionsOverride = commandData.optionsOverride;
if (rate.IsNull())
{
ChipLogProgress(Zcl, "%s MOVE %x null %x %x", "RX level-control:", to_underlying(moveMode), optionsMask.Raw(),
optionsOverride.Raw());
}
else
{
ChipLogProgress(Zcl, "%s MOVE %x %u %x %x", "RX level-control:", to_underlying(moveMode), rate.Value(), optionsMask.Raw(),
optionsOverride.Raw());
}
moveHandler(commandObj, commandPath, moveMode, rate, Optional<BitMask<OptionsBitmap>>(optionsMask),
Optional<BitMask<OptionsBitmap>>(optionsOverride));
return true;
}
bool emberAfLevelControlClusterMoveWithOnOffCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
const Commands::MoveWithOnOff::DecodableType & commandData)
{
MATTER_TRACE_SCOPE("MoveWithOnOff", "LevelControl");
auto & moveMode = commandData.moveMode;
auto & rate = commandData.rate;
auto & optionsMask = commandData.optionsMask;
auto & optionsOverride = commandData.optionsOverride;
if (rate.IsNull())
{
ChipLogProgress(Zcl, "%s MOVE_WITH_ON_OFF %x null %x %x", "RX level-control:", to_underlying(moveMode), optionsMask.Raw(),
optionsOverride.Raw());
}
else
{
ChipLogProgress(Zcl, "%s MOVE_WITH_ON_OFF %u %2x %x %x", "RX level-control:", to_underlying(moveMode), rate.Value(),
optionsMask.Raw(), optionsOverride.Raw());
}
moveHandler(commandObj, commandPath, moveMode, rate, Optional<BitMask<OptionsBitmap>>(optionsMask),
Optional<BitMask<OptionsBitmap>>(optionsOverride));
return true;
}
bool emberAfLevelControlClusterStepCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
const Commands::Step::DecodableType & commandData)
{
MATTER_TRACE_SCOPE("Step", "LevelControl");
auto & stepMode = commandData.stepMode;
auto & stepSize = commandData.stepSize;
auto & transitionTime = commandData.transitionTime;
auto & optionsMask = commandData.optionsMask;
auto & optionsOverride = commandData.optionsOverride;
if (transitionTime.IsNull())
{
ChipLogProgress(Zcl, "%s STEP %x %x null %x %x", "RX level-control:", to_underlying(stepMode), stepSize, optionsMask.Raw(),
optionsOverride.Raw());
}
else
{
ChipLogProgress(Zcl, "%s STEP %x %x %2x %x %x", "RX level-control:", to_underlying(stepMode), stepSize,
transitionTime.Value(), optionsMask.Raw(), optionsOverride.Raw());
}
stepHandler(commandObj, commandPath, stepMode, stepSize, transitionTime, Optional<BitMask<OptionsBitmap>>(optionsMask),
Optional<BitMask<OptionsBitmap>>(optionsOverride));
return true;
}
bool emberAfLevelControlClusterStepWithOnOffCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
const Commands::StepWithOnOff::DecodableType & commandData)
{
MATTER_TRACE_SCOPE("StepWithOnOff", "LevelControl");
auto & stepMode = commandData.stepMode;
auto & stepSize = commandData.stepSize;
auto & transitionTime = commandData.transitionTime;
auto & optionsMask = commandData.optionsMask;
auto & optionsOverride = commandData.optionsOverride;
if (transitionTime.IsNull())
{
ChipLogProgress(Zcl, "%s STEP_WITH_ON_OFF %x %x null %x %x", "RX level-control:", to_underlying(stepMode), stepSize,
optionsMask.Raw(), optionsOverride.Raw());
}
else
{
ChipLogProgress(Zcl, "%s STEP_WITH_ON_OFF %x %x %2x %x %x", "RX level-control:", to_underlying(stepMode), stepSize,
transitionTime.Value(), optionsMask.Raw(), optionsOverride.Raw());
}
stepHandler(commandObj, commandPath, stepMode, stepSize, transitionTime, Optional<BitMask<OptionsBitmap>>(optionsMask),
Optional<BitMask<OptionsBitmap>>(optionsOverride));
return true;
}
bool emberAfLevelControlClusterStopCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
const Commands::Stop::DecodableType & commandData)
{
MATTER_TRACE_SCOPE("Stop", "LevelControl");
auto & optionsMask = commandData.optionsMask;
auto & optionsOverride = commandData.optionsOverride;
ChipLogProgress(Zcl, "%s STOP", "RX level-control:");
stopHandler(commandObj, commandPath, Optional<BitMask<OptionsBitmap>>(optionsMask),
Optional<BitMask<OptionsBitmap>>(optionsOverride));
return true;
}
bool emberAfLevelControlClusterStopWithOnOffCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
const Commands::StopWithOnOff::DecodableType & commandData)
{
MATTER_TRACE_SCOPE("StopWithOnOff", "LevelControl");
auto & optionsMask = commandData.optionsMask;
auto & optionsOverride = commandData.optionsOverride;
ChipLogProgress(Zcl, "%s STOP_WITH_ON_OFF", "RX level-control:");
stopHandler(commandObj, commandPath, Optional<BitMask<OptionsBitmap>>(optionsMask),
Optional<BitMask<OptionsBitmap>>(optionsOverride));
return true;
}
static Status moveToLevelHandler(EndpointId endpoint, CommandId commandId, uint8_t level,
DataModel::Nullable<uint16_t> transitionTimeDs, chip::Optional<BitMask<OptionsBitmap>> optionsMask,
chip::Optional<BitMask<OptionsBitmap>> optionsOverride, uint16_t storedLevel)
{
EmberAfLevelControlState * state = getState(endpoint);
DataModel::Nullable<uint8_t> currentLevel;
uint8_t actualStepSize;
if (state == nullptr)
{
return Status::Failure;
}
if (level > MATTER_DM_PLUGIN_LEVEL_CONTROL_MAXIMUM_LEVEL)
{
return Status::InvalidCommand;
}
if (!shouldExecuteIfOff(endpoint, commandId, optionsMask, optionsOverride))
{
return Status::Success;
}
// Cancel any currently active command before fiddling with the state.
cancelEndpointTimerCallback(endpoint);
Status status = Attributes::CurrentLevel::Get(endpoint, currentLevel);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status));
return status;
}
if (currentLevel.IsNull())
{
ChipLogProgress(Zcl, "ERR: Current Level is null");
return Status::Failure;
}
state->commandId = commandId;
// Move To Level commands cause the device to move from its current level to
// the specified level at the specified rate.
if (state->maxLevel <= level)
{
state->moveToLevel = state->maxLevel;
}
else if (level <= state->minLevel)
{
state->moveToLevel = state->minLevel;
}
else
{
state->moveToLevel = level;
}
// If the level is decreasing, the On/Off attribute is left unchanged. This
// logic is to prevent a light from transitioning from off to bright to dim.
// Instead, a light that is off will stay off until the target level is
// reached.
if (currentLevel.Value() <= state->moveToLevel)
{
if (commandId == Commands::MoveToLevelWithOnOff::Id)
{
setOnOffValue(endpoint, (state->moveToLevel != state->minLevel));
}
if (currentLevel.Value() == state->moveToLevel)
{
return Status::Success;
}
state->increasing = true;
actualStepSize = static_cast<uint8_t>(state->moveToLevel - currentLevel.Value());
}
else
{
state->increasing = false;
actualStepSize = static_cast<uint8_t>(currentLevel.Value() - state->moveToLevel);
}
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION
// If the Transition time field takes the value null, then the time taken
// to move to the new level shall instead be determined by the On/Off
// Transition Time attribute. If On/Off Transition Time, which is an
// optional attribute, is not present, the device shall move to its new level
// as fast as it is able.
if (transitionTimeDs.IsNull())
{
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_ON_OFF_TRANSITION_TIME
if (emberAfContainsAttribute(endpoint, LevelControl::Id, Attributes::OnOffTransitionTime::Id))
{
uint16_t onOffTransitionTime = 0;
status = Attributes::OnOffTransitionTime::Get(endpoint, &onOffTransitionTime);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "ERR: reading on/off transition time %x", to_underlying(status));
return status;
}
// Transition time comes in (or is stored, in the case of On/Off Transition
// Time) as tenths of a second, but we work in milliseconds.
state->transitionTimeMs = (onOffTransitionTime * MILLISECOND_TICKS_PER_SECOND / 10);
}
else
{
state->transitionTimeMs = FASTEST_TRANSITION_TIME_MS;
}
#else
// If the Transition Time field is 0xFFFF and On/Off Transition Time,
// which is an optional attribute, is not present, the device shall move to
// its new level as fast as it is able.
state->transitionTimeMs = FASTEST_TRANSITION_TIME_MS;
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_ON_OFF_TRANSITION_TIME
}
else
{
// Transition time comes in (or is stored, in the case of On/Off Transition
// Time) as tenths of a second, but we work in milliseconds.
state->transitionTimeMs = (transitionTimeDs.Value() * MILLISECOND_TICKS_PER_SECOND / 10);
}
#else
// Transition is not supported so always use fastest transition time and ignore
// both the provided transition time as well as OnOffTransitionTime.
ChipLogProgress(Zcl, "Device does not support transition, ignoring transition time");
state->transitionTimeMs = FASTEST_TRANSITION_TIME_MS;
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION
// The duration between events will be the transition time divided by the
// distance we must move.
state->eventDurationMs = state->transitionTimeMs / std::max(static_cast<uint8_t>(1u), actualStepSize);
state->elapsedTimeMs = 0;
state->storedLevel = storedLevel;
state->callbackSchedule.runTime = System::Clock::Milliseconds32(0);
#ifdef MATTER_DM_PLUGIN_SCENES_MANAGEMENT
// The level has changed, the scene is no longer valid.
if (emberAfContainsServer(endpoint, ScenesManagement::Id))
{
ScenesManagement::ScenesServer::Instance().MakeSceneInvalidForAllFabrics(endpoint);
}
#endif // MATTER_DM_PLUGIN_SCENES_MANAGEMENT
// The setup was successful, so mark the new state as active and return.
scheduleTimerCallbackMs(endpoint, computeCallbackWaitTimeMs(state->callbackSchedule, state->eventDurationMs));
#ifdef MATTER_DM_PLUGIN_ON_OFF
// Check that the received MoveToLevelWithOnOff produces a On action and that the onoff support the lighting featuremap
if (commandId == Commands::MoveToLevelWithOnOff::Id && state->moveToLevel != state->minLevel &&
OnOffServer::Instance().SupportsLightingApplications(endpoint))
{
OnOff::Attributes::GlobalSceneControl::Set(endpoint, true);
}
#endif // MATTER_DM_PLUGIN_ON_OFF
return Status::Success;
}
static void moveHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, MoveModeEnum moveMode,
DataModel::Nullable<uint8_t> rate, chip::Optional<BitMask<OptionsBitmap>> optionsMask,
chip::Optional<BitMask<OptionsBitmap>> optionsOverride)
{
Status status;
uint8_t difference;
EmberAfLevelControlState * state;
DataModel::Nullable<uint8_t> currentLevel;
EndpointId endpoint = commandPath.mEndpointId;
CommandId commandId = commandPath.mCommandId;
// Validate the received rate and moveMode first.
if (rate == static_cast<uint8_t>(0) || moveMode == MoveModeEnum::kUnknownEnumValue)
{
status = Status::InvalidCommand;
goto send_default_response;
}
state = getState(endpoint);
if (state == nullptr)
{
status = Status::Failure;
goto send_default_response;
}
if (!shouldExecuteIfOff(endpoint, commandId, optionsMask, optionsOverride))
{
status = Status::Success;
goto send_default_response;
}
uint8_t eventDuration; // use this local var so state->eventDurationMs is only set once the command is validated.
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION
// If the Rate field is null, the device should move at the default move rate, if available,
// Otherwise, move as fast as possible
if (rate.IsNull())
{
DataModel::Nullable<uint8_t> defaultMoveRate;
status = Attributes::DefaultMoveRate::Get(endpoint, defaultMoveRate);
if (status != Status::Success || defaultMoveRate.IsNull())
{
ChipLogProgress(Zcl, "ERR: reading default move rate %x", to_underlying(status));
eventDuration = FASTEST_TRANSITION_TIME_MS;
}
else
{
// This should never occur, but old devices could have this, now invalid, value stored.
if (defaultMoveRate.Value() == 0)
{
// The spec is not explicit about what should be done if this happens.
// For now Error out if DefaultMoveRate is equal to 0 as this is invalid
// until spec defines a behaviour.
status = Status::InvalidCommand;
goto send_default_response;
}
// Already checked that defaultMoveRate.Value() != 0.
eventDuration = static_cast<uint8_t>(MILLISECOND_TICKS_PER_SECOND / defaultMoveRate.Value());
}
}
else
{
// Already confirmed rate.Value() != 0.
eventDuration = static_cast<uint8_t>(MILLISECOND_TICKS_PER_SECOND / rate.Value());
}
#else
// Transition/rate is not supported so always use fastest transition time and ignore
// both the provided transition time as well as OnOffTransitionTime.
ChipLogProgress(Zcl, "Device does not support transition, ignoring rate");
eventDuration = FASTEST_TRANSITION_TIME_MS;
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION
// Cancel any currently active command before fiddling with the state.
cancelEndpointTimerCallback(endpoint);
state->eventDurationMs = eventDuration;
status = Attributes::CurrentLevel::Get(endpoint, currentLevel);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status));
goto send_default_response;
}
if (currentLevel.IsNull())
{
ChipLogProgress(Zcl, "ERR: Current Level is null");
status = Status::Failure;
goto send_default_response;
}
state->commandId = commandId;
// Move commands cause the device to move from its current level to either
// the maximum or minimum level at the specified rate.
switch (moveMode)
{
case MoveModeEnum::kUp:
state->increasing = true;
state->moveToLevel = state->maxLevel;
difference = static_cast<uint8_t>(state->maxLevel - currentLevel.Value());
break;
case MoveModeEnum::kDown:
state->increasing = false;
state->moveToLevel = state->minLevel;
difference = static_cast<uint8_t>(currentLevel.Value() - state->minLevel);
break;
default:
status = Status::InvalidCommand;
goto send_default_response;
}
// If the level is decreasing, the On/Off attribute is left unchanged. This
// logic is to prevent a light from transitioning from off to bright to dim.
// Instead, a light that is off will stay off until the target level is
// reached.
if (currentLevel.Value() <= state->moveToLevel)
{
if (commandId == Commands::MoveWithOnOff::Id)
{
setOnOffValue(endpoint, (state->moveToLevel != state->minLevel));
}
if (currentLevel.Value() == state->moveToLevel)
{
status = Status::Success;
goto send_default_response;
}
}
state->transitionTimeMs = difference * state->eventDurationMs;
state->elapsedTimeMs = 0;
// storedLevel is not used for Move commands.
state->storedLevel = INVALID_STORED_LEVEL;
state->callbackSchedule.runTime = System::Clock::Milliseconds32(0);
// The setup was successful, so mark the new state as active and return.
scheduleTimerCallbackMs(endpoint, computeCallbackWaitTimeMs(state->callbackSchedule, state->eventDurationMs));
status = Status::Success;
send_default_response:
commandObj->AddStatus(commandPath, status);
}
static void stepHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, StepModeEnum stepMode,
uint8_t stepSize, DataModel::Nullable<uint16_t> transitionTimeDs,
chip::Optional<BitMask<OptionsBitmap>> optionsMask, chip::Optional<BitMask<OptionsBitmap>> optionsOverride)
{
Status status;
EmberAfLevelControlState * state;
DataModel::Nullable<uint8_t> currentLevel;
EndpointId endpoint = commandPath.mEndpointId;
CommandId commandId = commandPath.mCommandId;
uint8_t actualStepSize = stepSize;
// Validate the received stepSize and stepMode first.
if (stepSize == 0 || stepMode == StepModeEnum::kUnknownEnumValue)
{
status = Status::InvalidCommand;
goto send_default_response;
}
state = getState(endpoint);
if (state == nullptr)
{
status = Status::Failure;
goto send_default_response;
}
if (!shouldExecuteIfOff(endpoint, commandId, optionsMask, optionsOverride))
{
status = Status::Success;
goto send_default_response;
}
// Cancel any currently active command before fiddling with the state.
cancelEndpointTimerCallback(endpoint);
status = Attributes::CurrentLevel::Get(endpoint, currentLevel);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status));
goto send_default_response;
}
if (currentLevel.IsNull())
{
ChipLogProgress(Zcl, "ERR: Current Level is null");
status = Status::Failure;
goto send_default_response;
}
state->commandId = commandId;
// Step commands cause the device to move from its current level to a new
// level over the specified transition time.
switch (stepMode)
{
case StepModeEnum::kUp:
state->increasing = true;
if (state->maxLevel - currentLevel.Value() < stepSize)
{
state->moveToLevel = state->maxLevel;
actualStepSize = static_cast<uint8_t>(state->maxLevel - currentLevel.Value());
}
else
{
state->moveToLevel = static_cast<uint8_t>(currentLevel.Value() + stepSize);
}
break;
case StepModeEnum::kDown:
state->increasing = false;
if (currentLevel.Value() - state->minLevel < stepSize)
{
state->moveToLevel = state->minLevel;
actualStepSize = static_cast<uint8_t>(currentLevel.Value() - state->minLevel);
}
else
{
state->moveToLevel = static_cast<uint8_t>(currentLevel.Value() - stepSize);
}
break;
default:
// Should never happen as it is verified at function entry.
status = Status::InvalidCommand;
goto send_default_response;
}
// If the level is decreasing, the On/Off attribute is left unchanged. This
// logic is to prevent a light from transitioning from off to bright to dim.
// Instead, a light that is off will stay off until the target level is
// reached.
if (currentLevel.Value() <= state->moveToLevel)
{
if (commandId == Commands::StepWithOnOff::Id)
{
setOnOffValue(endpoint, (state->moveToLevel != state->minLevel));
}
if (currentLevel.Value() == state->moveToLevel)
{
status = Status::Success;
goto send_default_response;
}
}
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION
// If the Transition Time field is null, the device should move as fast as
// it is able.
if (transitionTimeDs.IsNull())
{
state->transitionTimeMs = FASTEST_TRANSITION_TIME_MS;
}
else
{
// Transition time comes in as tenths of a second, but we work in
// milliseconds.
state->transitionTimeMs = (transitionTimeDs.Value() * MILLISECOND_TICKS_PER_SECOND / 10);
// If the new level was pegged at the minimum level, the transition time
// shall be proportionally reduced. This is done after the conversion to
// milliseconds to reduce rounding errors in integer division.
if (stepSize != actualStepSize)
{
state->transitionTimeMs = (state->transitionTimeMs * actualStepSize / std::max(static_cast<uint8_t>(1u), stepSize));
}
}
#else
// Transition is not supported so always use fastest transition time and ignore
// both the provided transition time as well as OnOffTransitionTime.
ChipLogProgress(Zcl, "Device does not support transition, ignoring transition time");
state->transitionTimeMs = FASTEST_TRANSITION_TIME_MS;
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION
// The duration between events will be the transition time divided by the
// distance we must move.
state->eventDurationMs = state->transitionTimeMs / std::max(static_cast<uint8_t>(1u), actualStepSize);
state->elapsedTimeMs = 0;
// storedLevel is not used for Step commands
state->storedLevel = INVALID_STORED_LEVEL;
state->callbackSchedule.runTime = System::Clock::Milliseconds32(0);
// The setup was successful, so mark the new state as active and return.
scheduleTimerCallbackMs(endpoint, computeCallbackWaitTimeMs(state->callbackSchedule, state->eventDurationMs));
status = Status::Success;
send_default_response:
commandObj->AddStatus(commandPath, status);
}
static void stopHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
chip::Optional<BitMask<OptionsBitmap>> optionsMask, chip::Optional<BitMask<OptionsBitmap>> optionsOverride)
{
EndpointId endpoint = commandPath.mEndpointId;
CommandId commandId = commandPath.mCommandId;
EmberAfLevelControlState * state = getState(endpoint);
Status status = Status::Success;
if (state == nullptr)
{
status = Status::Failure;
goto send_default_response;
}
if (!shouldExecuteIfOff(endpoint, commandId, optionsMask, optionsOverride))
{
goto send_default_response;
}
// Cancel any currently active command.
cancelEndpointTimerCallback(endpoint);
SetCurrentLevelQuietReport(endpoint, state, state->quietCurrentLevel.value(), true /*isEndOfTransition*/);
writeRemainingTime(endpoint, 0);
send_default_response:
commandObj->AddStatus(commandPath, status);
}
// Follows 07-5123-04 (ZigBee Cluster Library doc), section 3.10.2.1.1.
// Quotes are from table 3.46.
void emberAfOnOffClusterLevelControlEffectCallback(EndpointId endpoint, bool newValue)
{
DataModel::Nullable<uint8_t> resolvedLevel;
DataModel::Nullable<uint8_t> temporaryCurrentLevelCache;
DataModel::Nullable<uint16_t> transitionTime;
uint16_t currentOnOffTransitionTime;
Status status;
bool useOnLevel = false;
EmberAfLevelControlState * state = getState(endpoint);
if (state == nullptr)
{
ChipLogProgress(Zcl, "ERR: Level control cluster not available on ep%d", endpoint);
return;
}
uint8_t minimumLevelAllowedForTheDevice = state->minLevel;
// "Temporarily store CurrentLevel."
status = Attributes::CurrentLevel::Get(endpoint, temporaryCurrentLevelCache);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status));
return;
}
if (temporaryCurrentLevelCache.IsNull())
{
ChipLogProgress(Zcl, "ERR: Current Level is null");
return;
}
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_ON_LEVEL_ATTRIBUTE
if (emberAfContainsAttribute(endpoint, LevelControl::Id, Attributes::OnLevel::Id))
{
status = Attributes::OnLevel::Get(endpoint, resolvedLevel);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "ERR: reading on level %x", to_underlying(status));
return;
}
if (resolvedLevel.IsNull())
{
// OnLevel has undefined value; fall back to CurrentLevel.
resolvedLevel.SetNonNull(temporaryCurrentLevelCache.Value());
}
else
{
useOnLevel = true;
}
}
else
{
resolvedLevel.SetNonNull(temporaryCurrentLevelCache.Value());
}
#else
resolvedLevel.SetNonNull(temporaryCurrentLevelCache.Value());
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_ON_LEVEL_ATTRIBUTE
// Read the OnOffTransitionTime attribute.
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_ON_OFF_TRANSITION_TIME
if (emberAfContainsAttribute(endpoint, LevelControl::Id, Attributes::OnOffTransitionTime::Id))
{
status = Attributes::OnOffTransitionTime::Get(endpoint, &currentOnOffTransitionTime);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status));
return;
}
transitionTime.SetNonNull(currentOnOffTransitionTime);
}
else
{
transitionTime.SetNull();
}
#else
transitionTime.SetNull();
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_ON_OFF_TRANSITION_TIME
if (newValue)
{
// If newValue is OnOff::Commands::On::Id...
// "Set CurrentLevel to minimum level allowed for the device."
status = SetCurrentLevelQuietReport(endpoint, state, minimumLevelAllowedForTheDevice, false /*isEndOfTransition*/);
if (status != Status::Success)
{
ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status));
return;
}
// "Move CurrentLevel to OnLevel, or to the stored level if OnLevel is not
// defined, over the time period OnOffTransitionTime."
moveToLevelHandler(endpoint, Commands::MoveToLevel::Id, resolvedLevel.Value(), transitionTime, chip::NullOptional,
chip::NullOptional,
INVALID_STORED_LEVEL); // Don't revert to stored level
}
else
{
// ...else if newValue is OnOff::Commands::Off::Id...
// "Move CurrentLevel to the minimum level allowed for the device over the
// time period OnOffTransitionTime."
if (useOnLevel)
{
// If OnLevel is defined, don't revert to stored level.
moveToLevelHandler(endpoint, Commands::MoveToLevelWithOnOff::Id, minimumLevelAllowedForTheDevice, transitionTime,
chip::NullOptional, chip::NullOptional, INVALID_STORED_LEVEL);
}
else
{
// If OnLevel is not defined, set the CurrentLevel to the stored level.
moveToLevelHandler(endpoint, Commands::MoveToLevelWithOnOff::Id, minimumLevelAllowedForTheDevice, transitionTime,
chip::NullOptional, chip::NullOptional, temporaryCurrentLevelCache.Value());
}
}
}
void emberAfLevelControlClusterServerInitCallback(EndpointId endpoint)
{
EmberAfLevelControlState * state = getState(endpoint);
if (state == nullptr)
{
ChipLogProgress(Zcl, "ERR: Level control cluster not available on ep%d", endpoint);
return;
}
state->minLevel = MATTER_DM_PLUGIN_LEVEL_CONTROL_MINIMUM_LEVEL;
state->maxLevel = MATTER_DM_PLUGIN_LEVEL_CONTROL_MAXIMUM_LEVEL;
// If these read only attributes are enabled we use those values as our set minLevel and maxLevel
// if get isn't possible, value stays at default
Attributes::MinLevel::Get(endpoint, &state->minLevel);
Attributes::MaxLevel::Get(endpoint, &state->maxLevel);
if (LevelControlHasFeature(endpoint, Feature::kLighting))
{
if (state->minLevel < LEVEL_CONTROL_LIGHTING_MIN_LEVEL)
{
state->minLevel = LEVEL_CONTROL_LIGHTING_MIN_LEVEL;
}
if (state->maxLevel > LEVEL_CONTROL_LIGHTING_MAX_LEVEL)
{
state->maxLevel = LEVEL_CONTROL_LIGHTING_MAX_LEVEL;
}
}
DataModel::Nullable<uint8_t> currentLevel;
Status status = Attributes::CurrentLevel::Get(endpoint, currentLevel);
if (status == Status::Success)
{
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL
// StartUp behavior relies StartUpCurrentLevel attributes being Non Volatile.
if (areStartUpLevelControlServerAttributesNonVolatile(endpoint))
{
// 1.5.14. StartUpCurrentLevel Attribute
// The StartUpCurrentLevel attribute SHALL define the desired startup level
// for a device when it is supplied with power and this level SHALL be
// reflected in the CurrentLevel attribute. The values of the StartUpCurrentLevel
// attribute are listed below:
// Table 4. Values of the StartUpCurrentLevel attribute
// Value Action on power up
// 0x00 Set the CurrentLevel attribute to the minimum value permitted on the device.
// 0x01-0xfe Set the CurrentLevel attribute to this value.
// NULL Set the CurrentLevel attribute to its previous value.
// 0xFF Work Around ZAP Can't set default value to NULL
// https://github.com/project-chip/zap/issues/354
DataModel::Nullable<uint8_t> startUpCurrentLevel;
status = Attributes::StartUpCurrentLevel::Get(endpoint, startUpCurrentLevel);
if (status == Status::Success)
{
if (!startUpCurrentLevel.IsNull())
{
if (startUpCurrentLevel.Value() == STARTUP_CURRENT_LEVEL_USE_DEVICE_MINIMUM)
{
currentLevel.SetNonNull(state->minLevel);
}
else
{
// Otherwise set to specified value 0x01-0xFE.
// But, need to enforce currentLevel's min/max, right?
// Spec doesn't mention this.
if (startUpCurrentLevel.Value() < state->minLevel)
{
currentLevel.SetNonNull(state->minLevel);
}
else if (startUpCurrentLevel.Value() > state->maxLevel)
{
currentLevel.SetNonNull(state->maxLevel);
}
else
{
currentLevel.SetNonNull(startUpCurrentLevel.Value());
}
}
}
// Otherwise Set the CurrentLevel attribute to its previous value which was already fetch above
SetCurrentLevelQuietReport(endpoint, state, currentLevel, false /*isEndOfTransition*/);
}
}
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL
// In any case, we make sure that the respects min/max
if (currentLevel.IsNull() || currentLevel.Value() < state->minLevel)
{
SetCurrentLevelQuietReport(endpoint, state, state->minLevel, false /*isEndOfTransition*/);
}
else if (currentLevel.Value() > state->maxLevel)
{
SetCurrentLevelQuietReport(endpoint, state, state->maxLevel, false /*isEndOfTransition*/);
}
}
#if defined(MATTER_DM_PLUGIN_SCENES_MANAGEMENT) && CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS
// Registers Scene handlers for the level control cluster on the server
Clusters::ScenesManagement::ScenesServer::Instance().RegisterSceneHandler(endpoint, LevelControlServer::GetSceneHandler());
#endif // defined(MATTER_DM_PLUGIN_SCENES_MANAGEMENT) && CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS
emberAfPluginLevelControlClusterServerPostInitCallback(endpoint);
}
void MatterLevelControlClusterServerShutdownCallback(EndpointId endpoint)
{
ChipLogProgress(Zcl, "Shuting down level control server cluster on endpoint %d", endpoint);
cancelEndpointTimerCallback(endpoint);
}
#ifndef IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL
static bool areStartUpLevelControlServerAttributesNonVolatile(EndpointId endpoint)
{
return !emberAfIsKnownVolatileAttribute(endpoint, LevelControl::Id, Attributes::CurrentLevel::Id) &&
!emberAfIsKnownVolatileAttribute(endpoint, LevelControl::Id, Attributes::StartUpCurrentLevel::Id);
}
#endif // IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL
void emberAfPluginLevelControlClusterServerPostInitCallback(EndpointId endpoint) {}
bool LevelControlHasFeature(EndpointId endpoint, Feature feature)
{
bool success;
uint32_t featureMap;
success = (Attributes::FeatureMap::Get(endpoint, &featureMap) == Status::Success);
return success ? ((featureMap & to_underlying(feature)) != 0) : false;
}
void MatterLevelControlPluginServerInitCallback() {}