| /** |
| * Copyright (c) 2023-2025 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/boolean-state-configuration-server/BooleanStateConfigurationCluster.h> |
| |
| #include <app/SafeAttributePersistenceProvider.h> |
| #include <app/data-model/Decode.h> |
| #include <app/persistence/AttributePersistence.h> |
| #include <app/server-cluster/AttributeListBuilder.h> |
| #include <clusters/BooleanStateConfiguration/AttributeIds.h> |
| #include <clusters/BooleanStateConfiguration/Commands.h> |
| #include <clusters/BooleanStateConfiguration/Events.h> |
| #include <clusters/BooleanStateConfiguration/Metadata.h> |
| #include <lib/core/CHIPError.h> |
| #include <lib/support/CodeUtils.h> |
| |
| #include <algorithm> |
| |
| using namespace chip::app::DataModel; |
| using namespace chip::app::Clusters::BooleanStateConfiguration; |
| using namespace chip::app::Clusters::BooleanStateConfiguration::Attributes; |
| using namespace chip::Protocols::InteractionModel; |
| |
| namespace chip::app::Clusters { |
| |
| constexpr uint8_t kAllKnownAlarmModes = |
| static_cast<uint8_t>(AlarmModeBitmap::kAudible) | static_cast<uint8_t>(AlarmModeBitmap::kVisual); |
| |
| BooleanStateConfigurationCluster::BooleanStateConfigurationCluster(EndpointId endpointId, |
| BitMask<BooleanStateConfiguration::Feature> features, |
| OptionalAttributesSet optionalAttributes, |
| const StartupConfiguration & config) : |
| DefaultServerCluster({ endpointId, BooleanStateConfiguration::Id }), |
| mFeatures(features), mOptionalAttributes([&features, &optionalAttributes]() -> FullOptionalAttributesSet { |
| // constructs the attribute set, that once constructed stays const |
| AttributeSet enabledOptionalAttributes; |
| |
| if (features.Has(Feature::kSensitivityLevel)) |
| { |
| enabledOptionalAttributes.ForceSet<CurrentSensitivityLevel::Id>(); |
| enabledOptionalAttributes.ForceSet<SupportedSensitivityLevels::Id>(); |
| if (optionalAttributes.IsSet(DefaultSensitivityLevel::Id)) |
| { |
| enabledOptionalAttributes.ForceSet<DefaultSensitivityLevel::Id>(); |
| } |
| } |
| |
| if (features.Has(Feature::kVisual) || features.Has(Feature::kAudible)) |
| { |
| enabledOptionalAttributes.ForceSet<AlarmsActive::Id>(); |
| enabledOptionalAttributes.ForceSet<AlarmsSupported::Id>(); |
| |
| if (optionalAttributes.IsSet(AlarmsEnabled::Id)) |
| { |
| enabledOptionalAttributes.ForceSet<AlarmsEnabled::Id>(); |
| } |
| } |
| |
| if (features.Has(Feature::kAlarmSuppress)) |
| { |
| enabledOptionalAttributes.ForceSet<AlarmsSuppressed::Id>(); |
| } |
| |
| if (optionalAttributes.IsSet(SensorFault::Id)) |
| { |
| enabledOptionalAttributes.ForceSet<SensorFault::Id>(); |
| } |
| |
| return enabledOptionalAttributes; |
| }()), |
| mSupportedSensitivityLevels( |
| std::clamp(config.supportedSensitivityLevels, kMinSupportedSensitivityLevels, kMaxSupportedSensitivityLevels)), |
| mDefaultSensitivityLevel(std::min(config.defaultSensitivityLevel, static_cast<uint8_t>(mSupportedSensitivityLevels - 1))), |
| mAlarmsSupported(config.alarmsSupported) |
| {} |
| |
| CHIP_ERROR BooleanStateConfigurationCluster::Startup(ServerClusterContext & context) |
| { |
| ReturnErrorOnFailure(DefaultServerCluster::Startup(context)); |
| |
| if (GetSafeAttributePersistenceProvider()->ReadScalarValue({ mPath.mEndpointId, mPath.mClusterId, CurrentSensitivityLevel::Id }, |
| mCurrentSensitivityLevel) != CHIP_NO_ERROR) |
| { |
| mCurrentSensitivityLevel = mDefaultSensitivityLevel; |
| } |
| |
| if (mCurrentSensitivityLevel >= mSupportedSensitivityLevels) |
| { |
| mCurrentSensitivityLevel = mSupportedSensitivityLevels - 1; |
| } |
| |
| // alarms enabled persistence was handled by ember previously (as opposed to AAI usage of sensitivity level) |
| // TODO: this is VERY inconvenient/strange and we should really fix this inconsistence |
| AttributePersistence attributePersistence(context.attributeStorage); |
| AlarmModeBitMask::IntegerType alarmsEnabled; |
| attributePersistence.LoadNativeEndianValue({ mPath.mEndpointId, mPath.mClusterId, AlarmsEnabled::Id }, alarmsEnabled, |
| AlarmModeBitMask::IntegerType(0)); |
| mAlarmsEnabled = AlarmModeBitMask(alarmsEnabled); |
| |
| // internal state validation: |
| if (mFeatures.Has(Feature::kAlarmSuppress)) |
| { |
| // alarm Suppression requires visual/audible alarms |
| VerifyOrReturnError(mFeatures.HasAny(Feature::kAudible, Feature::kVisual), CHIP_ERROR_INCORRECT_STATE); |
| } |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR BooleanStateConfigurationCluster::AcceptedCommands(const ConcreteClusterPath & path, |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder) |
| { |
| |
| if (mFeatures.HasAny(Feature::kAudible, Feature::kVisual)) |
| { |
| ReturnErrorOnFailure(builder.AppendElements({ Commands::EnableDisableAlarm::kMetadataEntry })); |
| } |
| |
| if (mFeatures.Has(Feature::kAlarmSuppress)) |
| { |
| ReturnErrorOnFailure(builder.AppendElements({ Commands::SuppressAlarm::kMetadataEntry })); |
| } |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| std::optional<DataModel::ActionReturnStatus> |
| BooleanStateConfigurationCluster::InvokeCommand(const DataModel::InvokeRequest & request, chip::TLV::TLVReader & input_arguments, |
| CommandHandler * handler) |
| { |
| switch (request.path.mCommandId) |
| { |
| case Commands::SuppressAlarm::Id: { |
| Commands::SuppressAlarm::DecodableType request_data; |
| ReturnErrorOnFailure(request_data.Decode(input_arguments)); |
| |
| return SuppressAlarms(request_data.alarmsToSuppress); |
| } |
| case Commands::EnableDisableAlarm::Id: { |
| Commands::EnableDisableAlarm::DecodableType request_data; |
| ReturnErrorOnFailure(request_data.Decode(input_arguments)); |
| |
| const auto alarms = request_data.alarmsToEnableDisable; |
| |
| VerifyOrReturnError(mAlarmsSupported.HasAll(alarms), Status::ConstraintError); |
| |
| if (mAlarmsEnabled != alarms) |
| { |
| mAlarmsEnabled = alarms; |
| |
| AlarmModeBitMask::IntegerType rawAlarmsEnabled = mAlarmsEnabled.Raw(); |
| if (CHIP_ERROR err = mContext->attributeStorage.WriteValue({ mPath.mEndpointId, mPath.mClusterId, AlarmsEnabled::Id }, |
| { &rawAlarmsEnabled, sizeof(rawAlarmsEnabled) }); |
| err != CHIP_NO_ERROR) |
| { |
| ChipLogError(DataManagement, "Failed to persist alarms enabled: %" CHIP_ERROR_FORMAT, err.Format()); |
| } |
| OnClusterAttributeChanged(AlarmsEnabled::Id); |
| } |
| |
| if (mDelegate != nullptr) |
| { |
| // TODO: For backwards compatibility with previous code, we ignore the return code |
| // from the delegate handler. This feels off though... |
| TEMPORARY_RETURN_IGNORED mDelegate->HandleEnableDisableAlarms(alarms); |
| } |
| |
| // This inverts the bits (0x03 is the current max bitmap): |
| // - every "known" bit that is set to 0 in the request will be set to 1 in the `alarmsToDisable` |
| const BitMask<BooleanStateConfiguration::AlarmModeBitmap> alarmsToDisable{ static_cast<uint8_t>(~alarms.Raw() & |
| kAllKnownAlarmModes) }; |
| |
| bool generateEvent = false; |
| if (mAlarmsActive.HasAny(alarmsToDisable)) |
| { |
| mAlarmsActive.Clear(alarmsToDisable); |
| OnClusterAttributeChanged(AlarmsActive::Id); |
| generateEvent = true; |
| } |
| if (mAlarmsSuppressed.HasAny(alarmsToDisable)) |
| { |
| mAlarmsSuppressed.Clear(alarmsToDisable); |
| OnClusterAttributeChanged(AlarmsSuppressed::Id); |
| generateEvent = true; |
| } |
| |
| if (generateEvent) |
| { |
| GenerateAlarmsStateChangedEvent(); |
| } |
| return Status::Success; |
| } |
| default: |
| return Protocols::InteractionModel::Status::UnsupportedCommand; |
| } |
| } |
| |
| ActionReturnStatus BooleanStateConfigurationCluster::ReadAttribute(const ReadAttributeRequest & request, |
| AttributeValueEncoder & encoder) |
| { |
| switch (request.path.mAttributeId) |
| { |
| case ClusterRevision::Id: |
| return encoder.Encode(BooleanStateConfiguration::kRevision); |
| case FeatureMap::Id: |
| return encoder.Encode(mFeatures); |
| case CurrentSensitivityLevel::Id: |
| return encoder.Encode(mCurrentSensitivityLevel); |
| case SupportedSensitivityLevels::Id: |
| return encoder.Encode(mSupportedSensitivityLevels); |
| case DefaultSensitivityLevel::Id: |
| return encoder.Encode(mDefaultSensitivityLevel); |
| case AlarmsActive::Id: |
| return encoder.Encode(mAlarmsActive); |
| case AlarmsSuppressed::Id: |
| return encoder.Encode(mAlarmsSuppressed); |
| case AlarmsEnabled::Id: |
| return encoder.Encode(mAlarmsEnabled); |
| case AlarmsSupported::Id: |
| return encoder.Encode(mAlarmsSupported); |
| case SensorFault::Id: |
| return encoder.Encode(mSensorFault); |
| default: |
| return Protocols::InteractionModel::Status::UnsupportedAttribute; |
| } |
| } |
| |
| ActionReturnStatus BooleanStateConfigurationCluster::WriteAttribute(const WriteAttributeRequest & request, |
| AttributeValueDecoder & decoder) |
| { |
| switch (request.path.mAttributeId) |
| { |
| case CurrentSensitivityLevel::Id: { |
| uint8_t value; |
| ReturnErrorOnFailure(decoder.Decode(value)); |
| return SetCurrentSensitivityLevel(value); |
| } |
| default: |
| return Protocols::InteractionModel::Status::UnsupportedWrite; |
| } |
| } |
| |
| CHIP_ERROR BooleanStateConfigurationCluster::Attributes(const ConcreteClusterPath & path, |
| ReadOnlyBufferBuilder<AttributeEntry> & builder) |
| { |
| constexpr AttributeEntry optionalAttributesMeta[] = { |
| CurrentSensitivityLevel::kMetadataEntry, // |
| SupportedSensitivityLevels::kMetadataEntry, // |
| DefaultSensitivityLevel::kMetadataEntry, // |
| AlarmsActive::kMetadataEntry, // |
| AlarmsSuppressed::kMetadataEntry, // |
| AlarmsEnabled::kMetadataEntry, // |
| AlarmsSupported::kMetadataEntry, // |
| SensorFault::kMetadataEntry, // |
| }; |
| |
| AttributeListBuilder listBuilder(builder); |
| return listBuilder.Append(Span(kMandatoryMetadata), Span<const AttributeEntry>{ optionalAttributesMeta }, mOptionalAttributes); |
| } |
| |
| void BooleanStateConfigurationCluster::GenerateAlarmsStateChangedEvent() |
| { |
| VerifyOrReturn(mContext != nullptr); |
| VerifyOrReturn(mFeatures.HasAny(Feature::kAudible, Feature::kVisual)); |
| |
| BooleanStateConfiguration::Events::AlarmsStateChanged::Type event; |
| event.alarmsActive = mAlarmsActive; |
| |
| if (mFeatures.Has(Feature::kAlarmSuppress)) |
| { |
| event.alarmsSuppressed.SetValue(mAlarmsSuppressed); |
| } |
| mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId); |
| } |
| |
| void BooleanStateConfigurationCluster::GenerateSensorFault(SensorFaultBitMask fault) |
| { |
| VerifyOrReturn(mContext != nullptr); |
| |
| BooleanStateConfiguration::Events::SensorFault::Type event; |
| event.sensorFault = fault; |
| mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId); |
| |
| if (mOptionalAttributes.IsSet(SensorFault::Id) && (mSensorFault != fault)) |
| { |
| mSensorFault = fault; |
| OnClusterAttributeChanged(SensorFault::Id); |
| } |
| } |
| |
| CHIP_ERROR BooleanStateConfigurationCluster::SetCurrentSensitivityLevel(uint8_t level) |
| { |
| VerifyOrReturnError(level < mSupportedSensitivityLevels, CHIP_IM_GLOBAL_STATUS(ConstraintError)); |
| VerifyOrReturnError(mCurrentSensitivityLevel != level, CHIP_NO_ERROR); |
| |
| mCurrentSensitivityLevel = level; |
| OnClusterAttributeChanged(CurrentSensitivityLevel::Id); |
| |
| // TODO: we should migrate this to not use `Safe` attribute persistence and use |
| // a common persistence layer. |
| return GetSafeAttributePersistenceProvider()->WriteScalarValue( |
| { mPath.mEndpointId, mPath.mClusterId, CurrentSensitivityLevel::Id }, level); |
| } |
| |
| void BooleanStateConfigurationCluster::OnClusterAttributeChanged(AttributeId attributeId) |
| { |
| NotifyAttributeChanged(attributeId); |
| if (mDelegate != nullptr) |
| { |
| mDelegate->OnAttributeChanged(attributeId, this); |
| } |
| } |
| |
| Status BooleanStateConfigurationCluster::SetAlarmsActive(AlarmModeBitMask alarms) |
| { |
| VerifyOrReturnError(mFeatures.HasAny(Feature::kAudible, Feature::kVisual), Status::Failure); |
| VerifyOrReturnError(mAlarmsEnabled.HasAll(alarms), Status::Failure); |
| |
| // No change is a noop |
| VerifyOrReturnError(mAlarmsActive != alarms, Status::Success); |
| |
| mAlarmsActive = alarms; |
| OnClusterAttributeChanged(AlarmsActive::Id); |
| GenerateAlarmsStateChangedEvent(); |
| |
| return Status::Success; |
| } |
| |
| Status BooleanStateConfigurationCluster::SetAllEnabledAlarmsActive() |
| { |
| VerifyOrReturnError(mFeatures.HasAny(Feature::kAudible, Feature::kVisual), Status::Failure); |
| |
| // No change is a noop |
| VerifyOrReturnError(mAlarmsActive != mAlarmsEnabled, Status::Success); |
| |
| mAlarmsActive = mAlarmsEnabled; |
| OnClusterAttributeChanged(AlarmsActive::Id); |
| GenerateAlarmsStateChangedEvent(); |
| return Status::Success; |
| } |
| |
| void BooleanStateConfigurationCluster::ClearAllAlarms() |
| { |
| VerifyOrReturn(mAlarmsActive.HasAny() || mAlarmsSuppressed.HasAny()); |
| |
| if (mAlarmsActive.HasAny()) |
| { |
| mAlarmsActive.ClearAll(); |
| OnClusterAttributeChanged(AlarmsActive::Id); |
| } |
| if (mAlarmsSuppressed.HasAny()) |
| { |
| mAlarmsSuppressed.ClearAll(); |
| OnClusterAttributeChanged(AlarmsSuppressed::Id); |
| } |
| |
| GenerateAlarmsStateChangedEvent(); |
| } |
| |
| Status BooleanStateConfigurationCluster::SuppressAlarms(AlarmModeBitMask alarms) |
| { |
| // Need SPRS feature and that is only available if [VIS | AUD]. These are all checked here. |
| VerifyOrReturnError(mFeatures.Has(Feature::kAlarmSuppress), Status::UnsupportedCommand); |
| VerifyOrReturnError(mFeatures.HasAny(Feature::kAudible, Feature::kVisual), Status::UnsupportedCommand); |
| |
| // can only suppress valid active alarms |
| VerifyOrReturnError(mAlarmsSupported.HasAll(alarms), Status::ConstraintError); |
| VerifyOrReturnError(mAlarmsActive.HasAll(alarms), Status::InvalidInState); |
| |
| // validate this is not a NOOP |
| VerifyOrReturnError(!mAlarmsSuppressed.HasAll(alarms), Status::Success); |
| |
| if (mDelegate != nullptr) |
| { |
| // TODO: To preserve original logic, we ignore error code from the |
| // delegate, however this feels off. |
| TEMPORARY_RETURN_IGNORED mDelegate->HandleSuppressAlarm(alarms); |
| } |
| |
| mAlarmsSuppressed.Set(alarms); |
| OnClusterAttributeChanged(AlarmsSuppressed::Id); |
| GenerateAlarmsStateChangedEvent(); |
| return Status::Success; |
| } |
| |
| } // namespace chip::app::Clusters |