| /* |
| * Copyright (c) 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 "clusters/BooleanStateConfiguration/Commands.h" |
| #include <pw_unit_test/framework.h> |
| |
| #include <app/DefaultSafeAttributePersistenceProvider.h> |
| #include <app/SafeAttributePersistenceProvider.h> |
| #include <app/clusters/boolean-state-configuration-server/BooleanStateConfigurationCluster.h> |
| #include <app/server-cluster/testing/AttributeTesting.h> |
| #include <app/server-cluster/testing/ClusterTester.h> |
| #include <app/server-cluster/testing/TestServerClusterContext.h> |
| #include <app/server-cluster/testing/ValidateGlobalAttributes.h> |
| #include <clusters/BooleanStateConfiguration/Enums.h> |
| #include <clusters/BooleanStateConfiguration/Metadata.h> |
| #include <lib/core/CHIPError.h> |
| #include <lib/core/DataModelTypes.h> |
| #include <lib/support/ReadOnlyBuffer.h> |
| |
| namespace { |
| |
| using namespace chip; |
| using namespace chip::app::Clusters; |
| using namespace chip::app::Clusters::BooleanStateConfiguration; |
| |
| using chip::app::ClusterShutdownType; |
| using chip::app::Clusters::BooleanStateConfiguration::Feature; |
| using chip::app::DataModel::AcceptedCommandEntry; |
| using chip::app::DataModel::AttributeEntry; |
| using chip::Testing::ClusterTester; |
| using chip::Testing::IsAcceptedCommandsListEqualTo; |
| using chip::Testing::IsAttributesListEqualTo; |
| using chip::Testing::TestServerClusterContext; |
| |
| // initialize memory as ReadOnlyBufferBuilder may allocate |
| struct TestBooleanStateConfigurationCluster : public ::testing::Test |
| { |
| static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } |
| static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } |
| }; |
| |
| constexpr EndpointId kTestEndpointId = 1; |
| |
| class StartupConfigurationBuilder |
| { |
| public: |
| StartupConfigurationBuilder() = default; |
| operator BooleanStateConfigurationCluster::StartupConfiguration() { return build(); } |
| |
| BooleanStateConfigurationCluster::StartupConfiguration build() |
| { |
| return { .supportedSensitivityLevels = mSupportedSensitivityLevels, |
| .defaultSensitivityLevel = mDefaultSensitivityLevel, |
| .alarmsSupported = mAlarmsSupported }; |
| } |
| |
| uint8_t DefaultSensitivityLevel() const { return mDefaultSensitivityLevel; } |
| uint8_t SupportedSensitivityLevels() const { return mSupportedSensitivityLevels; } |
| |
| StartupConfigurationBuilder & WithSupportedSensitivityLevels(uint8_t level) |
| { |
| mSupportedSensitivityLevels = level; |
| return *this; |
| } |
| StartupConfigurationBuilder & WithDefaultSensitivityLevel(uint8_t level) |
| { |
| mDefaultSensitivityLevel = level; |
| return *this; |
| } |
| |
| StartupConfigurationBuilder & AddAlarmsSupported(BooleanStateConfiguration::AlarmModeBitmap alarm) |
| { |
| mAlarmsSupported.Set(alarm); |
| return *this; |
| } |
| |
| private: |
| uint8_t mSupportedSensitivityLevels = 7; |
| uint8_t mDefaultSensitivityLevel = 3; |
| BooleanStateConfigurationCluster::AlarmModeBitMask mAlarmsSupported; |
| }; |
| |
| StartupConfigurationBuilder DefaultConfig() |
| { |
| return {}; |
| } |
| |
| class ScopedSafeAttributePersistence |
| { |
| public: |
| ScopedSafeAttributePersistence(TestServerClusterContext & context) : mOldPersistence(app::GetSafeAttributePersistenceProvider()) |
| { |
| VerifyOrDie(mPersistence.Init(&context.StorageDelegate()) == CHIP_NO_ERROR); |
| app::SetSafeAttributePersistenceProvider(&mPersistence); |
| } |
| ~ScopedSafeAttributePersistence() { app::SetSafeAttributePersistenceProvider(mOldPersistence); } |
| |
| private: |
| app::SafeAttributePersistenceProvider * mOldPersistence; |
| app::DefaultSafeAttributePersistenceProvider mPersistence; |
| }; |
| |
| TEST_F(TestBooleanStateConfigurationCluster, TestAttributeList) |
| { |
| // cluster without any attributes |
| { |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, {}, {}, DefaultConfig()); |
| |
| ASSERT_TRUE(IsAttributesListEqualTo(cluster, {})); |
| } |
| |
| // cluster supporting some things |
| { |
| BooleanStateConfigurationCluster cluster( |
| kTestEndpointId, { Feature::kSensitivityLevel, Feature::kAudible }, |
| { BooleanStateConfigurationCluster::OptionalAttributesSet().Set<Attributes::SensorFault::Id>() }, DefaultConfig()); |
| |
| ASSERT_TRUE(IsAttributesListEqualTo(cluster, |
| { |
| Attributes::CurrentSensitivityLevel::kMetadataEntry, |
| Attributes::SupportedSensitivityLevels::kMetadataEntry, |
| Attributes::AlarmsActive::kMetadataEntry, |
| Attributes::AlarmsSupported::kMetadataEntry, |
| Attributes::SensorFault::kMetadataEntry, |
| })); |
| } |
| // cluster supporting only visual alarms |
| { |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, BitMask<Feature>(Feature::kVisual), {}, DefaultConfig()); |
| ASSERT_TRUE(IsAttributesListEqualTo(cluster, |
| { |
| Attributes::AlarmsActive::kMetadataEntry, |
| Attributes::AlarmsSupported::kMetadataEntry, |
| })); |
| } |
| // cluster supporting alarm suppression (but not visual or audible) |
| // This is not a valid configuration, but we should handle it gracefully |
| { |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, BitMask<Feature>(Feature::kAlarmSuppress), {}, DefaultConfig()); |
| ASSERT_TRUE(IsAttributesListEqualTo(cluster, |
| { |
| Attributes::AlarmsSuppressed::kMetadataEntry, |
| })); |
| } |
| // cluster supporting visual alarms and alarm suppression |
| { |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, { Feature::kVisual, Feature::kAlarmSuppress }, {}, |
| DefaultConfig()); |
| ASSERT_TRUE(IsAttributesListEqualTo(cluster, |
| { |
| Attributes::AlarmsActive::kMetadataEntry, |
| Attributes::AlarmsSuppressed::kMetadataEntry, |
| Attributes::AlarmsSupported::kMetadataEntry, |
| })); |
| } |
| // cluster supporting all features and optional attributes |
| { |
| BooleanStateConfigurationCluster cluster( |
| kTestEndpointId, { Feature::kVisual, Feature::kAudible, Feature::kAlarmSuppress, Feature::kSensitivityLevel }, |
| { BooleanStateConfigurationCluster::OptionalAttributesSet() |
| .Set<Attributes::DefaultSensitivityLevel::Id>() |
| .Set<Attributes::AlarmsEnabled::Id>() |
| .Set<Attributes::SensorFault::Id>() }, |
| DefaultConfig()); |
| ASSERT_TRUE(IsAttributesListEqualTo(cluster, |
| { |
| Attributes::CurrentSensitivityLevel::kMetadataEntry, |
| Attributes::SupportedSensitivityLevels::kMetadataEntry, |
| Attributes::DefaultSensitivityLevel::kMetadataEntry, |
| Attributes::AlarmsActive::kMetadataEntry, |
| Attributes::AlarmsSuppressed::kMetadataEntry, |
| Attributes::AlarmsEnabled::kMetadataEntry, |
| Attributes::AlarmsSupported::kMetadataEntry, |
| Attributes::SensorFault::kMetadataEntry, |
| })); |
| } |
| } |
| |
| TEST_F(TestBooleanStateConfigurationCluster, TestAcceptedCommandList) |
| { |
| // cluster without any features |
| { |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, {}, {}, DefaultConfig()); |
| ASSERT_TRUE(IsAcceptedCommandsListEqualTo(cluster, {})); |
| } |
| // cluster supporting only visual alarms |
| { |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, BitMask<Feature>(Feature::kVisual), {}, DefaultConfig()); |
| ASSERT_TRUE(IsAcceptedCommandsListEqualTo(cluster, |
| { |
| Commands::EnableDisableAlarm::kMetadataEntry, |
| })); |
| } |
| // cluster supporting only audible alarms |
| { |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, BitMask<Feature>(Feature::kAudible), {}, DefaultConfig()); |
| ASSERT_TRUE(IsAcceptedCommandsListEqualTo(cluster, |
| { |
| Commands::EnableDisableAlarm::kMetadataEntry, |
| })); |
| } |
| // cluster supporting visual alarms and alarm suppression |
| { |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, { Feature::kVisual, Feature::kAlarmSuppress }, {}, |
| DefaultConfig()); |
| ASSERT_TRUE(IsAcceptedCommandsListEqualTo(cluster, |
| { |
| Commands::EnableDisableAlarm::kMetadataEntry, |
| Commands::SuppressAlarm::kMetadataEntry, |
| })); |
| } |
| // cluster supporting all features |
| { |
| BooleanStateConfigurationCluster cluster( |
| kTestEndpointId, { Feature::kVisual, Feature::kAudible, Feature::kAlarmSuppress, Feature::kSensitivityLevel }, {}, |
| DefaultConfig()); |
| ASSERT_TRUE(IsAcceptedCommandsListEqualTo(cluster, |
| { |
| Commands::EnableDisableAlarm::kMetadataEntry, |
| Commands::SuppressAlarm::kMetadataEntry, |
| })); |
| } |
| } |
| |
| TEST_F(TestBooleanStateConfigurationCluster, TestSensitivityClamping) |
| { |
| TestServerClusterContext context; |
| ScopedSafeAttributePersistence persistence(context); |
| |
| // supportedSensitivityLevels is clamped to [2, 10] |
| { |
| // Test value below min |
| auto config = DefaultConfig().WithSupportedSensitivityLevels(1); |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, Feature::kSensitivityLevel, {}, config); |
| ClusterTester tester(cluster); |
| |
| uint8_t supportedLevels = 0; |
| EXPECT_EQ(tester.ReadAttribute(Attributes::SupportedSensitivityLevels::Id, supportedLevels), |
| Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(supportedLevels, BooleanStateConfigurationCluster::kMinSupportedSensitivityLevels); |
| } |
| { |
| // Test value above max |
| auto config = DefaultConfig().WithSupportedSensitivityLevels(101); |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, Feature::kSensitivityLevel, {}, config); |
| ClusterTester tester(cluster); |
| |
| uint8_t supportedLevels = 0; |
| EXPECT_EQ(tester.ReadAttribute(Attributes::SupportedSensitivityLevels::Id, supportedLevels), |
| Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(supportedLevels, BooleanStateConfigurationCluster::kMaxSupportedSensitivityLevels); |
| } |
| |
| // defaultSensitivityLevel is clamped to supported-1 |
| { |
| auto config = DefaultConfig().WithDefaultSensitivityLevel(5).WithSupportedSensitivityLevels(5); |
| BooleanStateConfigurationCluster cluster( |
| kTestEndpointId, Feature::kSensitivityLevel, |
| BooleanStateConfigurationCluster::OptionalAttributesSet().Set<Attributes::DefaultSensitivityLevel::Id>(), config); |
| ClusterTester tester(cluster); |
| |
| uint8_t defaultLevel = 0; |
| EXPECT_EQ(tester.ReadAttribute(Attributes::DefaultSensitivityLevel::Id, defaultLevel), |
| Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(defaultLevel, 4); |
| } |
| |
| // Writing CurrentSensitivityLevel is clamped |
| { |
| auto config = DefaultConfig().WithSupportedSensitivityLevels(10); |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, Feature::kSensitivityLevel, {}, config); |
| ClusterTester tester(cluster); |
| ASSERT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| uint8_t currentLevel = 0; |
| |
| // Write a valid level |
| EXPECT_EQ(tester.WriteAttribute(Attributes::CurrentSensitivityLevel::Id, static_cast<uint8_t>(5)), |
| Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(tester.ReadAttribute(Attributes::CurrentSensitivityLevel::Id, currentLevel), |
| Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(currentLevel, 5); |
| |
| // Write an invalid level |
| EXPECT_EQ(tester.WriteAttribute(Attributes::CurrentSensitivityLevel::Id, static_cast<uint8_t>(10)), |
| Protocols::InteractionModel::Status::ConstraintError); |
| |
| // Value should not have changed |
| EXPECT_EQ(tester.ReadAttribute(Attributes::CurrentSensitivityLevel::Id, currentLevel), |
| Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(currentLevel, 5); |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| } |
| |
| TEST_F(TestBooleanStateConfigurationCluster, TestPersistenceAndStartup) |
| { |
| TestServerClusterContext context; |
| ScopedSafeAttributePersistence persistence(context); |
| |
| // 1. Create a cluster, write a value. |
| { |
| auto config = DefaultConfig().WithSupportedSensitivityLevels(9); |
| |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, Feature::kSensitivityLevel, {}, config); |
| ASSERT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| ClusterTester tester(cluster); |
| |
| // check default value first |
| uint8_t sensitivity = 0; |
| EXPECT_EQ(tester.ReadAttribute(Attributes::CurrentSensitivityLevel::Id, sensitivity), |
| Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(sensitivity, config.DefaultSensitivityLevel()); |
| |
| // Write a new value. This will be persisted. |
| uint8_t levelToWrite = 6; |
| EXPECT_EQ(tester.WriteAttribute(Attributes::CurrentSensitivityLevel::Id, levelToWrite), |
| Protocols::InteractionModel::Status::Success); |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // 2. Create a new cluster instance with the same context, and check if the value was restored. |
| { |
| auto config = DefaultConfig().WithSupportedSensitivityLevels(9); |
| |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, Feature::kSensitivityLevel, {}, config); |
| ASSERT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| ClusterTester tester(cluster); |
| uint8_t sensitivity = 0; |
| EXPECT_EQ(tester.ReadAttribute(Attributes::CurrentSensitivityLevel::Id, sensitivity), |
| Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(sensitivity, 6); // Check if value is persisted. |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // 3. Create another new cluster with a smaller supported range and check clamping on startup. |
| { |
| auto smallerConfig = DefaultConfig().WithSupportedSensitivityLevels(5).WithDefaultSensitivityLevel(3); |
| // The stored value 6 is now out of bounds. |
| // Default sensitivity for this config is 3. |
| |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, Feature::kSensitivityLevel, {}, smallerConfig); |
| ASSERT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); // Should read 6 and clamp it to (max - 1 == 4) |
| |
| ClusterTester tester(cluster); |
| uint8_t sensitivity = 0; |
| EXPECT_EQ(tester.ReadAttribute(Attributes::CurrentSensitivityLevel::Id, sensitivity), |
| Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(sensitivity, 4); // 5-1. Clamped from persisted value. |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // 4. Test that if persistence fails, default is used. Let's clear the storage. |
| context.StorageDelegate().ClearStorage(); |
| { |
| auto config = DefaultConfig().WithSupportedSensitivityLevels(9).WithDefaultSensitivityLevel(5); |
| |
| BooleanStateConfigurationCluster cluster(kTestEndpointId, Feature::kSensitivityLevel, {}, config); |
| ASSERT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| ClusterTester tester(cluster); |
| |
| uint8_t sensitivity = 0; |
| EXPECT_EQ(tester.ReadAttribute(Attributes::CurrentSensitivityLevel::Id, sensitivity), |
| Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(sensitivity, 5); // Should be default, as storage is empty. |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| } |
| |
| TEST_F(TestBooleanStateConfigurationCluster, TestAlarmsEnabledPersistence) |
| { |
| TestServerClusterContext context; |
| ScopedSafeAttributePersistence persistence(context); |
| |
| // 1. Create a cluster, set a value for AlarmsEnabled, which should be persisted. |
| { |
| auto config = DefaultConfig().AddAlarmsSupported(AlarmModeBitmap::kVisual); |
| BooleanStateConfigurationCluster cluster( |
| kTestEndpointId, { Feature::kVisual, Feature::kAudible }, |
| { BooleanStateConfigurationCluster::OptionalAttributesSet().Set<Attributes::AlarmsEnabled::Id>() }, config); |
| |
| ASSERT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| ClusterTester tester(cluster); |
| |
| // Check default value first |
| BooleanStateConfigurationCluster::AlarmModeBitMask alarmsEnabled; |
| EXPECT_EQ(tester.ReadAttribute(Attributes::AlarmsEnabled::Id, alarmsEnabled), Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(alarmsEnabled.Raw(), 0); |
| |
| // Set a new value. This will be persisted. |
| Commands::EnableDisableAlarm::Type request; |
| request.alarmsToEnableDisable.Set(AlarmModeBitmap::kVisual); |
| |
| auto result = tester.Invoke(request); |
| EXPECT_TRUE(result.IsSuccess()); |
| |
| EXPECT_EQ(tester.ReadAttribute(Attributes::AlarmsEnabled::Id, alarmsEnabled), Protocols::InteractionModel::Status::Success); |
| EXPECT_TRUE(alarmsEnabled.Has(AlarmModeBitmap::kVisual)); |
| EXPECT_FALSE(alarmsEnabled.Has(AlarmModeBitmap::kAudible)); |
| |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // 2. Create a new cluster instance with the same context, and check if the value was restored. |
| { |
| auto config = DefaultConfig().AddAlarmsSupported(AlarmModeBitmap::kAudible).AddAlarmsSupported(AlarmModeBitmap::kVisual); |
| BooleanStateConfigurationCluster cluster( |
| kTestEndpointId, { Feature::kVisual, Feature::kAudible }, |
| { BooleanStateConfigurationCluster::OptionalAttributesSet().Set<Attributes::AlarmsEnabled::Id>() }, config); |
| ASSERT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| ClusterTester tester(cluster); |
| |
| BooleanStateConfigurationCluster::AlarmModeBitMask alarmsEnabled; |
| EXPECT_EQ(tester.ReadAttribute(Attributes::AlarmsEnabled::Id, alarmsEnabled), Protocols::InteractionModel::Status::Success); |
| EXPECT_TRUE(alarmsEnabled.Has(AlarmModeBitmap::kVisual)); // Check if value is persisted. |
| EXPECT_FALSE(alarmsEnabled.Has(AlarmModeBitmap::kAudible)); |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| // 3. Test that if persistence fails, default is used. Let's clear the storage. |
| context.StorageDelegate().ClearStorage(); |
| { |
| auto config = DefaultConfig().AddAlarmsSupported(AlarmModeBitmap::kAudible).AddAlarmsSupported(AlarmModeBitmap::kVisual); |
| BooleanStateConfigurationCluster cluster( |
| kTestEndpointId, { Feature::kVisual, Feature::kAudible }, |
| { BooleanStateConfigurationCluster::OptionalAttributesSet().Set<Attributes::AlarmsEnabled::Id>() }, config); |
| ASSERT_EQ(cluster.Startup(context.Get()), CHIP_NO_ERROR); |
| ClusterTester tester(cluster); |
| |
| BooleanStateConfigurationCluster::AlarmModeBitMask alarmsEnabled; |
| EXPECT_EQ(tester.ReadAttribute(Attributes::AlarmsEnabled::Id, alarmsEnabled), Protocols::InteractionModel::Status::Success); |
| EXPECT_EQ(alarmsEnabled.Raw(), 0); // Should be default, as storage is empty. |
| cluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| } |
| |
| } // namespace |