blob: f97af28aa8a7eb2d451c601258eefd3b75bfc7cc [file]
/*
*
* Copyright (c) 2023-2026 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <app/clusters/mode-base-server/ModeBaseCluster.h>
#include <app/data-model/Nullable.h>
#include <app/persistence/AttributePersistence.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <app/server-cluster/DefaultServerCluster.h>
#include <platform/PlatformManager.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::ModeBase;
using namespace chip::app::Clusters::ModeBase::Attributes;
using chip::Protocols::InteractionModel::Status;
using BootReasonType = GeneralDiagnostics::BootReasonEnum;
using ModeOptionStructType = chip::app::Clusters::detail::Structs::ModeOptionStruct::Type;
using ModeTagStructType = chip::app::Clusters::detail::Structs::ModeTagStruct::Type;
namespace chip::app::Clusters {
namespace {
// Cluster constants, from the spec.
constexpr uint8_t kMaxModeLabelSize = 64;
constexpr uint8_t kMaxNumOfModeTags = 8;
} // namespace
ModeBaseCluster::ModeBaseCluster(EndpointId endpointId, ClusterId aClusterId, const Config & config) :
DefaultServerCluster({ endpointId, aClusterId }), mFeature(config.feature), mOptionalAttributeSet(config.optionalAttributeSet),
mAppDelegate(config.appDelegate), mOnOffValueForStartUp(config.onOffValueForStartUp),
mDiagnosticDataProvider(config.diagnosticDataProvider), mClusterRevision(config.clusterRevision)
{}
CHIP_ERROR ModeBaseCluster::Startup(ServerClusterContext & context)
{
ReturnErrorOnFailure(DefaultServerCluster::Startup(context));
// Initialise the current mode with the value of the first mode. This ensures that it is representing a valid mode.
ReturnErrorOnFailure(mAppDelegate.GetModeValueByIndex(0, mCurrentMode));
LoadPersistentAttributes();
// If the StartUpMode is set, the CurrentMode attribute SHALL be set to the StartUpMode value, when the server is powered up.
if (!mStartUpMode.IsNull())
{
// This behavior does not apply to reboots associated with OTA.
// After an OTA restart, the CurrentMode attribute SHALL return to its value prior to the restart.
// todo this only works for matter OTAs. According to the spec, this should also work for general OTAs.
BootReasonType bootReason = BootReasonType::kUnspecified;
CHIP_ERROR err = mDiagnosticDataProvider.GetBootReason(bootReason);
if (err != CHIP_NO_ERROR)
{
ChipLogError(
Zcl, "Unable to retrieve boot reason: %" CHIP_ERROR_FORMAT ". Assuming that we did not reboot because of an OTA",
err.Format());
bootReason = BootReasonType::kUnspecified;
}
if (bootReason == BootReasonType::kSoftwareUpdateCompleted)
{
ChipLogDetail(Zcl, "ModeBase: StartUpMode is ignored for OTA reboot.");
}
else
{
// Set CurrentMode to StartUpMode
if (mStartUpMode.Value() != mCurrentMode)
{
ChipLogProgress(Zcl, "ModeBase: Changing CurrentMode to the StartUpMode value.");
Status status = UpdateCurrentMode(mStartUpMode.Value());
if (status != Status::Success)
{
ChipLogError(Zcl, "ModeBase: Failed to change the CurrentMode to the StartUpMode value: %u",
to_underlying(status));
return StatusIB(status).ToChipError();
}
ChipLogProgress(Zcl, "ModeBase: Successfully initialized CurrentMode to the StartUpMode value %u",
mStartUpMode.Value());
}
}
}
if (mOnOffValueForStartUp && !mOnMode.IsNull())
{
// Set CurrentMode to OnMode
if (mOnMode.Value() != mCurrentMode)
{
ChipLogProgress(Zcl, "ModeBase: Changing CurrentMode to the OnMode value.");
Status status = UpdateCurrentMode(mOnMode.Value());
if (status != Status::Success)
{
ChipLogError(Zcl, "ModeBase: Failed to change the CurrentMode to the OnMode value: %u", to_underlying(status));
return StatusIB(status).ToChipError();
}
ChipLogProgress(Zcl, "ModeBase: Successfully initialized CurrentMode to the OnMode value %u", mOnMode.Value());
}
}
return CHIP_NO_ERROR;
}
Status ModeBaseCluster::UpdateCurrentMode(uint8_t aNewMode)
{
VerifyOrReturnValue(IsSupportedMode(aNewMode), Status::ConstraintError);
VerifyOrReturnValue(SetAttributeValue(mCurrentMode, aNewMode, CurrentMode::Id), Status::Success);
if (mContext != nullptr)
{
AttributePersistence attrPersistence{ mContext->attributeStorage };
LogErrorOnFailure(
attrPersistence.StoreNativeEndianValue({ mPath.mEndpointId, mPath.mClusterId, CurrentMode::Id }, mCurrentMode));
}
return Status::Success;
}
Status ModeBaseCluster::UpdateStartUpMode(DataModel::Nullable<uint8_t> aNewStartUpMode)
{
VerifyOrReturnValue(aNewStartUpMode.IsNull() || IsSupportedMode(aNewStartUpMode.Value()), Status::ConstraintError);
VerifyOrReturnValue(SetAttributeValue(mStartUpMode, aNewStartUpMode, StartUpMode::Id), Status::Success);
if (mContext != nullptr)
{
AttributePersistence attrPersistence{ mContext->attributeStorage };
LogErrorOnFailure(
attrPersistence.StoreNativeEndianValue({ mPath.mEndpointId, mPath.mClusterId, StartUpMode::Id }, mStartUpMode));
}
return Status::Success;
}
Status ModeBaseCluster::UpdateOnMode(DataModel::Nullable<uint8_t> aNewOnMode)
{
VerifyOrReturnValue(aNewOnMode.IsNull() || IsSupportedMode(aNewOnMode.Value()), Status::ConstraintError);
VerifyOrReturnValue(SetAttributeValue(mOnMode, aNewOnMode, OnMode::Id), Status::Success);
if (mContext != nullptr)
{
AttributePersistence attrPersistence{ mContext->attributeStorage };
LogErrorOnFailure(attrPersistence.StoreNativeEndianValue({ mPath.mEndpointId, mPath.mClusterId, OnMode::Id }, mOnMode));
}
return Status::Success;
}
void ModeBaseCluster::ReportSupportedModesChange()
{
NotifyAttributeChanged(SupportedModes::Id);
}
bool ModeBaseCluster::IsSupportedMode(uint8_t modeValue)
{
uint8_t value;
CHIP_ERROR err;
for (uint8_t i = 0; (err = mAppDelegate.GetModeValueByIndex(i, value)) != CHIP_ERROR_PROVIDER_LIST_EXHAUSTED; i++)
{
if (err != CHIP_NO_ERROR)
{
ChipLogError(Zcl, "ModeBase: Failed to get the mode value by index %u: %" CHIP_ERROR_FORMAT, i, err.Format());
return false;
}
if (value == modeValue)
{
return true;
}
}
ChipLogDetail(Zcl, "ModeBase: Cannot find a mode with value %u", modeValue);
return false;
}
CHIP_ERROR ModeBaseCluster::GetModeValueByModeTag(uint16_t modeTagValue, uint8_t & value)
{
ModeTagStructType tagsBuffer[kMaxNumOfModeTags];
DataModel::List<ModeTagStructType> mTags(tagsBuffer);
CHIP_ERROR err;
for (uint8_t i = 0; (err = mAppDelegate.GetModeTagsByIndex(i, mTags)) != CHIP_ERROR_PROVIDER_LIST_EXHAUSTED; i++)
{
ReturnErrorOnFailure(err);
for (size_t ii = 0; ii < mTags.size(); ii++)
{
if (mTags[ii].value == modeTagValue)
{
return mAppDelegate.GetModeValueByIndex(i, value);
}
}
mTags = tagsBuffer;
}
ChipLogDetail(Zcl, "ModeBase: Cannot find a mode with mode tag %x", modeTagValue);
return CHIP_ERROR_PROVIDER_LIST_EXHAUSTED;
}
std::optional<DataModel::ActionReturnStatus>
ModeBaseCluster::InvokeCommand(const DataModel::InvokeRequest & request, TLV::TLVReader & input_arguments, CommandHandler * handler)
{
switch (request.path.mCommandId)
{
case Commands::ChangeToMode::Id: {
Commands::ChangeToMode::DecodableType data;
ReturnErrorOnFailure(data.Decode(input_arguments));
return HandleChangeToMode(*handler, request.path, data);
}
default:
return Status::UnsupportedCommand;
}
}
DataModel::ActionReturnStatus ModeBaseCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder)
{
switch (request.path.mAttributeId)
{
case ClusterRevision::Id:
return encoder.Encode(mClusterRevision);
case SupportedModes::Id:
return encoder.EncodeList([this](const auto & encod) -> CHIP_ERROR { return EncodeSupportedModes(encod); });
case CurrentMode::Id:
return encoder.Encode(mCurrentMode);
case StartUpMode::Id:
return encoder.Encode(mStartUpMode);
case OnMode::Id:
return encoder.Encode(mOnMode);
case FeatureMap::Id:
return encoder.Encode(mFeature);
default:
return Status::UnsupportedAttribute;
}
}
DataModel::ActionReturnStatus ModeBaseCluster::WriteAttribute(const DataModel::WriteAttributeRequest & request,
AttributeValueDecoder & decoder)
{
switch (request.path.mAttributeId)
{
case StartUpMode::Id: {
DataModel::Nullable<uint8_t> newValue;
ReturnErrorOnFailure(decoder.Decode(newValue));
return UpdateStartUpMode(newValue);
}
case OnMode::Id: {
DataModel::Nullable<uint8_t> newValue;
ReturnErrorOnFailure(decoder.Decode(newValue));
return UpdateOnMode(newValue);
}
default:
return Status::UnsupportedAttribute;
}
}
CHIP_ERROR ModeBaseCluster::Attributes(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
AttributeListBuilder listBuilder(builder);
const AttributeListBuilder::OptionalAttributeEntry optionalAttributes[] = {
{ mOptionalAttributeSet.IsSet(StartUpMode::Id), StartUpMode::kMetadataEntry },
{ mFeature.Has(Feature::kOnOff), OnMode::kMetadataEntry },
};
return listBuilder.Append(Span(kMandatoryMetadata), Span(optionalAttributes));
}
CHIP_ERROR ModeBaseCluster::AcceptedCommands(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
{
// MicrowaveOvenMode is a special case. It does not support the ChangeToMode command.
if (mPath.mClusterId != MicrowaveOvenMode::Id)
{
return builder.AppendElements({ Commands::ChangeToMode::kMetadataEntry });
}
return CHIP_NO_ERROR;
}
CHIP_ERROR ModeBaseCluster::GeneratedCommands(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<CommandId> & builder)
{
// MicrowaveOvenMode is a special case. It does not support the ChangeToModeResponse command.
if (mPath.mClusterId != MicrowaveOvenMode::Id)
{
return builder.AppendElements({ Commands::ChangeToModeResponse::Id });
}
return CHIP_NO_ERROR;
}
std::optional<DataModel::ActionReturnStatus>
ModeBaseCluster::HandleChangeToMode(CommandHandler & commandObj, const ConcreteCommandPath & commandPath,
const Commands::ChangeToMode::DecodableType & commandData)
{
uint8_t newMode = commandData.newMode;
Commands::ChangeToModeResponse::Type response;
// If the NewMode field doesn't match the Mode field of any entry of the SupportedModes list,
// the ChangeToModeResponse command's Status field SHALL indicate UnsupportedMode and
// the StatusText field SHALL be included and MAY be used to indicate the issue, with a human readable string,
// or include an empty string.
// We are leaving the StatusText empty since the Status is descriptive enough.
if (!IsSupportedMode(newMode))
{
ChipLogError(Zcl, "ModeBase: Failed to find the option with mode %u", newMode);
response.status = to_underlying(StatusCode::kUnsupportedMode);
commandObj.AddResponse(commandPath, response);
return std::nullopt;
}
// If the NewMode field is the same as the value of the CurrentMode attribute
// the ChangeToModeResponse command SHALL have the Status field set to Success and
// the StatusText field MAY be supplied with a human readable string or include an empty string.
// We are leaving the StatusText empty since the Status is descriptive enough.
if (newMode == GetCurrentMode())
{
response.status = to_underlying(StatusCode::kSuccess);
commandObj.AddResponse(commandPath, response);
return std::nullopt;
}
mAppDelegate.HandleChangeToMode(newMode, response);
if (response.status == to_underlying(StatusCode::kSuccess))
{
Status status = UpdateCurrentMode(newMode);
if (status != Status::Success)
{
response.status = to_underlying(StatusCode::kGenericFailure);
}
else
{
ChipLogProgress(Zcl, "ModeBase: HandleChangeToMode changed to mode %u", newMode);
}
}
commandObj.AddResponse(commandPath, response);
return std::nullopt;
}
void ModeBaseCluster::LogStatus(Status status, const uint8_t & value, const char * attributeName)
{
if (status == Status::Success)
{
ChipLogDetail(Zcl, "ModeBase: Loaded %s as %u", attributeName, value);
}
else
{
ChipLogError(Zcl, "ModeBase: Could not update %s to %u: %u", attributeName, value, to_underlying(status));
}
}
void ModeBaseCluster::LogStatus(Status status, const DataModel::Nullable<uint8_t> & value, const char * attributeName)
{
if (status == Status::Success)
{
if (!value.IsNull())
{
ChipLogDetail(Zcl, "ModeBase: Loaded %s as %u", attributeName, value.Value());
}
else
{
ChipLogDetail(Zcl, "ModeBase: Loaded %s as null", attributeName);
}
}
else
{
if (!value.IsNull())
{
ChipLogError(Zcl, "ModeBase: Could not update %s to %u: %u", attributeName, value.Value(), to_underlying(status));
}
else
{
ChipLogError(Zcl, "ModeBase: Could not update %s to null: %u", attributeName, to_underlying(status));
}
}
}
void ModeBaseCluster::LoadPersistentAttributes()
{
AttributePersistence attrPersistence{ mContext->attributeStorage };
uint8_t currentMode = 0;
DataModel::Nullable<uint8_t> startUpMode;
DataModel::Nullable<uint8_t> onMode;
if (attrPersistence.LoadNativeEndianValue({ mPath.mEndpointId, mPath.mClusterId, CurrentMode::Id }, currentMode, mCurrentMode))
{
LogStatus(UpdateCurrentMode(currentMode), currentMode, "CurrentMode");
}
else
{
ChipLogError(Zcl, "ModeBase: Unable to load the CurrentMode from the KVS. Assuming zero.");
}
if (mOptionalAttributeSet.IsSet(StartUpMode::Id))
{
if (attrPersistence.LoadNativeEndianValue({ mPath.mEndpointId, mPath.mClusterId, StartUpMode::Id }, startUpMode,
mStartUpMode))
{
LogStatus(UpdateStartUpMode(startUpMode), startUpMode, "StartUpMode");
}
else
{
ChipLogError(Zcl, "ModeBase: Unable to load the StartUpMode from the KVS. Assuming null.");
}
}
if (mFeature.Has(Feature::kOnOff))
{
if (attrPersistence.LoadNativeEndianValue({ mPath.mEndpointId, mPath.mClusterId, OnMode::Id }, onMode, mOnMode))
{
LogStatus(UpdateOnMode(onMode), onMode, "OnMode");
}
else
{
ChipLogError(Zcl, "ModeBase: Unable to load the OnMode from the KVS. Assuming null.");
}
}
}
CHIP_ERROR ModeBaseCluster::EncodeSupportedModes(const AttributeValueEncoder::ListEncodeHelper & encoder)
{
for (uint8_t i = 0; true; i++)
{
ModeOptionStructType mode;
// Get the mode label
char buffer[kMaxModeLabelSize];
MutableCharSpan label(buffer);
auto err = mAppDelegate.GetModeLabelByIndex(i, label);
if (err == CHIP_ERROR_PROVIDER_LIST_EXHAUSTED)
{
return CHIP_NO_ERROR;
}
ReturnErrorOnFailure(err);
mode.label = label;
// Get the mode value
ReturnErrorOnFailure(mAppDelegate.GetModeValueByIndex(i, mode.mode));
// Get the mode tags
ModeTagStructType tagsBuffer[kMaxNumOfModeTags];
DataModel::List<ModeTagStructType> tags(tagsBuffer);
ReturnErrorOnFailure(mAppDelegate.GetModeTagsByIndex(i, tags));
mode.modeTags = tags;
ReturnErrorOnFailure(encoder.Encode(mode));
}
return CHIP_NO_ERROR;
}
} // namespace chip::app::Clusters