blob: b31c0802dd0603a6cb7f40aa41d55011bb29afee [file] [log] [blame]
/*
* Copyright (c) 2026 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 <app/clusters/on-off-server/OnOffCluster.h>
#include <app/persistence/AttributePersistence.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <clusters/OnOff/Metadata.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/logging/CHIPLogging.h>
using namespace chip::app::Clusters::OnOff;
using chip::Protocols::InteractionModel::Status;
namespace chip::app::Clusters {
namespace {
class OnOffValidator : public scenes::AttributeValuePairValidator
{
public:
CHIP_ERROR Validate(const app::ConcreteClusterPath & clusterPath,
AttributeValuePairValidator::AttributeValuePairType & value) override
{
VerifyOrReturnError(clusterPath.mClusterId == Clusters::OnOff::Id, CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError(value.attributeID == Attributes::OnOff::Id, CHIP_ERROR_INVALID_ARGUMENT);
return CHIP_NO_ERROR;
}
};
OnOffValidator & GlobalOnOffValidator()
{
static OnOffValidator sValidator;
return sValidator;
}
} // namespace
OnOffCluster::OnOffCluster(EndpointId endpointId, const Context & context) :
OnOffCluster(endpointId, context, { Feature::kDeadFrontBehavior, Feature::kOffOnly })
{}
OnOffCluster::OnOffCluster(EndpointId endpointId, const Context & context, BitMask<Feature> supportedFeatures) :
DefaultServerCluster({ endpointId, Clusters::OnOff::Id }), DefaultSceneHandlerImpl(GlobalOnOffValidator()),
mFeatureMap(context.featureMap), mOnOff(context.defaults.onOff), mTimerDelegate(context.timerDelegate), mSceneTimer(*this)
{
VerifyOrDie(supportedFeatures.HasAll(context.featureMap));
// Feature validity check: offonly does not support any of the other features.
VerifyOrDie(!context.featureMap.Has(Feature::kOffOnly) || context.featureMap.HasOnly(Feature::kOffOnly));
}
OnOffCluster::~OnOffCluster()
{
mSceneTimer.Cancel();
mDelegates.Clear();
}
CHIP_ERROR OnOffCluster::Startup(ServerClusterContext & context)
{
ReturnErrorOnFailure(DefaultServerCluster::Startup(context));
AttributePersistence attributePersistence(context.attributeStorage);
attributePersistence.LoadNativeEndianValue(ConcreteAttributePath(mPath.mEndpointId, Clusters::OnOff::Id, Attributes::OnOff::Id),
mOnOff, mOnOff);
for (auto & delegate : mDelegates)
{
delegate.OnOffStartup(mOnOff);
}
return CHIP_NO_ERROR;
}
CHIP_ERROR OnOffCluster::AcceptedCommands(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
{
if (mFeatureMap.Has(Feature::kOffOnly))
{
static constexpr DataModel::AcceptedCommandEntry kOffOnlyCommands[] = {
Commands::Off::kMetadataEntry,
};
return builder.ReferenceExisting(kOffOnlyCommands);
}
static constexpr DataModel::AcceptedCommandEntry kAllCommands[] = {
Commands::Off::kMetadataEntry,
Commands::On::kMetadataEntry,
Commands::Toggle::kMetadataEntry,
};
return builder.ReferenceExisting(kAllCommands);
}
CHIP_ERROR OnOffCluster::Attributes(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
AttributeListBuilder listBuilder(builder);
return listBuilder.Append(Span(Attributes::kMandatoryMetadata), {});
}
DataModel::ActionReturnStatus OnOffCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder)
{
switch (request.path.mAttributeId)
{
case Attributes::ClusterRevision::Id:
return encoder.Encode(kRevision);
case Attributes::FeatureMap::Id:
return encoder.Encode(mFeatureMap.Raw());
case Attributes::OnOff::Id:
return encoder.Encode(mOnOff);
default:
return Protocols::InteractionModel::Status::UnsupportedAttribute;
}
}
CHIP_ERROR OnOffCluster::SetOnOff(bool on)
{
mSceneTimer.Cancel();
VerifyOrReturnError(mOnOff != on, CHIP_NO_ERROR);
mOnOff = on;
NotifyAttributeChanged(Attributes::OnOff::Id);
// Persist
LogErrorOnFailure(
mContext->attributeStorage.WriteValue(ConcreteAttributePath(mPath.mEndpointId, Clusters::OnOff::Id, Attributes::OnOff::Id),
ByteSpan(reinterpret_cast<const uint8_t *>(&mOnOff), sizeof(mOnOff))));
for (auto & delegate : mDelegates)
{
delegate.OnOnOffChanged(mOnOff);
}
return CHIP_NO_ERROR;
}
std::optional<DataModel::ActionReturnStatus> OnOffCluster::InvokeCommand(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments, CommandHandler * handler)
{
// Note: OffOnly feature validation is handled by the Interaction Model by checking the AcceptedCommands list.
switch (request.path.mCommandId)
{
case Commands::Off::Id:
return SetOnOff(false);
case Commands::On::Id:
return SetOnOff(true);
case Commands::Toggle::Id:
return SetOnOff(!mOnOff);
default:
return Status::UnsupportedCommand;
}
}
bool OnOffCluster::SupportsCluster(EndpointId endpoint, ClusterId cluster)
{
return (cluster == Clusters::OnOff::Id) && (endpoint == mPath.mEndpointId);
}
CHIP_ERROR OnOffCluster::SerializeSave(EndpointId endpoint, ClusterId cluster, MutableByteSpan & serializedBytes)
{
using AttributeValuePair = ScenesManagement::Structs::AttributeValuePairStruct::Type;
if (!SupportsCluster(endpoint, cluster))
{
return CHIP_ERROR_INVALID_ARGUMENT;
}
AttributeValuePair pairs[1];
pairs[0].attributeID = Attributes::OnOff::Id;
pairs[0].valueUnsigned8.SetValue(mOnOff);
app::DataModel::List<AttributeValuePair> attributeValueList(pairs);
return EncodeAttributeValueList(attributeValueList, serializedBytes);
}
CHIP_ERROR OnOffCluster::ApplyScene(EndpointId endpoint, ClusterId cluster, const ByteSpan & serializedBytes,
scenes::TransitionTimeMs timeMs)
{
app::DataModel::DecodableList<ScenesManagement::Structs::AttributeValuePairStruct::DecodableType> attributeValueList;
if (!SupportsCluster(endpoint, cluster))
{
return CHIP_ERROR_INVALID_ARGUMENT;
}
ReturnErrorOnFailure(DecodeAttributeValueList(serializedBytes, attributeValueList));
auto pair_iterator = attributeValueList.begin();
while (pair_iterator.Next())
{
auto & decodePair = pair_iterator.GetValue();
// Match codegen strictness: verify attribute ID is strictly OnOff and value is present
VerifyOrReturnError(decodePair.attributeID == Attributes::OnOff::Id, CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError(decodePair.valueUnsigned8.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
bool targetValue = static_cast<bool>(decodePair.valueUnsigned8.Value());
if (timeMs > 0)
{
mSceneTimer.Cancel();
mSceneTimer.Start(timeMs, targetValue);
}
else
{
ReturnErrorOnFailure(SetOnOff(targetValue));
}
}
return pair_iterator.GetStatus();
}
void OnOffCluster::SceneTransitionTimer::Start(uint32_t timeMs, bool targetValue)
{
mTargetValue = targetValue;
LogErrorOnFailure(mCluster.mTimerDelegate.StartTimer(this, System::Clock::Milliseconds32(timeMs)));
}
void OnOffCluster::SceneTransitionTimer::Cancel()
{
mCluster.mTimerDelegate.CancelTimer(this);
}
void OnOffCluster::SceneTransitionTimer::TimerFired()
{
LogErrorOnFailure(mCluster.SetOnOff(mTargetValue));
}
} // namespace chip::app::Clusters