/* * * Copyright (c) 2023 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 #include #include #include #include #include #include #include #include #include #include using namespace chip; using namespace chip::app; using namespace chip::app::Clusters; 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 { namespace app { namespace Clusters { namespace ModeBase { Instance::Instance(Delegate * aDelegate, EndpointId aEndpointId, ClusterId aClusterId, uint32_t aFeature) : CommandHandlerInterface(Optional(aEndpointId), aClusterId), AttributeAccessInterface(Optional(aEndpointId), aClusterId), mDelegate(aDelegate), mEndpointId(aEndpointId), mClusterId(aClusterId), mCurrentMode(0), // This is a temporary value and may not be valid. We will change this to the value of the first // mode in the list at the start of the Init function to ensure that it represents a valid mode. mFeature(aFeature) { mDelegate->SetInstance(this); } Instance::~Instance() { Shutdown(); } void Instance::Shutdown() { if (!IsInList()) { return; } UnregisterThisInstance(); CommandHandlerInterfaceRegistry::Instance().UnregisterCommandHandler(this); AttributeAccessInterfaceRegistry::Instance().Unregister(this); } CHIP_ERROR Instance::Init() { // Initialise the current mode with the value of the first mode. This ensures that it is representing a valid mode. ReturnErrorOnFailure(mDelegate->GetModeValueByIndex(0, mCurrentMode)); // Check if the cluster has been selected in zap VerifyOrDie(emberAfContainsServer(mEndpointId, mClusterId) == true); LoadPersistentAttributes(); ReturnErrorOnFailure(CommandHandlerInterfaceRegistry::Instance().RegisterCommandHandler(this)); VerifyOrReturnError(AttributeAccessInterfaceRegistry::Instance().Register(this), CHIP_ERROR_INCORRECT_STATE); RegisterThisInstance(); ReturnErrorOnFailure(mDelegate->Init()); // 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 error = DeviceLayer::GetDiagnosticDataProvider().GetBootReason(bootReason); if (error != CHIP_NO_ERROR) { ChipLogError( Zcl, "Unable to retrieve boot reason: %" CHIP_ERROR_FORMAT ". Assuming that we did not reboot because of an OTA", error.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()); } } } #ifdef MATTER_DM_PLUGIN_ON_OFF_SERVER // OnMode with Power Up // If the On/Off feature is supported and the On/Off cluster attribute StartUpOnOff is present, with a // value of On (turn on at power up), then the CurrentMode attribute SHALL be set to the OnMode attribute // value when the server is supplied with power, except if the OnMode attribute is null. if (emberAfContainsServer(mEndpointId, OnOff::Id) && emberAfContainsAttribute(mEndpointId, OnOff::Id, OnOff::Attributes::StartUpOnOff::Id) && emberAfContainsAttribute(mEndpointId, mClusterId, ModeBase::Attributes::OnMode::Id) && HasFeature(ModeBase::Feature::kOnOff)) { DataModel::Nullable onMode = GetOnMode(); bool onOffValueForStartUp = false; if (!emberAfIsKnownVolatileAttribute(mEndpointId, OnOff::Id, OnOff::Attributes::StartUpOnOff::Id) && OnOffServer::Instance().getOnOffValueForStartUp(mEndpointId, onOffValueForStartUp) == Status::Success) { if (onOffValueForStartUp && !onMode.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()); } } } } #endif // MATTER_DM_PLUGIN_ON_OFF_SERVER return CHIP_NO_ERROR; } Status Instance::UpdateCurrentMode(uint8_t aNewMode) { if (!IsSupportedMode(aNewMode)) { return Protocols::InteractionModel::Status::ConstraintError; } uint8_t oldMode = mCurrentMode; mCurrentMode = aNewMode; if (mCurrentMode != oldMode) { // Write new value to persistent storage. ConcreteAttributePath path = ConcreteAttributePath(mEndpointId, mClusterId, Attributes::CurrentMode::Id); GetSafeAttributePersistenceProvider()->WriteScalarValue(path, mCurrentMode); MatterReportingAttributeChangeCallback(path); } return Protocols::InteractionModel::Status::Success; } Status Instance::UpdateStartUpMode(DataModel::Nullable aNewStartUpMode) { if (!aNewStartUpMode.IsNull()) { if (!IsSupportedMode(aNewStartUpMode.Value())) { return Protocols::InteractionModel::Status::ConstraintError; } } DataModel::Nullable oldStartUpMode = mStartUpMode; mStartUpMode = aNewStartUpMode; if (mStartUpMode != oldStartUpMode) { // Write new value to persistent storage. ConcreteAttributePath path = ConcreteAttributePath(mEndpointId, mClusterId, Attributes::StartUpMode::Id); GetSafeAttributePersistenceProvider()->WriteScalarValue(path, mStartUpMode); MatterReportingAttributeChangeCallback(path); } return Protocols::InteractionModel::Status::Success; } Status Instance::UpdateOnMode(DataModel::Nullable aNewOnMode) { if (!aNewOnMode.IsNull()) { if (!IsSupportedMode(aNewOnMode.Value())) { return Protocols::InteractionModel::Status::ConstraintError; } } DataModel::Nullable oldOnMode = mOnMode; mOnMode = aNewOnMode; if (mOnMode != oldOnMode) { // Write new value to persistent storage. ConcreteAttributePath path = ConcreteAttributePath(mEndpointId, mClusterId, Attributes::OnMode::Id); GetSafeAttributePersistenceProvider()->WriteScalarValue(path, mOnMode); MatterReportingAttributeChangeCallback(path); } return Protocols::InteractionModel::Status::Success; } uint8_t Instance::GetCurrentMode() const { return mCurrentMode; } DataModel::Nullable Instance::GetStartUpMode() const { return mStartUpMode; } DataModel::Nullable Instance::GetOnMode() const { return mOnMode; } void Instance::ReportSupportedModesChange() { MatterReportingAttributeChangeCallback(ConcreteAttributePath(mEndpointId, mClusterId, Attributes::SupportedModes::Id)); } bool Instance::HasFeature(Feature feature) const { return (mFeature & to_underlying(feature)) != 0; } bool Instance::IsSupportedMode(uint8_t modeValue) { uint8_t value; for (uint8_t i = 0; mDelegate->GetModeValueByIndex(i, value) != CHIP_ERROR_PROVIDER_LIST_EXHAUSTED; i++) { if (value == modeValue) { return true; } } ChipLogDetail(Zcl, "Cannot find a mode with value %u", modeValue); return false; } CHIP_ERROR Instance::GetModeValueByModeTag(uint16_t modeTagValue, uint8_t & value) { ModeTagStructType tagsBuffer[kMaxNumOfModeTags]; DataModel::List mTags(tagsBuffer); for (uint8_t i = 0; mDelegate->GetModeTagsByIndex(i, mTags) != CHIP_ERROR_PROVIDER_LIST_EXHAUSTED; i++) { for (size_t ii = 0; ii < mTags.size(); ii++) { if (mTags[ii].value == modeTagValue) { mDelegate->GetModeValueByIndex(i, value); return CHIP_NO_ERROR; } } mTags = tagsBuffer; } ChipLogDetail(Zcl, "Cannot find a mode with mode tag %x", modeTagValue); return CHIP_ERROR_PROVIDER_LIST_EXHAUSTED; } // private methods template 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) { switch (handlerContext.mRequestPath.mCommandId) { case ModeBase::Commands::ChangeToMode::Id: ChipLogDetail(Zcl, "ModeBase: Entering handling ChangeToModeWithStatus"); HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleChangeToMode(ctx, commandData); }); } } // Implements the read functionality for complex attributes. CHIP_ERROR Instance::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) { switch (aPath.mAttributeId) { case Attributes::CurrentMode::Id: ReturnErrorOnFailure(aEncoder.Encode(mCurrentMode)); break; case Attributes::StartUpMode::Id: ReturnErrorOnFailure(aEncoder.Encode(mStartUpMode)); break; case Attributes::OnMode::Id: ReturnErrorOnFailure(aEncoder.Encode(mOnMode)); break; case Attributes::FeatureMap::Id: ReturnErrorOnFailure(aEncoder.Encode(mFeature)); break; case Attributes::SupportedModes::Id: Instance * d = this; CHIP_ERROR err = aEncoder.EncodeList([d](const auto & encoder) -> CHIP_ERROR { return d->EncodeSupportedModes(encoder); }); return err; } return CHIP_NO_ERROR; } // Implements checking before attribute writes. CHIP_ERROR Instance::Write(const ConcreteDataAttributePath & attributePath, AttributeValueDecoder & aDecoder) { DataModel::Nullable newMode; ReturnErrorOnFailure(aDecoder.Decode(newMode)); Status status; switch (attributePath.mAttributeId) { case ModeBase::Attributes::StartUpMode::Id: status = UpdateStartUpMode(newMode); return StatusIB(status).ToChipError(); case ModeBase::Attributes::OnMode::Id: status = UpdateOnMode(newMode); return StatusIB(status).ToChipError(); } return CHIP_ERROR_INCORRECT_STATE; } void Instance::RegisterThisInstance() { if (!gModeBaseAliasesInstances.Contains(this)) { gModeBaseAliasesInstances.PushBack(this); } } void Instance::UnregisterThisInstance() { gModeBaseAliasesInstances.Remove(this); } void Instance::HandleChangeToMode(HandlerContext & ctx, const Commands::ChangeToMode::DecodableType & commandData) { MATTER_TRACE_SCOPE("ChangeToMode", "ModeBase"); 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); ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); return; } // 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(ModeBase::StatusCode::kSuccess); ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); return; } mDelegate->HandleChangeToMode(newMode, response); if (response.status == to_underlying(StatusCode::kSuccess)) { UpdateCurrentMode(newMode); ChipLogProgress(Zcl, "ModeBase: HandleChangeToMode changed to mode %u", newMode); } ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); } void Instance::LoadPersistentAttributes() { // Load Current Mode uint8_t tempCurrentMode; CHIP_ERROR err = GetSafeAttributePersistenceProvider()->ReadScalarValue( ConcreteAttributePath(mEndpointId, mClusterId, Attributes::CurrentMode::Id), tempCurrentMode); if (err == CHIP_NO_ERROR) { Status status = UpdateCurrentMode(tempCurrentMode); if (status == Status::Success) { ChipLogDetail(Zcl, "ModeBase: Loaded CurrentMode as %u", GetCurrentMode()); } else { ChipLogError(Zcl, "ModeBase: Could not update CurrentMode to %u: %u", tempCurrentMode, to_underlying(status)); } } else { // If we cannot find the previous CurrentMode, we will assume it to be the first mode in the // list, as was initialised in the constructor. ChipLogDetail(Zcl, "ModeBase: Unable to load the CurrentMode from the KVS. Assuming %u", GetCurrentMode()); } // Load Start-Up Mode DataModel::Nullable tempStartUpMode; err = GetSafeAttributePersistenceProvider()->ReadScalarValue( ConcreteAttributePath(mEndpointId, mClusterId, Attributes::StartUpMode::Id), tempStartUpMode); if (err == CHIP_NO_ERROR) { Status status = UpdateStartUpMode(tempStartUpMode); if (status == Status::Success) { if (GetStartUpMode().IsNull()) { ChipLogDetail(Zcl, "ModeBase: Loaded StartUpMode as null"); } else { ChipLogDetail(Zcl, "ModeBase: Loaded StartUpMode as %u", GetStartUpMode().Value()); } } else { ChipLogError(Zcl, "ModeBase: Could not update StartUpMode: %u", to_underlying(status)); } } else { ChipLogDetail(Zcl, "ModeBase: Unable to load the StartUpMode from the KVS. Assuming null"); } // Load On Mode DataModel::Nullable tempOnMode; err = GetSafeAttributePersistenceProvider()->ReadScalarValue( ConcreteAttributePath(mEndpointId, mClusterId, Attributes::OnMode::Id), tempOnMode); if (err == CHIP_NO_ERROR) { Status status = UpdateOnMode(tempOnMode); if (status == Status::Success) { if (GetOnMode().IsNull()) { ChipLogDetail(Zcl, "ModeBase: Loaded OnMode as null"); } else { ChipLogDetail(Zcl, "ModeBase: Loaded OnMode as %u", GetOnMode().Value()); } } else { ChipLogError(Zcl, "ModeBase: Could not update OnMode: %u", to_underlying(status)); } } else { ChipLogDetail(Zcl, "ModeBase: Unable to load the OnMode from the KVS. Assuming null"); } } CHIP_ERROR Instance::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 = mDelegate->GetModeLabelByIndex(i, label); if (err == CHIP_ERROR_PROVIDER_LIST_EXHAUSTED) { return CHIP_NO_ERROR; } ReturnErrorOnFailure(err); mode.label = label; // Get the mode value ReturnErrorOnFailure(mDelegate->GetModeValueByIndex(i, mode.mode)); // Get the mode tags ModeTagStructType tagsBuffer[kMaxNumOfModeTags]; DataModel::List tags(tagsBuffer); ReturnErrorOnFailure(mDelegate->GetModeTagsByIndex(i, tags)); mode.modeTags = tags; ReturnErrorOnFailure(encoder.Encode(mode)); } return CHIP_NO_ERROR; } IntrusiveList & GetModeBaseInstanceList() { return gModeBaseAliasesInstances; } } // namespace ModeBase } // namespace Clusters } // namespace app } // namespace chip