blob: 2a306c33f5ea0b9c1ddaca9c47ed61b12fb207cc [file]
/*
* Copyright (c) 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/audio-control-server/AudioControlCluster.h>
#include <algorithm>
#include <app/ConcreteAttributePath.h>
#include <app/MessageDef/StatusIB.h>
#include <app/data-model/Decode.h>
#include <app/data-model/List.h>
#include <app/persistence/AttributePersistence.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <clusters/AudioControl/Commands.h>
#include <clusters/AudioControl/Metadata.h>
#include <clusters/ScenesManagement/Structs.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/Span.h>
#include <lib/support/logging/CHIPLogging.h>
#include <optional>
namespace chip::app::Clusters {
using namespace AudioControl;
using namespace AudioControl::Attributes;
using namespace AudioControl::Commands;
using chip::Protocols::InteractionModel::Status;
namespace {
// Adds stepSize to volume, capped at effectiveMax. The intermediate sum is computed in uint32_t
// to avoid overflow; the cast back to uint16_t is safe because std::min bounds the result to
// effectiveMax, which is already a uint16_t.
uint16_t AddCappedAtMax(uint16_t volume, uint16_t stepSize, uint16_t effectiveMax)
{
return static_cast<uint16_t>(std::min<uint32_t>(static_cast<uint32_t>(volume) + static_cast<uint32_t>(stepSize), effectiveMax));
}
// Restricts scene extension field sets to the AudioControl cluster and its scene-able
// attributes. Per-attribute enablement (BEQ / optional bands) is enforced by the setters in
// ApplyScene(); this only rejects clearly out-of-cluster IDs.
class AudioControlSceneValidator : public scenes::AttributeValuePairValidator
{
public:
CHIP_ERROR Validate(const app::ConcreteClusterPath & clusterPath,
AttributeValuePairValidator::AttributeValuePairType & value) override
{
VerifyOrReturnError(clusterPath.mClusterId == AudioControl::Id, CHIP_ERROR_INVALID_ARGUMENT);
switch (value.attributeID)
{
case SoftMuted::Id:
case Volume::Id:
case Bass::Id:
case Mid::Id:
case Treble::Id:
return CHIP_NO_ERROR;
default:
return CHIP_ERROR_INVALID_ARGUMENT;
}
}
};
AudioControlSceneValidator & GlobalSceneValidator()
{
static AudioControlSceneValidator sValidator;
return sValidator;
}
} // namespace
AudioControlCluster::AudioControlCluster(EndpointId endpointId, AudioControlDelegate & delegate, const Config & config) :
DefaultServerCluster({ endpointId, AudioControl::Id }), scenes::DefaultSceneHandlerImpl(GlobalSceneValidator()),
mDelegate(delegate), mFeatures(config.mFeatures), mOptionalAttributeSet(config.mOptionalAttributeSet),
mMinDeviceVolume(delegate.GetMinDeviceVolume()), mMaxDeviceVolume(delegate.GetMaxDeviceVolume()),
mMaxDeviceVolumeDB(delegate.GetMaxDeviceVolumeDB()), mMinCorrection(delegate.GetMinCorrection()),
mMaxCorrection(delegate.GetMaxCorrection())
{
VerifyOrDie(mMinDeviceVolume >= 1);
VerifyOrDie(mMaxDeviceVolume >= mMinDeviceVolume);
VerifyOrDie(config.mVolume == 0 || (config.mVolume >= mMinDeviceVolume && config.mVolume <= mMaxDeviceVolume));
if (mFeatures.Has(Feature::kBasicEqualizer))
{
VerifyOrDie(mMinCorrection <= 0);
VerifyOrDie(mMaxCorrection >= 0);
// BEQ feature requires at least one EQ band to be enabled.
VerifyOrDie(mOptionalAttributeSet.IsSet(Bass::Id) || mOptionalAttributeSet.IsSet(Mid::Id) ||
mOptionalAttributeSet.IsSet(Treble::Id));
}
if (mFeatures.Has(Feature::kDecibel))
{
VerifyOrDie(!mOptionalAttributeSet.IsSet(MaxDeviceVolumeDB::Id));
}
VerifyOrDie(config.mSetVolumeUnmutePolicy < UnmutePolicyEnum::kUnknownEnumValue);
VerifyOrDie(config.mIncreaseVolumeUnmutePolicy < UnmutePolicyEnum::kUnknownEnumValue);
VerifyOrDie(config.mIncreaseVolumeUnmuteVolume < UnmuteVolumeEnum::kUnknownEnumValue);
VerifyOrDie(config.mDecreaseVolumeUnmutePolicy < UnmutePolicyEnum::kUnknownEnumValue);
mSoftMuted = config.mSoftMuted;
mPhysicallyMuted = config.mPhysicallyMuted;
mVolume = config.mVolume;
mDefaultStepSize = config.mDefaultStepSize;
mSetVolumeUnmutePolicy = config.mSetVolumeUnmutePolicy;
mIncreaseVolumeUnmutePolicy = config.mIncreaseVolumeUnmutePolicy;
mIncreaseVolumeUnmuteVolume = config.mIncreaseVolumeUnmuteVolume;
mDecreaseVolumeUnmutePolicy = config.mDecreaseVolumeUnmutePolicy;
mStartUpMuted = config.mStartUpMuted;
mStartUpVolume = config.mStartUpVolume;
mBass = config.mBass;
mMid = config.mMid;
mTreble = config.mTreble;
mMaxUserVolume = mOptionalAttributeSet.IsSet(MaxUserVolume::Id) ? config.mMaxUserVolume : mMaxDeviceVolume;
VerifyOrDie(mMaxUserVolume >= mMinDeviceVolume && mMaxUserVolume <= mMaxDeviceVolume);
}
CHIP_ERROR AudioControlCluster::Startup(ServerClusterContext & context)
{
ReturnErrorOnFailure(DefaultServerCluster::Startup(context));
AttributePersistence attributePersistence(context.attributeStorage);
// MaxUserVolume must be restored before the volume clamp (EffectiveMaxVolume() depends on it).
if (mOptionalAttributeSet.IsSet(MaxUserVolume::Id))
{
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::MaxUserVolume::Id), mMaxUserVolume,
mMaxUserVolume);
// Guard against a corrupted/stale KVS value so EffectiveMaxVolume() never returns something
// below mMinDeviceVolume, which would violate std::clamp's precondition below.
mMaxUserVolume = std::clamp(mMaxUserVolume, mMinDeviceVolume, mMaxDeviceVolume);
}
// Restore last-known Volume (always; serves as fallback when StartUpVolume is null or absent).
attributePersistence.LoadNativeEndianValue(ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Volume::Id),
mVolume, mVolume);
// Restore behavioral configuration.
// For enum attributes, capture the config default before loading so that a stale/corrupted
// KVS entry that decodes to kUnknownEnumValue falls back to the config default rather than
// leaving the enum in an invalid state that would break command handling.
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::DefaultStepSize::Id), mDefaultStepSize,
mDefaultStepSize);
// Guard against a corrupted/stale KVS value: a step size of 0 would make IncreaseVolume/
// DecreaseVolume fail permanently whenever the command omits an explicit stepSize.
mDefaultStepSize = std::max<uint16_t>(mDefaultStepSize, 1);
const auto defaultSetVolumePolicy = mSetVolumeUnmutePolicy;
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::SetVolumeUnmutePolicy::Id), mSetVolumeUnmutePolicy,
defaultSetVolumePolicy);
if (mSetVolumeUnmutePolicy >= UnmutePolicyEnum::kUnknownEnumValue)
{
mSetVolumeUnmutePolicy = defaultSetVolumePolicy;
}
const auto defaultIncreaseVolumePolicy = mIncreaseVolumeUnmutePolicy;
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::IncreaseVolumeUnmutePolicy::Id),
mIncreaseVolumeUnmutePolicy, defaultIncreaseVolumePolicy);
if (mIncreaseVolumeUnmutePolicy >= UnmutePolicyEnum::kUnknownEnumValue)
{
mIncreaseVolumeUnmutePolicy = defaultIncreaseVolumePolicy;
}
const auto defaultIncreaseVolumeVolume = mIncreaseVolumeUnmuteVolume;
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::IncreaseVolumeUnmuteVolume::Id),
mIncreaseVolumeUnmuteVolume, defaultIncreaseVolumeVolume);
if (mIncreaseVolumeUnmuteVolume >= UnmuteVolumeEnum::kUnknownEnumValue)
{
mIncreaseVolumeUnmuteVolume = defaultIncreaseVolumeVolume;
}
const auto defaultDecreaseVolumePolicy = mDecreaseVolumeUnmutePolicy;
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::DecreaseVolumeUnmutePolicy::Id),
mDecreaseVolumeUnmutePolicy, defaultDecreaseVolumePolicy);
if (mDecreaseVolumeUnmutePolicy >= UnmutePolicyEnum::kUnknownEnumValue)
{
mDecreaseVolumeUnmutePolicy = defaultDecreaseVolumePolicy;
}
// SoftMuted is non-volatile per spec and must be restored on startup regardless of whether
// StartUpMuted is supported.
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::SoftMuted::Id), mSoftMuted, mSoftMuted);
// Apply StartUpMuted:
// absent -> mSoftMuted stays at the just-restored persisted value.
// null -> retain the persisted SoftMuted state.
// non-null -> override with the StartUpMuted value.
if (mOptionalAttributeSet.IsSet(StartUpMuted::Id))
{
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::StartUpMuted::Id), mStartUpMuted, mStartUpMuted);
if (!mStartUpMuted.IsNull())
{
const bool startUpMuted = mStartUpMuted.Value();
if (mSoftMuted != startUpMuted)
{
mSoftMuted = startUpMuted;
StoreSoftMuted();
}
}
}
// Apply StartUpVolume: absent → mVolume stays at the persisted Volume (null-like behavior).
const uint16_t persistedVolume = mVolume;
if (mOptionalAttributeSet.IsSet(StartUpVolume::Id))
{
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::StartUpVolume::Id), mStartUpVolume,
mStartUpVolume);
if (!mStartUpVolume.IsNull())
{
mVolume = mStartUpVolume.Value();
}
}
mVolume = std::clamp(mVolume, mMinDeviceVolume, EffectiveMaxVolume());
if (mVolume != persistedVolume)
{
StoreVolume();
}
if (mFeatures.Has(Feature::kBasicEqualizer))
{
if (mOptionalAttributeSet.IsSet(Bass::Id))
{
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Bass::Id), mBass, mBass);
mBass = std::clamp(mBass, mMinCorrection, mMaxCorrection);
}
if (mOptionalAttributeSet.IsSet(Mid::Id))
{
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Mid::Id), mMid, mMid);
mMid = std::clamp(mMid, mMinCorrection, mMaxCorrection);
}
if (mOptionalAttributeSet.IsSet(Treble::Id))
{
attributePersistence.LoadNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Treble::Id), mTreble, mTreble);
mTreble = std::clamp(mTreble, mMinCorrection, mMaxCorrection);
}
}
mDelegate.OnStartup({ mVolume, mSoftMuted, mBass, mMid, mTreble });
return CHIP_NO_ERROR;
}
uint16_t AudioControlCluster::EffectiveMaxVolume() const
{
return mOptionalAttributeSet.IsSet(MaxUserVolume::Id) ? mMaxUserVolume : mMaxDeviceVolume;
}
void AudioControlCluster::StoreVolume()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Volume::Id), mVolume));
}
void AudioControlCluster::StoreSoftMuted()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::SoftMuted::Id), mSoftMuted));
}
void AudioControlCluster::StoreStartUpMuted()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::StartUpMuted::Id), mStartUpMuted));
}
void AudioControlCluster::StoreStartUpVolume()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::StartUpVolume::Id), mStartUpVolume));
}
void AudioControlCluster::StoreDefaultStepSize()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::DefaultStepSize::Id), mDefaultStepSize));
}
void AudioControlCluster::StoreSetVolumeUnmutePolicy()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::SetVolumeUnmutePolicy::Id), mSetVolumeUnmutePolicy));
}
void AudioControlCluster::StoreIncreaseVolumeUnmutePolicy()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::IncreaseVolumeUnmutePolicy::Id),
mIncreaseVolumeUnmutePolicy));
}
void AudioControlCluster::StoreIncreaseVolumeUnmuteVolume()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::IncreaseVolumeUnmuteVolume::Id),
mIncreaseVolumeUnmuteVolume));
}
void AudioControlCluster::StoreDecreaseVolumeUnmutePolicy()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::DecreaseVolumeUnmutePolicy::Id),
mDecreaseVolumeUnmutePolicy));
}
void AudioControlCluster::StoreMaxUserVolume()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::MaxUserVolume::Id), mMaxUserVolume));
}
void AudioControlCluster::StoreBass()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Bass::Id), mBass));
}
void AudioControlCluster::StoreMid()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Mid::Id), mMid));
}
void AudioControlCluster::StoreTreble()
{
VerifyOrReturn(mContext != nullptr);
AttributePersistence attributePersistence(mContext->attributeStorage);
LogErrorOnFailure(attributePersistence.StoreNativeEndianValue(
ConcreteAttributePath(mPath.mEndpointId, AudioControl::Id, Attributes::Treble::Id), mTreble));
}
DataModel::ActionReturnStatus AudioControlCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder)
{
switch (request.path.mAttributeId)
{
case ClusterRevision::Id:
return encoder.Encode(AudioControl::kRevision);
case FeatureMap::Id:
return encoder.Encode(mFeatures);
case SoftMuted::Id:
return encoder.Encode(mSoftMuted);
case PhysicallyMuted::Id:
return encoder.Encode(mPhysicallyMuted);
case Volume::Id:
return encoder.Encode(mVolume);
case MinDeviceVolume::Id:
return encoder.Encode(mMinDeviceVolume);
case MaxDeviceVolume::Id:
return encoder.Encode(mMaxDeviceVolume);
case MaxDeviceVolumeDB::Id:
return encoder.Encode(mMaxDeviceVolumeDB);
case MaxUserVolume::Id:
return encoder.Encode(mMaxUserVolume);
case DefaultStepSize::Id:
return encoder.Encode(mDefaultStepSize);
case SetVolumeUnmutePolicy::Id:
return encoder.Encode(mSetVolumeUnmutePolicy);
case IncreaseVolumeUnmutePolicy::Id:
return encoder.Encode(mIncreaseVolumeUnmutePolicy);
case IncreaseVolumeUnmuteVolume::Id:
return encoder.Encode(mIncreaseVolumeUnmuteVolume);
case DecreaseVolumeUnmutePolicy::Id:
return encoder.Encode(mDecreaseVolumeUnmutePolicy);
case StartUpMuted::Id:
return encoder.Encode(mStartUpMuted);
case StartUpVolume::Id:
return encoder.Encode(mStartUpVolume);
case Bass::Id:
return encoder.Encode(mBass);
case Mid::Id:
return encoder.Encode(mMid);
case Treble::Id:
return encoder.Encode(mTreble);
case MinCorrection::Id:
return encoder.Encode(mMinCorrection);
case MaxCorrection::Id:
return encoder.Encode(mMaxCorrection);
default:
return Status::UnsupportedAttribute;
}
}
DataModel::ActionReturnStatus AudioControlCluster::WriteAttribute(const DataModel::WriteAttributeRequest & request,
AttributeValueDecoder & decoder)
{
switch (request.path.mAttributeId)
{
case MaxUserVolume::Id: {
uint16_t value{};
ReturnErrorOnFailure(decoder.Decode(value));
return SetMaxUserVolume(value);
}
case DefaultStepSize::Id: {
uint16_t value{};
ReturnErrorOnFailure(decoder.Decode(value));
const uint16_t effectiveMax = EffectiveMaxVolume();
VerifyOrReturnError(value >= 1 && effectiveMax > mMinDeviceVolume && value <= effectiveMax - mMinDeviceVolume,
Status::ConstraintError);
if (SetAttributeValue(mDefaultStepSize, value, DefaultStepSize::Id))
{
StoreDefaultStepSize();
}
return CHIP_NO_ERROR;
}
case SetVolumeUnmutePolicy::Id: {
UnmutePolicyEnum value{};
ReturnErrorOnFailure(decoder.Decode(value));
VerifyOrReturnError(value < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError);
if (SetAttributeValue(mSetVolumeUnmutePolicy, value, SetVolumeUnmutePolicy::Id))
{
StoreSetVolumeUnmutePolicy();
}
return CHIP_NO_ERROR;
}
case IncreaseVolumeUnmutePolicy::Id: {
UnmutePolicyEnum value{};
ReturnErrorOnFailure(decoder.Decode(value));
VerifyOrReturnError(value < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError);
if (SetAttributeValue(mIncreaseVolumeUnmutePolicy, value, IncreaseVolumeUnmutePolicy::Id))
{
StoreIncreaseVolumeUnmutePolicy();
}
return CHIP_NO_ERROR;
}
case IncreaseVolumeUnmuteVolume::Id: {
UnmuteVolumeEnum value{};
ReturnErrorOnFailure(decoder.Decode(value));
VerifyOrReturnError(value < UnmuteVolumeEnum::kUnknownEnumValue, Status::ConstraintError);
if (SetAttributeValue(mIncreaseVolumeUnmuteVolume, value, IncreaseVolumeUnmuteVolume::Id))
{
StoreIncreaseVolumeUnmuteVolume();
}
return CHIP_NO_ERROR;
}
case DecreaseVolumeUnmutePolicy::Id: {
UnmutePolicyEnum value{};
ReturnErrorOnFailure(decoder.Decode(value));
VerifyOrReturnError(value < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError);
if (SetAttributeValue(mDecreaseVolumeUnmutePolicy, value, DecreaseVolumeUnmutePolicy::Id))
{
StoreDecreaseVolumeUnmutePolicy();
}
return CHIP_NO_ERROR;
}
case StartUpMuted::Id: {
DataModel::Nullable<bool> value{};
ReturnErrorOnFailure(decoder.Decode(value));
if (SetAttributeValue(mStartUpMuted, value, StartUpMuted::Id))
{
StoreStartUpMuted();
}
return CHIP_NO_ERROR;
}
case StartUpVolume::Id: {
DataModel::Nullable<uint16_t> value{};
ReturnErrorOnFailure(decoder.Decode(value));
if (!value.IsNull())
{
const uint16_t effectiveMax = EffectiveMaxVolume();
VerifyOrReturnError(value.Value() >= mMinDeviceVolume && value.Value() <= effectiveMax, Status::ConstraintError);
}
if (SetAttributeValue(mStartUpVolume, value, StartUpVolume::Id))
{
StoreStartUpVolume();
}
return CHIP_NO_ERROR;
}
case Bass::Id: {
int16_t value{};
ReturnErrorOnFailure(decoder.Decode(value));
VerifyOrReturnError(value >= mMinCorrection && value <= mMaxCorrection, Status::ConstraintError);
// Equality check gates the delegate call below, not just the reported status: a same-value
// write must not spuriously invoke HandleBassChanged.
if (value != mBass)
{
Status s = mDelegate.HandleBassChanged(value);
VerifyOrReturnError(s == Status::Success, s);
SetAttributeValue(mBass, value, Bass::Id);
StoreBass();
}
return CHIP_NO_ERROR;
}
case Mid::Id: {
int16_t value{};
ReturnErrorOnFailure(decoder.Decode(value));
VerifyOrReturnError(value >= mMinCorrection && value <= mMaxCorrection, Status::ConstraintError);
// Equality check gates the delegate call below, not just the reported status: a same-value
// write must not spuriously invoke HandleMidChanged.
if (value != mMid)
{
Status s = mDelegate.HandleMidChanged(value);
VerifyOrReturnError(s == Status::Success, s);
SetAttributeValue(mMid, value, Mid::Id);
StoreMid();
}
return CHIP_NO_ERROR;
}
case Treble::Id: {
int16_t value{};
ReturnErrorOnFailure(decoder.Decode(value));
VerifyOrReturnError(value >= mMinCorrection && value <= mMaxCorrection, Status::ConstraintError);
// Equality check gates the delegate call below, not just the reported status: a same-value
// write must not spuriously invoke HandleTrebleChanged.
if (value != mTreble)
{
Status s = mDelegate.HandleTrebleChanged(value);
VerifyOrReturnError(s == Status::Success, s);
SetAttributeValue(mTreble, value, Treble::Id);
StoreTreble();
}
return CHIP_NO_ERROR;
}
default:
return Status::UnsupportedAttribute;
}
}
CHIP_ERROR AudioControlCluster::Attributes(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
AttributeListBuilder listBuilder(builder);
const AttributeListBuilder::OptionalAttributeEntry optionalAttributes[] = {
{ mOptionalAttributeSet.IsSet(PhysicallyMuted::Id), PhysicallyMuted::kMetadataEntry },
{ !mFeatures.Has(Feature::kDecibel) && mOptionalAttributeSet.IsSet(MaxDeviceVolumeDB::Id),
MaxDeviceVolumeDB::kMetadataEntry },
{ mOptionalAttributeSet.IsSet(MaxUserVolume::Id), MaxUserVolume::kMetadataEntry },
{ mOptionalAttributeSet.IsSet(StartUpMuted::Id), StartUpMuted::kMetadataEntry },
{ mOptionalAttributeSet.IsSet(StartUpVolume::Id), StartUpVolume::kMetadataEntry },
{ mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Bass::Id), Bass::kMetadataEntry },
{ mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Mid::Id), Mid::kMetadataEntry },
{ mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Treble::Id), Treble::kMetadataEntry },
{ mFeatures.Has(Feature::kBasicEqualizer), MinCorrection::kMetadataEntry },
{ mFeatures.Has(Feature::kBasicEqualizer), MaxCorrection::kMetadataEntry },
};
return listBuilder.Append(Span(kMandatoryMetadata), Span(optionalAttributes));
}
CHIP_ERROR AudioControlCluster::AcceptedCommands(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
{
static constexpr DataModel::AcceptedCommandEntry kCommands[] = {
Mute::kMetadataEntry, Unmute::kMetadataEntry, ToggleMuted::kMetadataEntry,
SetVolume::kMetadataEntry, IncreaseVolume::kMetadataEntry, DecreaseVolume::kMetadataEntry,
};
return builder.ReferenceExisting(Span(kCommands));
}
// Applies the UnmutePolicy table (spec §ref_UnmutePolicyTable).
// Computes final (volume, softMuted), notifies the delegate, then updates cluster state.
// Only called when at least one of SoftMuted or PhysicallyMuted is TRUE.
Status AudioControlCluster::ApplyUnmutePolicy(UnmutePolicyEnum policy, uint16_t requestedVolume)
{
uint16_t newVolume = mVolume;
bool newSoftMuted = mSoftMuted;
switch (policy)
{
case UnmutePolicyEnum::kUnmuteOrChangeVolume:
// Always set Volume and clear SoftMuted, regardless of PhysicallyMuted.
newVolume = requestedVolume;
newSoftMuted = false;
break;
case UnmutePolicyEnum::kUnmuteOrDoNotChangeVolume:
if (mPhysicallyMuted)
{
// Cannot unmute a physically-muted device — leave state unchanged.
return Status::InvalidInState;
}
// PhysicallyMuted is FALSE; SoftMuted must have been TRUE (we're in muted state).
newVolume = requestedVolume;
newSoftMuted = false;
break;
case UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume:
// Change volume but leave SoftMuted unchanged.
newVolume = requestedVolume;
break;
case UnmutePolicyEnum::kDoNotUnmuteAndDoNotChangeVolume:
// Leave all state unchanged.
return Status::InvalidInState;
default:
// Not a valid UnmutePolicyEnum value — malformed command field, not a state conflict.
return Status::ConstraintError;
}
Status s = mDelegate.HandleVolumeAndMuteChange(newVolume, newSoftMuted);
VerifyOrReturnError(s == Status::Success, s);
if (SetAttributeValue(mVolume, newVolume, Volume::Id))
{
StoreVolume();
}
if (SetAttributeValue(mSoftMuted, newSoftMuted, SoftMuted::Id))
{
StoreSoftMuted();
}
return Status::Success;
}
std::optional<DataModel::ActionReturnStatus> AudioControlCluster::InvokeCommand(const DataModel::InvokeRequest & request,
chip::TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
switch (request.path.mCommandId)
{
case Mute::Id:
return HandleMute();
case Unmute::Id:
return HandleUnmute();
case ToggleMuted::Id:
return HandleToggleMuted();
case SetVolume::Id:
return HandleSetVolume(input_arguments);
case IncreaseVolume::Id:
return HandleIncreaseVolume(input_arguments);
case DecreaseVolume::Id:
return HandleDecreaseVolume(input_arguments);
default:
return Status::UnsupportedCommand;
}
}
DataModel::ActionReturnStatus AudioControlCluster::HandleMute()
{
Status s = mDelegate.HandleVolumeAndMuteChange(mVolume, true);
VerifyOrReturnError(s == Status::Success, s);
if (SetAttributeValue(mSoftMuted, true, SoftMuted::Id))
{
StoreSoftMuted();
}
return Status::Success;
}
DataModel::ActionReturnStatus AudioControlCluster::HandleUnmute()
{
Status s = mDelegate.HandleVolumeAndMuteChange(mVolume, false);
VerifyOrReturnError(s == Status::Success, s);
if (SetAttributeValue(mSoftMuted, false, SoftMuted::Id))
{
StoreSoftMuted();
}
return Status::Success;
}
DataModel::ActionReturnStatus AudioControlCluster::HandleToggleMuted()
{
const bool newSoftMuted = !mSoftMuted;
Status s = mDelegate.HandleVolumeAndMuteChange(mVolume, newSoftMuted);
VerifyOrReturnError(s == Status::Success, s);
if (SetAttributeValue(mSoftMuted, newSoftMuted, SoftMuted::Id))
{
StoreSoftMuted();
}
return Status::Success;
}
DataModel::ActionReturnStatus AudioControlCluster::HandleSetVolume(chip::TLV::TLVReader & input_arguments)
{
SetVolume::DecodableType req;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, req));
if (req.unmutePolicy.HasValue())
{
VerifyOrReturnError(req.unmutePolicy.Value() < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError);
}
const uint16_t effectiveMax = EffectiveMaxVolume();
// Constraint: 0 OR [MinDeviceVolume, EffectiveMax]
if (req.newVolume != 0 && (req.newVolume < mMinDeviceVolume || req.newVolume > effectiveMax))
{
return Status::ConstraintError;
}
// NewVolume == 0: mute and succeed.
if (req.newVolume == 0)
{
Status s = mDelegate.HandleVolumeAndMuteChange(mVolume, true);
VerifyOrReturnError(s == Status::Success, s);
if (SetAttributeValue(mSoftMuted, true, SoftMuted::Id))
{
StoreSoftMuted();
}
return Status::Success;
}
// Fast path: device is not muted.
if (!mSoftMuted && !mPhysicallyMuted)
{
Status s = mDelegate.HandleVolumeAndMuteChange(req.newVolume, false);
VerifyOrReturnError(s == Status::Success, s);
if (SetAttributeValue(mVolume, req.newVolume, Volume::Id))
{
StoreVolume();
}
return Status::Success;
}
return ApplyUnmutePolicy(req.unmutePolicy.ValueOr(mSetVolumeUnmutePolicy), req.newVolume);
}
DataModel::ActionReturnStatus AudioControlCluster::HandleIncreaseVolume(chip::TLV::TLVReader & input_arguments)
{
IncreaseVolume::DecodableType req;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, req));
if (req.unmutePolicy.HasValue())
{
VerifyOrReturnError(req.unmutePolicy.Value() < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError);
}
if (req.unmuteVolume.HasValue())
{
VerifyOrReturnError(req.unmuteVolume.Value() < UnmuteVolumeEnum::kUnknownEnumValue, Status::ConstraintError);
}
const uint16_t effectiveMax = EffectiveMaxVolume();
const uint16_t stepSize = req.stepSize.ValueOr(mDefaultStepSize);
// StepSize is clamped against the volume range below rather than rejected here, so a
// previously-valid DefaultStepSize doesn't start failing IncreaseVolume calls after
// MaxUserVolume shrinks.
VerifyOrReturnError(stepSize >= 1, Status::ConstraintError);
const UnmutePolicyEnum policy = req.unmutePolicy.ValueOr(mIncreaseVolumeUnmutePolicy);
const UnmuteVolumeEnum unmuteVolume = req.unmuteVolume.ValueOr(mIncreaseVolumeUnmuteVolume);
uint16_t requestedVolume;
// Step 2.a: special case for soft-muted-only device with unmute policy
if (mSoftMuted && !mPhysicallyMuted &&
(policy == UnmutePolicyEnum::kUnmuteOrChangeVolume || policy == UnmutePolicyEnum::kUnmuteOrDoNotChangeVolume))
{
switch (unmuteVolume)
{
case UnmuteVolumeEnum::kMinDeviceVolume:
requestedVolume = mMinDeviceVolume;
break;
case UnmuteVolumeEnum::kVolume:
requestedVolume = mVolume;
break;
case UnmuteVolumeEnum::kVolumePlusStepSize:
requestedVolume = AddCappedAtMax(mVolume, stepSize, effectiveMax);
break;
default:
return Status::ConstraintError;
}
}
else
{
// Step 2.b: Volume + StepSize, capped at EffectiveMax
requestedVolume = AddCappedAtMax(mVolume, stepSize, effectiveMax);
}
// Step 3: fast path — device is not muted
if (!mSoftMuted && !mPhysicallyMuted)
{
Status s = mDelegate.HandleVolumeAndMuteChange(requestedVolume, false);
VerifyOrReturnError(s == Status::Success, s);
if (SetAttributeValue(mVolume, requestedVolume, Volume::Id))
{
StoreVolume();
}
return Status::Success;
}
// Step 4: apply UnmutePolicy table
return ApplyUnmutePolicy(policy, requestedVolume);
}
DataModel::ActionReturnStatus AudioControlCluster::HandleDecreaseVolume(chip::TLV::TLVReader & input_arguments)
{
DecreaseVolume::DecodableType req;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, req));
if (req.unmutePolicy.HasValue())
{
VerifyOrReturnError(req.unmutePolicy.Value() < UnmutePolicyEnum::kUnknownEnumValue, Status::ConstraintError);
}
const uint16_t stepSize = req.stepSize.ValueOr(mDefaultStepSize);
// StepSize is clamped against the volume range below rather than rejected here, so a
// previously-valid DefaultStepSize doesn't start failing DecreaseVolume calls after
// MaxUserVolume shrinks.
VerifyOrReturnError(stepSize >= 1, Status::ConstraintError);
// RequestedVolume = Volume - StepSize (with underflow protection)
const uint32_t requestedVolume32 = static_cast<uint32_t>(mVolume) > static_cast<uint32_t>(stepSize)
? static_cast<uint32_t>(mVolume) - static_cast<uint32_t>(stepSize)
: 0;
// Step 3: floor at MinDeviceVolume → mute and succeed
if (requestedVolume32 < static_cast<uint32_t>(mMinDeviceVolume))
{
Status s = mDelegate.HandleVolumeAndMuteChange(mMinDeviceVolume, true);
VerifyOrReturnError(s == Status::Success, s);
if (SetAttributeValue(mVolume, mMinDeviceVolume, Volume::Id))
{
StoreVolume();
}
if (SetAttributeValue(mSoftMuted, true, SoftMuted::Id))
{
StoreSoftMuted();
}
return Status::Success;
}
const uint16_t requestedVolume = static_cast<uint16_t>(requestedVolume32);
// Step 4: fast path — device is not muted
if (!mSoftMuted && !mPhysicallyMuted)
{
Status s = mDelegate.HandleVolumeAndMuteChange(requestedVolume, false);
VerifyOrReturnError(s == Status::Success, s);
if (SetAttributeValue(mVolume, requestedVolume, Volume::Id))
{
StoreVolume();
}
return Status::Success;
}
// Step 5: apply UnmutePolicy table
return ApplyUnmutePolicy(req.unmutePolicy.ValueOr(mDecreaseVolumeUnmutePolicy), requestedVolume);
}
// Application-facing setters. SetSoftMuted/SetVolume/SetPhysicallyMuted reflect hardware-initiated
// changes and do NOT call the delegate. SetMaxUserVolume is app-initiated and DOES call
// HandleVolumeAndMuteChange when Volume must be clipped to the new ceiling.
CHIP_ERROR AudioControlCluster::SetSoftMuted(bool softMuted)
{
if (SetAttributeValue(mSoftMuted, softMuted, SoftMuted::Id))
{
StoreSoftMuted();
}
return CHIP_NO_ERROR;
}
CHIP_ERROR AudioControlCluster::SetPhysicallyMuted(bool physicallyMuted)
{
VerifyOrReturnError(mOptionalAttributeSet.IsSet(PhysicallyMuted::Id), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
SetAttributeValue(mPhysicallyMuted, physicallyMuted, PhysicallyMuted::Id);
return CHIP_NO_ERROR;
}
CHIP_ERROR AudioControlCluster::SetVolume(uint16_t volume)
{
VerifyOrReturnError(volume >= mMinDeviceVolume && volume <= EffectiveMaxVolume(), CHIP_IM_GLOBAL_STATUS(ConstraintError));
if (SetAttributeValue(mVolume, volume, Volume::Id))
{
StoreVolume();
}
return CHIP_NO_ERROR;
}
CHIP_ERROR AudioControlCluster::SetMaxUserVolume(uint16_t maxUserVolume)
{
VerifyOrReturnError(mOptionalAttributeSet.IsSet(MaxUserVolume::Id), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
VerifyOrReturnError(maxUserVolume >= mMinDeviceVolume && maxUserVolume <= mMaxDeviceVolume,
CHIP_IM_GLOBAL_STATUS(ConstraintError));
// If Volume must be clipped, inform hardware before committing any state change
// so that a delegate failure leaves the cluster in a consistent state.
if (mVolume > maxUserVolume)
{
Status s = mDelegate.HandleVolumeAndMuteChange(maxUserVolume, mSoftMuted);
ReturnErrorOnFailure(StatusIB(s).ToChipError());
}
if (SetAttributeValue(mMaxUserVolume, maxUserVolume, MaxUserVolume::Id))
{
StoreMaxUserVolume();
}
if (mVolume > maxUserVolume)
{
SetAttributeValue(mVolume, maxUserVolume, Volume::Id);
StoreVolume();
}
if (mOptionalAttributeSet.IsSet(StartUpVolume::Id) && !mStartUpVolume.IsNull() && mStartUpVolume.Value() > maxUserVolume)
{
SetAttributeValue(mStartUpVolume, DataModel::MakeNullable<uint16_t>(maxUserVolume), StartUpVolume::Id);
StoreStartUpVolume();
}
return CHIP_NO_ERROR;
}
CHIP_ERROR AudioControlCluster::SetBass(int16_t bass)
{
VerifyOrReturnError(mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Bass::Id),
CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
VerifyOrReturnError(bass >= mMinCorrection && bass <= mMaxCorrection, CHIP_IM_GLOBAL_STATUS(ConstraintError));
if (SetAttributeValue(mBass, bass, Bass::Id))
{
StoreBass();
}
return CHIP_NO_ERROR;
}
CHIP_ERROR AudioControlCluster::SetMid(int16_t mid)
{
VerifyOrReturnError(mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Mid::Id),
CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
VerifyOrReturnError(mid >= mMinCorrection && mid <= mMaxCorrection, CHIP_IM_GLOBAL_STATUS(ConstraintError));
if (SetAttributeValue(mMid, mid, Mid::Id))
{
StoreMid();
}
return CHIP_NO_ERROR;
}
CHIP_ERROR AudioControlCluster::SetTreble(int16_t treble)
{
VerifyOrReturnError(mFeatures.Has(Feature::kBasicEqualizer) && mOptionalAttributeSet.IsSet(Treble::Id),
CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
VerifyOrReturnError(treble >= mMinCorrection && treble <= mMaxCorrection, CHIP_IM_GLOBAL_STATUS(ConstraintError));
if (SetAttributeValue(mTreble, treble, Treble::Id))
{
StoreTreble();
}
return CHIP_NO_ERROR;
}
// ---------------------------------------------------------------------------------------
// scenes::SceneHandler
// ---------------------------------------------------------------------------------------
bool AudioControlCluster::SupportsCluster(EndpointId endpoint, ClusterId cluster)
{
return (cluster == AudioControl::Id) && (endpoint == mPath.mEndpointId);
}
CHIP_ERROR AudioControlCluster::SerializeSave(EndpointId endpoint, ClusterId cluster, MutableByteSpan & serializedBytes)
{
using AttributeValuePair = ScenesManagement::Structs::AttributeValuePairStruct::Type;
VerifyOrReturnError(SupportsCluster(endpoint, cluster), CHIP_ERROR_INVALID_ARGUMENT);
// SoftMuted and Volume are mandatory scene-able attributes; Bass/Mid/Treble are added only
// when the BasicEqualizer feature and the matching band attribute are enabled.
AttributeValuePair pairs[5];
size_t count = 0;
pairs[count].attributeID = SoftMuted::Id;
pairs[count].valueUnsigned8.SetValue(static_cast<uint8_t>(mSoftMuted));
count++;
pairs[count].attributeID = Volume::Id;
pairs[count].valueUnsigned16.SetValue(mVolume);
count++;
if (mFeatures.Has(Feature::kBasicEqualizer))
{
if (mOptionalAttributeSet.IsSet(Bass::Id))
{
pairs[count].attributeID = Bass::Id;
pairs[count].valueSigned16.SetValue(mBass);
count++;
}
if (mOptionalAttributeSet.IsSet(Mid::Id))
{
pairs[count].attributeID = Mid::Id;
pairs[count].valueSigned16.SetValue(mMid);
count++;
}
if (mOptionalAttributeSet.IsSet(Treble::Id))
{
pairs[count].attributeID = Treble::Id;
pairs[count].valueSigned16.SetValue(mTreble);
count++;
}
}
app::DataModel::List<AttributeValuePair> attributeValueList(pairs, count);
return EncodeAttributeValueList(attributeValueList, serializedBytes);
}
CHIP_ERROR AudioControlCluster::ApplyScene(EndpointId endpoint, ClusterId cluster, const ByteSpan & serializedBytes,
scenes::TransitionTimeMs timeMs)
{
// Transition time is intentionally ignored: the stored values are applied immediately, as
// there is no meaningful hardware ramp for mute or tone controls.
(void) timeMs;
VerifyOrReturnError(SupportsCluster(endpoint, cluster), CHIP_ERROR_INVALID_ARGUMENT);
app::DataModel::DecodableList<ScenesManagement::Structs::AttributeValuePairStruct::DecodableType> attributeValueList;
ReturnErrorOnFailure(DecodeAttributeValueList(serializedBytes, attributeValueList));
// Pass 1: decode each pair into an optional, so an extension field set carrying only a subset
// of the attributes leaves the rest untouched. Values outside the current constraints are
// saturated to the closest legal value (Scenes spec: a non-boolean non-nullable attribute
// takes the valid value closest to the one provided), so a scene stays recallable after
// MaxUserVolume or the correction range shrinks. Bands not enabled on this instance are
// skipped - the attribute does not exist here, and the shared scene validator is stateless so
// it cannot make that per-instance check at AddScene time.
const bool equalizer = mFeatures.Has(Feature::kBasicEqualizer);
std::optional<uint16_t> volume;
std::optional<bool> softMuted;
std::optional<int16_t> bass, mid, treble;
auto pairIterator = attributeValueList.begin();
while (pairIterator.Next())
{
const auto & pair = pairIterator.GetValue();
switch (pair.attributeID)
{
case SoftMuted::Id:
VerifyOrReturnError(pair.valueUnsigned8.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
softMuted = pair.valueUnsigned8.Value() != 0;
break;
case Volume::Id:
VerifyOrReturnError(pair.valueUnsigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
volume = std::clamp(pair.valueUnsigned16.Value(), mMinDeviceVolume, EffectiveMaxVolume());
break;
case Bass::Id:
VerifyOrReturnError(pair.valueSigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
if (equalizer && mOptionalAttributeSet.IsSet(Bass::Id))
{
bass = std::clamp(pair.valueSigned16.Value(), mMinCorrection, mMaxCorrection);
}
break;
case Mid::Id:
VerifyOrReturnError(pair.valueSigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
if (equalizer && mOptionalAttributeSet.IsSet(Mid::Id))
{
mid = std::clamp(pair.valueSigned16.Value(), mMinCorrection, mMaxCorrection);
}
break;
case Treble::Id:
VerifyOrReturnError(pair.valueSigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT);
if (equalizer && mOptionalAttributeSet.IsSet(Treble::Id))
{
treble = std::clamp(pair.valueSigned16.Value(), mMinCorrection, mMaxCorrection);
}
break;
default:
// Per the Scenes Management cluster (AttributeValuePairStruct), a pair referencing an
// attribute that is not implemented on the endpoint is ignored rather than failing
// the recall.
break;
}
}
ReturnErrorOnFailure(pairIterator.GetStatus());
// Pass 2: notify the delegate before committing, matching the command handlers and
// SetMaxUserVolume, so a rejection leaves the cluster unchanged. Volume and SoftMuted are one
// atomic call (the absent side taken from current state), made only when the scene carries at
// least one of them; the equalizer bands mirror the write path (guarded by an actual-change
// check). The bands are committed after Volume/SoftMuted, so a band delegate that rejects
// leaves the recall partially applied - logged below so that is diagnosable.
if (volume.has_value() || softMuted.has_value())
{
const uint16_t newVolume = volume.value_or(mVolume);
const bool newSoftMuted = softMuted.value_or(mSoftMuted);
Status s = mDelegate.HandleVolumeAndMuteChange(newVolume, newSoftMuted);
VerifyOrReturnError(s == Status::Success, StatusIB(s).ToChipError());
if (SetAttributeValue(mVolume, newVolume, Volume::Id))
{
StoreVolume();
}
if (SetAttributeValue(mSoftMuted, newSoftMuted, SoftMuted::Id))
{
StoreSoftMuted();
}
}
if (bass.has_value() && *bass != mBass)
{
Status s = mDelegate.HandleBassChanged(*bass);
VerifyOrReturnError(s == Status::Success, StatusIB(s).ToChipError(),
ChipLogError(Zcl, "AudioControl scene recall: Bass rejected; recall partially applied"));
SetAttributeValue(mBass, *bass, Bass::Id);
StoreBass();
}
if (mid.has_value() && *mid != mMid)
{
Status s = mDelegate.HandleMidChanged(*mid);
VerifyOrReturnError(s == Status::Success, StatusIB(s).ToChipError(),
ChipLogError(Zcl, "AudioControl scene recall: Mid rejected; recall partially applied"));
SetAttributeValue(mMid, *mid, Mid::Id);
StoreMid();
}
if (treble.has_value() && *treble != mTreble)
{
Status s = mDelegate.HandleTrebleChanged(*treble);
VerifyOrReturnError(s == Status::Success, StatusIB(s).ToChipError(),
ChipLogError(Zcl, "AudioControl scene recall: Treble rejected; recall partially applied"));
SetAttributeValue(mTreble, *treble, Treble::Id);
StoreTreble();
}
return CHIP_NO_ERROR;
}
} // namespace chip::app::Clusters