| /* |
| * |
| * Copyright (c) 2023 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. |
| */ |
| |
| /****************************************************************************' |
| * @file |
| * @brief Implementation for the Operational State Server Cluster |
| ***************************************************************************/ |
| #include "operational-state-server.h" |
| |
| #include <app-common/zap-generated/attributes/Accessors.h> |
| #include <app-common/zap-generated/callback.h> |
| #include <app/AttributeAccessInterfaceRegistry.h> |
| #include <app/CommandHandlerInterfaceRegistry.h> |
| #include <app/EventLogging.h> |
| #include <app/InteractionModelEngine.h> |
| #include <app/reporting/reporting.h> |
| #include <app/util/attribute-storage.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 namespace chip::app::Clusters::OperationalState::Attributes; |
| |
| using Status = Protocols::InteractionModel::Status; |
| |
| Instance::Instance(Delegate * aDelegate, EndpointId aEndpointId, ClusterId aClusterId) : |
| CommandHandlerInterface(MakeOptional(aEndpointId), aClusterId), AttributeAccessInterface(MakeOptional(aEndpointId), aClusterId), |
| mDelegate(aDelegate), mEndpointId(aEndpointId), mClusterId(aClusterId) |
| { |
| mDelegate->SetInstance(this); |
| mCountdownTime.policy() |
| .Set(QuieterReportingPolicyEnum::kMarkDirtyOnIncrement) |
| .Set(QuieterReportingPolicyEnum::kMarkDirtyOnChangeToFromZero); |
| } |
| |
| Instance::Instance(Delegate * aDelegate, EndpointId aEndpointId) : Instance(aDelegate, aEndpointId, OperationalState::Id) {} |
| |
| Instance::~Instance() |
| { |
| CommandHandlerInterfaceRegistry::Instance().UnregisterCommandHandler(this); |
| AttributeAccessInterfaceRegistry::Instance().Unregister(this); |
| } |
| |
| CHIP_ERROR Instance::Init() |
| { |
| // Check if the cluster has been selected in zap |
| if (!emberAfContainsServer(mEndpointId, mClusterId)) |
| { |
| ChipLogError(Zcl, "Operational State: The cluster with ID %lu was not enabled in zap.", long(mClusterId)); |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| |
| ReturnErrorOnFailure(CommandHandlerInterfaceRegistry::Instance().RegisterCommandHandler(this)); |
| |
| VerifyOrReturnError(AttributeAccessInterfaceRegistry::Instance().Register(this), CHIP_ERROR_INCORRECT_STATE); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR Instance::SetCurrentPhase(const DataModel::Nullable<uint8_t> & aPhase) |
| { |
| if (!aPhase.IsNull()) |
| { |
| if (!IsSupportedPhase(aPhase.Value())) |
| { |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| } |
| |
| DataModel::Nullable<uint8_t> oldPhase = mCurrentPhase; |
| mCurrentPhase = aPhase; |
| if (mCurrentPhase != oldPhase) |
| { |
| MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::CurrentPhase::Id); |
| UpdateCountdownTimeFromClusterLogic(); |
| } |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR Instance::SetOperationalState(uint8_t aOpState) |
| { |
| // Error is only allowed to be set by OnOperationalErrorDetected. |
| if (aOpState == to_underlying(OperationalStateEnum::kError) || !IsSupportedOperationalState(aOpState)) |
| { |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| |
| bool countdownTimeUpdateNeeded = false; |
| if (mOperationalError.errorStateID != to_underlying(ErrorStateEnum::kNoError)) |
| { |
| mOperationalError.Set(to_underlying(ErrorStateEnum::kNoError)); |
| countdownTimeUpdateNeeded = true; |
| MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::OperationalError::Id); |
| } |
| |
| uint8_t oldState = mOperationalState; |
| mOperationalState = aOpState; |
| if (mOperationalState != oldState) |
| { |
| MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::OperationalState::Id); |
| countdownTimeUpdateNeeded = true; |
| } |
| |
| if (countdownTimeUpdateNeeded) |
| { |
| UpdateCountdownTimeFromClusterLogic(); |
| } |
| return CHIP_NO_ERROR; |
| } |
| |
| DataModel::Nullable<uint8_t> Instance::GetCurrentPhase() const |
| { |
| return mCurrentPhase; |
| } |
| |
| uint8_t Instance::GetCurrentOperationalState() const |
| { |
| return mOperationalState; |
| } |
| |
| void Instance::GetCurrentOperationalError(GenericOperationalError & error) const |
| { |
| error.Set(mOperationalError.errorStateID, mOperationalError.errorStateLabel, mOperationalError.errorStateDetails); |
| } |
| |
| void Instance::OnOperationalErrorDetected(const Structs::ErrorStateStruct::Type & aError) |
| { |
| ChipLogDetail(Zcl, "OperationalStateServer: OnOperationalErrorDetected"); |
| // Set the OperationalState attribute to Error |
| if (mOperationalState != to_underlying(OperationalStateEnum::kError)) |
| { |
| mOperationalState = to_underlying(OperationalStateEnum::kError); |
| MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::OperationalState::Id); |
| } |
| |
| // Set the OperationalError attribute |
| if (!mOperationalError.IsEqual(aError)) |
| { |
| mOperationalError.Set(aError.errorStateID, aError.errorStateLabel, aError.errorStateDetails); |
| MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::OperationalError::Id); |
| } |
| |
| UpdateCountdownTimeFromClusterLogic(); |
| |
| // Generate an ErrorDetected event |
| GenericErrorEvent event(mClusterId, aError); |
| EventNumber eventNumber; |
| CHIP_ERROR error = LogEvent(event, mEndpointId, eventNumber); |
| |
| if (error != CHIP_NO_ERROR) |
| { |
| ChipLogError(Zcl, "OperationalStateServer: Failed to record OperationalError event: %" CHIP_ERROR_FORMAT, error.Format()); |
| } |
| } |
| |
| void Instance::OnOperationCompletionDetected(uint8_t aCompletionErrorCode, |
| const Optional<DataModel::Nullable<uint32_t>> & aTotalOperationalTime, |
| const Optional<DataModel::Nullable<uint32_t>> & aPausedTime) |
| { |
| ChipLogDetail(Zcl, "OperationalStateServer: OnOperationCompletionDetected"); |
| |
| GenericOperationCompletionEvent event(mClusterId, aCompletionErrorCode, aTotalOperationalTime, aPausedTime); |
| EventNumber eventNumber; |
| CHIP_ERROR error = LogEvent(event, mEndpointId, eventNumber); |
| |
| if (error != CHIP_NO_ERROR) |
| { |
| ChipLogError(Zcl, "OperationalStateServer: Failed to record OperationCompletion event: %" CHIP_ERROR_FORMAT, |
| error.Format()); |
| } |
| |
| UpdateCountdownTimeFromClusterLogic(); |
| } |
| |
| void Instance::ReportOperationalStateListChange() |
| { |
| MatterReportingAttributeChangeCallback(ConcreteAttributePath(mEndpointId, mClusterId, Attributes::OperationalStateList::Id)); |
| } |
| |
| void Instance::ReportPhaseListChange() |
| { |
| MatterReportingAttributeChangeCallback(ConcreteAttributePath(mEndpointId, mClusterId, Attributes::PhaseList::Id)); |
| UpdateCountdownTimeFromClusterLogic(); |
| } |
| |
| void Instance::UpdateCountdownTime(bool fromDelegate) |
| { |
| app::DataModel::Nullable<uint32_t> newCountdownTime = mDelegate->GetCountdownTime(); |
| auto now = System::SystemClock().GetMonotonicTimestamp(); |
| |
| bool markDirty = false; |
| |
| if (fromDelegate) |
| { |
| // Updates from delegate are reduce-reported to every 10s max (choice of this implementation), in addition |
| // to default change-from-null, change-from-zero and increment policy. |
| 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(); |
| uint32_t kNumSecondsDeltaToReport = 10; |
| return (newValue < lastDirtyValue) && ((lastDirtyValue - newValue) > kNumSecondsDeltaToReport); |
| }; |
| markDirty = (mCountdownTime.SetValue(newCountdownTime, now, predicate) == AttributeDirtyState::kMustReport); |
| } |
| else |
| { |
| auto predicate = [](const decltype(mCountdownTime)::SufficientChangePredicateCandidate &) -> bool { return true; }; |
| markDirty = (mCountdownTime.SetValue(newCountdownTime, now, predicate) == AttributeDirtyState::kMustReport); |
| } |
| |
| if (markDirty) |
| { |
| MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::CountdownTime::Id); |
| } |
| } |
| |
| bool Instance::IsSupportedPhase(uint8_t aPhase) |
| { |
| char buffer[kMaxPhaseNameLength]; |
| MutableCharSpan phase(buffer); |
| if (mDelegate->GetOperationalPhaseAtIndex(aPhase, phase) != CHIP_ERROR_NOT_FOUND) |
| { |
| return true; |
| } |
| return false; |
| } |
| |
| bool Instance::IsSupportedOperationalState(uint8_t aState) |
| { |
| GenericOperationalState opState; |
| for (uint8_t i = 0; mDelegate->GetOperationalStateAtIndex(i, opState) != CHIP_ERROR_NOT_FOUND; i++) |
| { |
| if (opState.operationalStateID == aState) |
| { |
| return true; |
| } |
| } |
| ChipLogDetail(Zcl, "Cannot find an operational state with value %u", aState); |
| return false; |
| } |
| |
| // private |
| |
| template <typename RequestT, typename FuncT> |
| void Instance::HandleCommand(HandlerContext & handlerContext, FuncT func) |
| { |
| if (!handlerContext.mCommandHandled && (handlerContext.mRequestPath.mCommandId == RequestT::GetCommandId())) |
| { |
| RequestT requestPayload; |
| |
| // If the command matches what the caller is looking for, let's mark this as being handled |
| // even if errors happen after this. This ensures that we don't execute any fall-back strategies |
| // to handle this command since at this point, the caller is taking responsibility for handling |
| // the command in its entirety, warts and all. |
| // |
| handlerContext.SetCommandHandled(); |
| |
| if (DataModel::Decode(handlerContext.mPayload, requestPayload) != CHIP_NO_ERROR) |
| { |
| handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, |
| Protocols::InteractionModel::Status::InvalidCommand); |
| return; |
| } |
| |
| func(handlerContext, requestPayload); |
| } |
| } |
| |
| // This function is called by the interaction model engine when a command destined for this instance is received. |
| void Instance::InvokeCommand(HandlerContext & handlerContext) |
| { |
| ChipLogDetail(Zcl, "OperationalState: InvokeCommand"); |
| switch (handlerContext.mRequestPath.mCommandId) |
| { |
| case Commands::Pause::Id: |
| ChipLogDetail(Zcl, "OperationalState: Entering handling Pause state"); |
| |
| HandleCommand<Commands::Pause::DecodableType>( |
| handlerContext, [this](HandlerContext & ctx, const auto & req) { HandlePauseState(ctx, req); }); |
| break; |
| |
| case Commands::Resume::Id: |
| ChipLogDetail(Zcl, "OperationalState: Entering handling Resume state"); |
| |
| HandleCommand<Commands::Resume::DecodableType>( |
| handlerContext, [this](HandlerContext & ctx, const auto & req) { HandleResumeState(ctx, req); }); |
| break; |
| |
| case Commands::Start::Id: |
| ChipLogDetail(Zcl, "OperationalState: Entering handling Start state"); |
| |
| HandleCommand<Commands::Start::DecodableType>( |
| handlerContext, [this](HandlerContext & ctx, const auto & req) { HandleStartState(ctx, req); }); |
| break; |
| |
| case Commands::Stop::Id: |
| ChipLogDetail(Zcl, "OperationalState: Entering handling Stop state"); |
| |
| HandleCommand<Commands::Stop::DecodableType>(handlerContext, |
| [this](HandlerContext & ctx, const auto & req) { HandleStopState(ctx, req); }); |
| break; |
| default: |
| ChipLogDetail(Zcl, "OperationalState: Entering handling derived cluster commands"); |
| |
| InvokeDerivedClusterCommand(handlerContext); |
| break; |
| } |
| } |
| |
| CHIP_ERROR Instance::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) |
| { |
| ChipLogError(Zcl, "OperationalState: Reading"); |
| switch (aPath.mAttributeId) |
| { |
| case OperationalState::Attributes::OperationalStateList::Id: { |
| return aEncoder.EncodeList([delegate = mDelegate](const auto & encoder) -> CHIP_ERROR { |
| GenericOperationalState opState; |
| size_t index = 0; |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| while ((err = delegate->GetOperationalStateAtIndex(index, opState)) == CHIP_NO_ERROR) |
| { |
| ReturnErrorOnFailure(encoder.Encode(opState)); |
| index++; |
| } |
| if (err == CHIP_ERROR_NOT_FOUND) |
| { |
| return CHIP_NO_ERROR; |
| } |
| return err; |
| }); |
| break; |
| } |
| |
| case OperationalState::Attributes::OperationalState::Id: { |
| ReturnErrorOnFailure(aEncoder.Encode(GetCurrentOperationalState())); |
| break; |
| } |
| |
| case OperationalState::Attributes::OperationalError::Id: { |
| ReturnErrorOnFailure(aEncoder.Encode(mOperationalError)); |
| break; |
| } |
| |
| case OperationalState::Attributes::PhaseList::Id: { |
| |
| char buffer[kMaxPhaseNameLength]; |
| MutableCharSpan phase(buffer); |
| size_t index = 0; |
| |
| if (mDelegate->GetOperationalPhaseAtIndex(index, phase) == CHIP_ERROR_NOT_FOUND) |
| { |
| return aEncoder.EncodeNull(); |
| } |
| |
| return aEncoder.EncodeList([delegate = mDelegate](const auto & encoder) -> CHIP_ERROR { |
| for (uint8_t i = 0; true; i++) |
| { |
| char buffer2[kMaxPhaseNameLength]; |
| MutableCharSpan phase2(buffer2); |
| auto err = delegate->GetOperationalPhaseAtIndex(i, phase2); |
| if (err == CHIP_ERROR_NOT_FOUND) |
| { |
| return CHIP_NO_ERROR; |
| } |
| ReturnErrorOnFailure(err); |
| ReturnErrorOnFailure(encoder.Encode(phase2)); |
| } |
| }); |
| break; |
| } |
| |
| case OperationalState::Attributes::CurrentPhase::Id: { |
| ReturnErrorOnFailure(aEncoder.Encode(GetCurrentPhase())); |
| break; |
| } |
| |
| case OperationalState::Attributes::CountdownTime::Id: { |
| // Read through to get value closest to reality. |
| ReturnErrorOnFailure(aEncoder.Encode(mDelegate->GetCountdownTime())); |
| break; |
| } |
| } |
| return CHIP_NO_ERROR; |
| } |
| |
| void Instance::HandlePauseState(HandlerContext & ctx, const Commands::Pause::DecodableType & req) |
| { |
| ChipLogDetail(Zcl, "OperationalState: HandlePauseState"); |
| |
| GenericOperationalError err(to_underlying(ErrorStateEnum::kNoError)); |
| uint8_t opState = GetCurrentOperationalState(); |
| |
| // Handle Operational State Pause-incompatible states. |
| if (opState == to_underlying(OperationalStateEnum::kStopped) || opState == to_underlying(OperationalStateEnum::kError)) |
| { |
| err.Set(to_underlying(ErrorStateEnum::kCommandInvalidInState)); |
| } |
| |
| // Handle Pause-incompatible states for derived clusters. |
| if (opState >= DerivedClusterNumberSpaceStart && opState < VendorNumberSpaceStart) |
| { |
| if (!IsDerivedClusterStatePauseCompatible(opState)) |
| { |
| err.Set(to_underlying(ErrorStateEnum::kCommandInvalidInState)); |
| } |
| } |
| |
| // If the error is still NoError, we can call the delegate's handle function. |
| // If the current state is Paused we can skip this call. |
| if (err.errorStateID == 0 && opState != to_underlying(OperationalStateEnum::kPaused)) |
| { |
| mDelegate->HandlePauseStateCallback(err); |
| } |
| |
| Commands::OperationalCommandResponse::Type response; |
| response.commandResponseState = err; |
| |
| ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); |
| } |
| |
| void Instance::HandleStopState(HandlerContext & ctx, const Commands::Stop::DecodableType & req) |
| { |
| ChipLogDetail(Zcl, "OperationalState: HandleStopState"); |
| |
| GenericOperationalError err(to_underlying(ErrorStateEnum::kNoError)); |
| uint8_t opState = GetCurrentOperationalState(); |
| |
| if (opState != to_underlying(OperationalStateEnum::kStopped)) |
| { |
| mDelegate->HandleStopStateCallback(err); |
| } |
| |
| Commands::OperationalCommandResponse::Type response; |
| response.commandResponseState = err; |
| |
| ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); |
| } |
| |
| void Instance::HandleStartState(HandlerContext & ctx, const Commands::Start::DecodableType & req) |
| { |
| ChipLogDetail(Zcl, "OperationalState: HandleStartState"); |
| |
| GenericOperationalError err(to_underlying(ErrorStateEnum::kNoError)); |
| uint8_t opState = GetCurrentOperationalState(); |
| |
| if (opState != to_underlying(OperationalStateEnum::kRunning)) |
| { |
| mDelegate->HandleStartStateCallback(err); |
| } |
| |
| Commands::OperationalCommandResponse::Type response; |
| response.commandResponseState = err; |
| |
| ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); |
| } |
| |
| void Instance::HandleResumeState(HandlerContext & ctx, const Commands::Resume::DecodableType & req) |
| { |
| ChipLogDetail(Zcl, "OperationalState: HandleResumeState"); |
| |
| GenericOperationalError err(to_underlying(ErrorStateEnum::kNoError)); |
| uint8_t opState = GetCurrentOperationalState(); |
| |
| // Handle Operational State Resume-incompatible states. |
| if (opState == to_underlying(OperationalStateEnum::kStopped) || opState == to_underlying(OperationalStateEnum::kError)) |
| { |
| err.Set(to_underlying(ErrorStateEnum::kCommandInvalidInState)); |
| } |
| |
| // Handle Resume-incompatible states for derived clusters. |
| if (opState >= DerivedClusterNumberSpaceStart && opState < VendorNumberSpaceStart) |
| { |
| if (!IsDerivedClusterStateResumeCompatible(opState)) |
| { |
| err.Set(to_underlying(ErrorStateEnum::kCommandInvalidInState)); |
| } |
| } |
| |
| // If the error is still NoError, we can call the delegate's handle function. |
| // If the current state is Running we can skip this call. |
| if (err.errorStateID == 0 && opState != to_underlying(OperationalStateEnum::kRunning)) |
| { |
| mDelegate->HandleResumeStateCallback(err); |
| } |
| |
| Commands::OperationalCommandResponse::Type response; |
| response.commandResponseState = err; |
| |
| ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); |
| } |
| |
| // RvcOperationalState |
| |
| bool RvcOperationalState::Instance::IsDerivedClusterStatePauseCompatible(uint8_t aState) |
| { |
| return aState == to_underlying(RvcOperationalState::OperationalStateEnum::kSeekingCharger); |
| } |
| |
| bool RvcOperationalState::Instance::IsDerivedClusterStateResumeCompatible(uint8_t aState) |
| { |
| return (aState == to_underlying(RvcOperationalState::OperationalStateEnum::kCharging) || |
| aState == to_underlying(RvcOperationalState::OperationalStateEnum::kDocked)); |
| } |
| |
| // This function is called by the base operational state cluster when a command in the derived cluster number-space is received. |
| void RvcOperationalState::Instance::InvokeDerivedClusterCommand(chip::app::CommandHandlerInterface::HandlerContext & handlerContext) |
| { |
| ChipLogDetail(Zcl, "RvcOperationalState: InvokeDerivedClusterCommand"); |
| switch (handlerContext.mRequestPath.mCommandId) |
| { |
| case RvcOperationalState::Commands::GoHome::Id: |
| ChipLogDetail(Zcl, "RvcOperationalState: Entering handling GoHome command"); |
| |
| CommandHandlerInterface::HandleCommand<Commands::GoHome::DecodableType>( |
| handlerContext, [this](HandlerContext & ctx, const auto & req) { HandleGoHomeCommand(ctx, req); }); |
| break; |
| } |
| } |
| |
| void RvcOperationalState::Instance::HandleGoHomeCommand(HandlerContext & ctx, const Commands::GoHome::DecodableType & req) |
| { |
| ChipLogDetail(Zcl, "RvcOperationalState: HandleGoHomeCommand"); |
| |
| GenericOperationalError err(to_underlying(OperationalState::ErrorStateEnum::kNoError)); |
| uint8_t opState = GetCurrentOperationalState(); |
| |
| // Handle the case of the device being in an invalid state |
| if (opState == to_underlying(OperationalStateEnum::kCharging) || opState == to_underlying(OperationalStateEnum::kDocked)) |
| { |
| err.Set(to_underlying(OperationalState::ErrorStateEnum::kCommandInvalidInState)); |
| } |
| |
| if (err.errorStateID == 0 && opState != to_underlying(OperationalStateEnum::kSeekingCharger)) |
| { |
| mDelegate->HandleGoHomeCommandCallback(err); |
| } |
| |
| Commands::OperationalCommandResponse::Type response; |
| response.commandResponseState = err; |
| |
| ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); |
| } |