blob: 4743e4940985418acab6419fba21cf4e39872b5a [file]
/*
*
* Copyright (c) 2023-2025 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.
*/
#include "OperationalStateCluster.h"
#include <app/server-cluster/AttributeListBuilder.h>
#include <clusters/OperationalState/Metadata.h>
#include <clusters/OvenCavityOperationalState/Metadata.h>
#include <clusters/RvcOperationalState/Metadata.h>
#include <lib/support/logging/CHIPLogging.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::OperationalState;
using Status = Protocols::InteractionModel::Status;
namespace {
// operationalStateID is a uint8_t, so there can be at most 256 distinct states. This bound also
// guards the delegate-iteration loops against a buggy delegate that never returns CHIP_ERROR_NOT_FOUND.
constexpr size_t kMaxOperationalStateCount = 256;
} // namespace
// ---------------------------------------------------------------------------
// OperationalStateCluster — constructors
// ---------------------------------------------------------------------------
OperationalStateCluster::OperationalStateCluster(EndpointId endpointId, Delegate * delegate, const Config & config) :
OperationalStateCluster(endpointId, OperationalState::Id, OperationalState::kRevision, delegate, config)
{}
OperationalStateCluster::OperationalStateCluster(EndpointId endpointId, ClusterId clusterId, uint16_t revision, Delegate * delegate,
const Config & config) :
DefaultServerCluster({ endpointId, clusterId }),
mDelegate(delegate), mRevision(revision), mConfig(config)
{
mCountdownTime.policy()
.Set(QuieterReportingPolicyEnum::kMarkDirtyOnIncrement)
.Set(QuieterReportingPolicyEnum::kMarkDirtyOnChangeToFromZero);
}
// ---------------------------------------------------------------------------
// Attribute setters / getters
// ---------------------------------------------------------------------------
CHIP_ERROR OperationalStateCluster::SetCurrentPhase(const DataModel::Nullable<uint8_t> & aPhase)
{
if (!aPhase.IsNull() && !IsSupportedPhase(aPhase.Value()))
{
return CHIP_ERROR_INVALID_ARGUMENT;
}
bool changed = SetAttributeValue(mCurrentPhase, aPhase, OperationalState::Attributes::CurrentPhase::Id);
if (changed)
{
UpdateCountdownTimeFromClusterLogic();
}
return CHIP_NO_ERROR;
}
CHIP_ERROR OperationalStateCluster::SetOperationalState(uint8_t aOpState)
{
if (aOpState == to_underlying(OperationalStateEnum::kError) || !IsSupportedOperationalState(aOpState))
{
return CHIP_ERROR_INVALID_ARGUMENT;
}
bool countdownUpdateNeeded = false;
if (mOperationalError.errorStateID != to_underlying(ErrorStateEnum::kNoError))
{
mOperationalError.Set(to_underlying(ErrorStateEnum::kNoError));
countdownUpdateNeeded = true;
NotifyAttributeChanged(OperationalState::Attributes::OperationalError::Id);
}
bool stateChanged = SetAttributeValue(mOperationalState, aOpState, OperationalState::Attributes::OperationalState::Id);
if (stateChanged)
{
countdownUpdateNeeded = true;
}
if (countdownUpdateNeeded)
{
UpdateCountdownTimeFromClusterLogic();
}
return CHIP_NO_ERROR;
}
void OperationalStateCluster::GetCurrentOperationalError(GenericOperationalError & error) const
{
error.Set(mOperationalError.errorStateID, mOperationalError.errorStateLabel, mOperationalError.errorStateDetails);
}
void OperationalStateCluster::OnOperationalErrorDetected(const Structs::ErrorStateStruct::Type & aError)
{
if (mOperationalState != to_underlying(OperationalStateEnum::kError))
{
mOperationalState = to_underlying(OperationalStateEnum::kError);
NotifyAttributeChanged(OperationalState::Attributes::OperationalState::Id);
}
if (!mOperationalError.IsEqual(aError))
{
mOperationalError.Set(aError.errorStateID, aError.errorStateLabel, aError.errorStateDetails);
NotifyAttributeChanged(OperationalState::Attributes::OperationalError::Id);
}
UpdateCountdownTimeFromClusterLogic();
VerifyOrReturn(mContext != nullptr);
GenericErrorEvent event(mPath.mClusterId, aError);
mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
}
void OperationalStateCluster::OnOperationCompletionDetected(uint8_t aCompletionErrorCode,
const Optional<DataModel::Nullable<uint32_t>> & aTotalOperationalTime,
const Optional<DataModel::Nullable<uint32_t>> & aPausedTime)
{
VerifyOrReturn(mContext != nullptr);
GenericOperationCompletionEvent event(mPath.mClusterId, aCompletionErrorCode, aTotalOperationalTime, aPausedTime);
mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
UpdateCountdownTimeFromClusterLogic();
}
void OperationalStateCluster::ReportOperationalStateListChange()
{
NotifyAttributeChanged(OperationalState::Attributes::OperationalStateList::Id);
}
void OperationalStateCluster::ReportPhaseListChange()
{
NotifyAttributeChanged(OperationalState::Attributes::PhaseList::Id);
UpdateCountdownTimeFromClusterLogic();
}
void OperationalStateCluster::UpdateCountdownTime(bool fromDelegate)
{
DataModel::Nullable<uint32_t> newCountdownTime = mDelegate->GetCountdownTime();
auto now = System::SystemClock().GetMonotonicTimestamp();
bool markDirty = false;
if (fromDelegate)
{
auto predicate = [](const decltype(mCountdownTime)::SufficientChangePredicateCandidate & candidate) -> bool {
if (candidate.lastDirtyValue.IsNull() || candidate.newValue.IsNull())
{
return false;
}
uint32_t lastDirtyValue = candidate.lastDirtyValue.Value();
uint32_t newValue = candidate.newValue.Value();
constexpr uint32_t kDeltaToReport = 10;
return (newValue < lastDirtyValue) && ((lastDirtyValue - newValue) > kDeltaToReport);
};
markDirty = (mCountdownTime.SetValue(newCountdownTime, now, predicate) == AttributeDirtyState::kMustReport);
}
else
{
// When the update is coming from the cluster logic (e.g. due to a state change),
// we should report if the value has changed since the last report.
auto predicate = [](const decltype(mCountdownTime)::SufficientChangePredicateCandidate & candidate) -> bool {
return candidate.lastDirtyValue != candidate.newValue;
};
markDirty = (mCountdownTime.SetValue(newCountdownTime, now, predicate) == AttributeDirtyState::kMustReport);
}
if (markDirty)
{
NotifyAttributeChanged(OperationalState::Attributes::CountdownTime::Id);
}
}
bool OperationalStateCluster::IsSupportedPhase(uint8_t aPhase)
{
char buffer[kMaxPhaseNameLength];
MutableCharSpan phase(buffer);
return mDelegate->GetOperationalPhaseAtIndex(aPhase, phase) == CHIP_NO_ERROR;
}
bool OperationalStateCluster::IsSupportedOperationalState(uint8_t aState)
{
GenericOperationalState opState;
for (size_t i = 0; i < kMaxOperationalStateCount && mDelegate->GetOperationalStateAtIndex(i, opState) == CHIP_NO_ERROR; i++)
{
if (opState.operationalStateID == aState)
{
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------
// ReadAttribute
// ---------------------------------------------------------------------------
DataModel::ActionReturnStatus OperationalStateCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder)
{
using namespace OperationalState::Attributes;
switch (request.path.mAttributeId)
{
case ClusterRevision::Id:
return encoder.Encode(mRevision);
case FeatureMap::Id:
return encoder.Encode<uint32_t>(0);
case PhaseList::Id: {
char firstBuf[kMaxPhaseNameLength];
MutableCharSpan firstPhase(firstBuf);
if (mDelegate->GetOperationalPhaseAtIndex(0, firstPhase) == CHIP_ERROR_NOT_FOUND)
{
return encoder.EncodeNull();
}
return encoder.EncodeList([this](const auto & listEncoder) -> CHIP_ERROR {
constexpr uint8_t kMaxPhaseCount = 32; // spec §1.14.5.1: max 32 entries
for (uint8_t i = 0; i < kMaxPhaseCount; i++)
{
char buf[kMaxPhaseNameLength];
MutableCharSpan phase(buf);
CHIP_ERROR err = mDelegate->GetOperationalPhaseAtIndex(i, phase);
if (err == CHIP_ERROR_NOT_FOUND)
{
return CHIP_NO_ERROR;
}
ReturnErrorOnFailure(err);
ReturnErrorOnFailure(listEncoder.Encode(phase));
}
return CHIP_NO_ERROR;
});
}
case CurrentPhase::Id:
return encoder.Encode(mCurrentPhase);
case CountdownTime::Id:
return encoder.Encode(mDelegate->GetCountdownTime());
case OperationalStateList::Id:
return encoder.EncodeList([this](const auto & listEncoder) -> CHIP_ERROR {
GenericOperationalState opState;
for (size_t i = 0; i < kMaxOperationalStateCount; i++)
{
CHIP_ERROR err = mDelegate->GetOperationalStateAtIndex(i, opState);
if (err == CHIP_ERROR_NOT_FOUND)
{
return CHIP_NO_ERROR;
}
ReturnErrorOnFailure(err);
ReturnErrorOnFailure(listEncoder.Encode(opState));
}
return CHIP_NO_ERROR;
});
case Attributes::OperationalState::Id:
return encoder.Encode(mOperationalState);
case OperationalError::Id:
return encoder.Encode(mOperationalError);
default:
return Status::UnsupportedAttribute;
}
}
// ---------------------------------------------------------------------------
// Attributes
// ---------------------------------------------------------------------------
CHIP_ERROR OperationalStateCluster::Attributes(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
AttributeListBuilder listBuilder(builder);
static constexpr DataModel::AttributeEntry kOptional[] = {
OperationalState::Attributes::CountdownTime::kMetadataEntry,
};
return listBuilder.Append(Span(OperationalState::Attributes::kMandatoryMetadata), Span(kOptional), mConfig.optionalAttributes);
}
// ---------------------------------------------------------------------------
// AcceptedCommands — base OperationalState cluster
// ---------------------------------------------------------------------------
CHIP_ERROR OperationalStateCluster::AcceptedCommands(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
{
static constexpr DataModel::AcceptedCommandEntry kCommands[] = {
OperationalState::Commands::Pause::kMetadataEntry,
OperationalState::Commands::Stop::kMetadataEntry,
OperationalState::Commands::Start::kMetadataEntry,
OperationalState::Commands::Resume::kMetadataEntry,
};
return builder.ReferenceExisting(Span(kCommands));
}
CHIP_ERROR OperationalStateCluster::GeneratedCommands(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<CommandId> & builder)
{
// OperationalCommandResponse (0x04) is sent in response to every accepted command.
// RVC and OvenCavity derived clusters share the same command ID value.
static_assert(OperationalState::Commands::OperationalCommandResponse::Id ==
RvcOperationalState::Commands::OperationalCommandResponse::Id,
"Derived clusters must share the same command ID for OperationalCommandResponse");
static_assert(OperationalState::Commands::OperationalCommandResponse::Id ==
OvenCavityOperationalState::Commands::OperationalCommandResponse::Id,
"Derived clusters must share the same command ID for OperationalCommandResponse");
static constexpr CommandId kGeneratedCommands[] = { OperationalState::Commands::OperationalCommandResponse::Id };
return builder.ReferenceExisting(kGeneratedCommands);
}
// ---------------------------------------------------------------------------
// InvokeCommand
// ---------------------------------------------------------------------------
std::optional<DataModel::ActionReturnStatus> OperationalStateCluster::InvokeCommand(const DataModel::InvokeRequest & request,
chip::TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
switch (request.path.mCommandId)
{
case OperationalState::Commands::Pause::Id:
return HandlePauseOrResumeState(request.path, input_arguments, handler, PauseOrResume::kPause);
case OperationalState::Commands::Resume::Id:
return HandlePauseOrResumeState(request.path, input_arguments, handler, PauseOrResume::kResume);
case OperationalState::Commands::Stop::Id:
return HandleStartOrStopState(request.path, input_arguments, handler, StartOrStop::kStop);
case OperationalState::Commands::Start::Id:
return HandleStartOrStopState(request.path, input_arguments, handler, StartOrStop::kStart);
default:
return HandleDerivedClusterCommand(request.path, input_arguments, handler);
}
}
// ---------------------------------------------------------------------------
// Command handlers
// ---------------------------------------------------------------------------
namespace {
// Builds the standard OperationalCommandResponse carrying `err` and adds it to `handler`.
std::optional<DataModel::ActionReturnStatus> AddCommandResponse(const ConcreteCommandPath & path, CommandHandler * handler,
const GenericOperationalError & err)
{
OperationalState::Commands::OperationalCommandResponse::Type response;
response.commandResponseState = err;
handler->AddResponse(path, response);
return std::nullopt;
}
} // namespace
std::optional<DataModel::ActionReturnStatus> OperationalStateCluster::HandlePauseOrResumeState(const ConcreteCommandPath & path,
chip::TLV::TLVReader & input,
CommandHandler * handler,
PauseOrResume action)
{
// Pause/Resume are fieldless commands; just confirm there is no trailing payload.
if (input.VerifyEndOfContainer() != CHIP_NO_ERROR)
{
return Protocols::InteractionModel::Status::InvalidCommand;
}
const bool isPause = (action == PauseOrResume::kPause);
GenericOperationalError err(to_underlying(ErrorStateEnum::kNoError));
uint8_t opState = GetCurrentOperationalState();
uint8_t noOpState = isPause ? to_underlying(OperationalStateEnum::kPaused) : to_underlying(OperationalStateEnum::kRunning);
if (opState == to_underlying(OperationalStateEnum::kStopped) || opState == to_underlying(OperationalStateEnum::kError))
{
err.Set(to_underlying(ErrorStateEnum::kCommandInvalidInState));
}
else if (opState >= DerivedClusterNumberSpaceStart && opState < VendorNumberSpaceStart)
{
bool compat = isPause ? IsDerivedClusterStatePauseCompatible(opState) : IsDerivedClusterStateResumeCompatible(opState);
if (!compat)
{
err.Set(to_underlying(ErrorStateEnum::kCommandInvalidInState));
}
}
if (err.errorStateID == 0 && opState != noOpState)
{
if (isPause)
{
mDelegate->HandlePauseStateCallback(err);
}
else
{
mDelegate->HandleResumeStateCallback(err);
}
}
return AddCommandResponse(path, handler, err);
}
std::optional<DataModel::ActionReturnStatus> OperationalStateCluster::HandleStartOrStopState(const ConcreteCommandPath & path,
chip::TLV::TLVReader & input,
CommandHandler * handler,
StartOrStop action)
{
// Start/Stop are fieldless commands; just confirm there is no trailing payload.
if (input.VerifyEndOfContainer() != CHIP_NO_ERROR)
{
return Protocols::InteractionModel::Status::InvalidCommand;
}
const bool isStart = (action == StartOrStop::kStart);
GenericOperationalError err(to_underlying(ErrorStateEnum::kNoError));
uint8_t noOpState = isStart ? to_underlying(OperationalStateEnum::kRunning) : to_underlying(OperationalStateEnum::kStopped);
if (GetCurrentOperationalState() != noOpState)
{
if (isStart)
{
mDelegate->HandleStartStateCallback(err);
}
else
{
mDelegate->HandleStopStateCallback(err);
}
}
return AddCommandResponse(path, handler, err);
}