blob: fef8a06126c8ebbd39075f330c19a53910513941 [file] [log] [blame]
/*
* 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