blob: 2f5d25608e19645e8429c96bb5b1ba9008f656be [file]
/*
*
* Copyright (c) 2022-2026 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <app/ConcreteAttributePath.h>
#include <app/clusters/fan-control-server/FanControlCluster.h>
#include <app/clusters/fan-control-server/fan-control-delegate.h>
#include <protocols/interaction_model/StatusCode.h>
#include <pw_unit_test/framework.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/FanControl/Attributes.h>
#include <clusters/FanControl/Commands.h>
#include <clusters/FanControl/Enums.h>
#include <clusters/FanControl/Metadata.h>
#include <lib/support/CHIPMem.h>
#include <lib/support/Span.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::FanControl;
using namespace chip::Testing;
using chip::Testing::IsAttributesListEqualTo;
namespace {
constexpr EndpointId kTestEndpointId = 1;
class TestFanControlDelegate : public FanControl::Delegate
{
public:
TestFanControlDelegate(EndpointId endpoint) : Delegate(endpoint) {}
Protocols::InteractionModel::Status HandleStep(StepDirectionEnum, bool, bool) override
{
return Protocols::InteractionModel::Status::Success;
}
};
TestFanControlDelegate gTestDelegate(kTestEndpointId);
class NotifyingFanControlDelegate : public FanControl::Delegate
{
public:
int mFanDriveStateNotifyCount = 0;
int mRockSettingNotifyCount = 0;
NotifyingFanControlDelegate(EndpointId endpoint) : Delegate(endpoint) {}
Protocols::InteractionModel::Status HandleStep(StepDirectionEnum, bool, bool) override
{
return Protocols::InteractionModel::Status::Success;
}
void OnFanDriveStateChanged(const FanDriveState &) override { mFanDriveStateNotifyCount++; }
void OnRockSettingChanged(BitMask<RockBitmap> newValue) override
{
(void) newValue;
mRockSettingNotifyCount++;
}
};
/// Exercises synchronous re-entry into the cluster from OnFanDriveStateChanged (nested notify must be suppressed).
class ReentrantFanDriveDelegate : public FanControl::Delegate
{
public:
FanControlCluster * mCluster = nullptr;
int mFanDriveStateNotifyCount = 0;
bool mDidNestedPercentWrite = false;
ReentrantFanDriveDelegate(EndpointId endpoint) : Delegate(endpoint) {}
Protocols::InteractionModel::Status HandleStep(StepDirectionEnum, bool, bool) override
{
return Protocols::InteractionModel::Status::Success;
}
void OnFanDriveStateChanged(const FanDriveState &) override
{
mFanDriveStateNotifyCount++;
if (mCluster != nullptr && !mDidNestedPercentWrite)
{
mDidNestedPercentWrite = true;
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(50);
EXPECT_EQ(mCluster->SetPercentSetting(percentSetting), Protocols::InteractionModel::Status::Success);
}
}
};
FanControlCluster::Config MakeTestConfig()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate).WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh);
}
FanControlCluster::Config MakeTestConfigWithStep()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate)
.WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh)
.WithStep();
}
FanControlCluster::Config MakeTestConfigWithMultiSpeed()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate)
.WithSpeedMax(10)
.WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh);
}
FanControlCluster::Config MakeTestConfigWithAuto()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate).WithFanModeSequence(FanModeSequenceEnum::kOffLowHighAuto);
}
FanControlCluster::Config MakeTestConfigWithMultiSpeedAndAuto()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate)
.WithSpeedMax(10)
.WithFanModeSequence(FanModeSequenceEnum::kOffLowHighAuto);
}
FanControlCluster::Config MakeTestConfigOffHigh()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate).WithFanModeSequence(FanModeSequenceEnum::kOffHigh);
}
FanControlCluster::Config MakeTestConfigOffHighAuto()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate).WithFanModeSequence(FanModeSequenceEnum::kOffHighAuto);
}
FanControlCluster::Config MakeTestConfigOffLowMedHigh()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate).WithFanModeSequence(FanModeSequenceEnum::kOffLowMedHigh);
}
FanControlCluster::Config MakeTestConfigWithRocking()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate)
.WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh)
.WithRockSupport(BitMask<RockBitmap>(RockBitmap::kRockLeftRight));
}
FanControlCluster::Config MakeTestConfigWithWind()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate)
.WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh)
.WithWindSupport(BitMask<WindBitmap>(WindBitmap::kSleepWind));
}
FanControlCluster::Config MakeTestConfigWithAirflowDirection()
{
return FanControlCluster::Config(kTestEndpointId, gTestDelegate)
.WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh)
.WithAirflowDirection();
}
template <FanControlCluster::Config (*ConfigFn)()>
struct TestFanControlClusterFixture : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
void SetUp() override { ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR); }
void TearDown() override { cluster.Shutdown(ClusterShutdownType::kClusterShutdown); }
TestServerClusterContext testContext;
FanControlCluster cluster{ ConfigFn() };
};
using TestFanControlCluster = TestFanControlClusterFixture<MakeTestConfig>;
using TestFanControlClusterWithStep = TestFanControlClusterFixture<MakeTestConfigWithStep>;
using TestFanControlClusterWithMultiSpeed = TestFanControlClusterFixture<MakeTestConfigWithMultiSpeed>;
using TestFanControlClusterWithAuto = TestFanControlClusterFixture<MakeTestConfigWithAuto>;
using TestFanControlClusterWithMultiSpeedAndAuto = TestFanControlClusterFixture<MakeTestConfigWithMultiSpeedAndAuto>;
using TestFanControlClusterOffHigh = TestFanControlClusterFixture<MakeTestConfigOffHigh>;
using TestFanControlClusterOffHighAuto = TestFanControlClusterFixture<MakeTestConfigOffHighAuto>;
using TestFanControlClusterOffLowMedHigh = TestFanControlClusterFixture<MakeTestConfigOffLowMedHigh>;
using TestFanControlClusterWithRocking = TestFanControlClusterFixture<MakeTestConfigWithRocking>;
using TestFanControlClusterWithWind = TestFanControlClusterFixture<MakeTestConfigWithWind>;
using TestFanControlClusterWithAirflowDirection = TestFanControlClusterFixture<MakeTestConfigWithAirflowDirection>;
struct TestFanControlPersistence : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
};
struct TestFanControlDelegateCallbacks : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
};
} // namespace
TEST_F(TestFanControlPersistence, StartupRestoresFanModeFromStorage)
{
TestServerClusterContext ctx;
FanModeEnum stored = FanModeEnum::kHigh;
ConcreteAttributePath path(kTestEndpointId, FanControl::Id, FanControl::Attributes::FanMode::Id);
ASSERT_EQ(
ctx.AttributePersistenceProvider().WriteValue(path, ByteSpan(reinterpret_cast<const uint8_t *>(&stored), sizeof(stored))),
CHIP_NO_ERROR);
NotifyingFanControlDelegate delegate(kTestEndpointId);
FanControlCluster cluster(
FanControlCluster::Config(kTestEndpointId, delegate).WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh));
ASSERT_EQ(cluster.Startup(ctx.Get()), CHIP_NO_ERROR);
EXPECT_EQ(delegate.mFanDriveStateNotifyCount, 1);
ClusterTester tester(cluster);
FanModeEnum readMode = FanModeEnum::kOff;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, readMode), CHIP_NO_ERROR);
EXPECT_EQ(readMode, FanModeEnum::kHigh);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestFanControlPersistence, StartupInvalidPersistedFanMode_FallsBackToOff)
{
TestServerClusterContext ctx;
FanModeEnum stored = FanModeEnum::kLow;
ConcreteAttributePath path(kTestEndpointId, FanControl::Id, FanControl::Attributes::FanMode::Id);
ASSERT_EQ(
ctx.AttributePersistenceProvider().WriteValue(path, ByteSpan(reinterpret_cast<const uint8_t *>(&stored), sizeof(stored))),
CHIP_NO_ERROR);
FanControlCluster cluster(
FanControlCluster::Config(kTestEndpointId, gTestDelegate).WithFanModeSequence(FanModeSequenceEnum::kOffHigh));
ASSERT_EQ(cluster.Startup(ctx.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
FanModeEnum readMode = FanModeEnum::kHigh;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, readMode), CHIP_NO_ERROR);
EXPECT_EQ(readMode, FanModeEnum::kOff);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestFanControlCluster, AttributeList)
{
ASSERT_TRUE(IsAttributesListEqualTo(cluster,
{
FanControl::Attributes::FanMode::kMetadataEntry,
FanControl::Attributes::FanModeSequence::kMetadataEntry,
FanControl::Attributes::PercentSetting::kMetadataEntry,
FanControl::Attributes::PercentCurrent::kMetadataEntry,
}));
}
TEST_F(TestFanControlCluster, ReadGlobalAttributes)
{
ClusterTester tester(cluster);
uint16_t revision{};
ASSERT_EQ(tester.ReadAttribute(Globals::Attributes::ClusterRevision::Id, revision), CHIP_NO_ERROR);
EXPECT_EQ(revision, FanControl::kRevision);
uint32_t featureMap{};
ASSERT_EQ(tester.ReadAttribute(Globals::Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, 0u);
}
TEST_F(TestFanControlClusterWithStep, FeatureMapIncludesStep)
{
ClusterTester tester(cluster);
uint32_t featureMap{};
ASSERT_EQ(tester.ReadAttribute(Globals::Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, static_cast<uint32_t>(Feature::kStep));
}
TEST_F(TestFanControlClusterWithMultiSpeed, FeatureMapIncludesMultiSpeed)
{
ClusterTester tester(cluster);
uint32_t featureMap{};
ASSERT_EQ(tester.ReadAttribute(Globals::Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, static_cast<uint32_t>(Feature::kMultiSpeed));
}
TEST_F(TestFanControlClusterWithAuto, FeatureMapIncludesAuto)
{
ClusterTester tester(cluster);
uint32_t featureMap{};
ASSERT_EQ(tester.ReadAttribute(Globals::Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, static_cast<uint32_t>(Feature::kAuto));
}
TEST_F(TestFanControlClusterWithMultiSpeedAndAuto, FeatureMapIncludesMultiSpeedAndAuto)
{
ClusterTester tester(cluster);
uint32_t featureMap{};
ASSERT_EQ(tester.ReadAttribute(Globals::Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, static_cast<uint32_t>(Feature::kMultiSpeed) | static_cast<uint32_t>(Feature::kAuto));
}
TEST_F(TestFanControlClusterOffHigh, FeatureMapIsZero)
{
ClusterTester tester(cluster);
uint32_t featureMap{};
ASSERT_EQ(tester.ReadAttribute(Globals::Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, 0u);
}
TEST_F(TestFanControlClusterOffHighAuto, FeatureMapIncludesAuto)
{
ClusterTester tester(cluster);
uint32_t featureMap{};
ASSERT_EQ(tester.ReadAttribute(Globals::Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, static_cast<uint32_t>(Feature::kAuto));
}
TEST_F(TestFanControlCluster, ReadFanMode)
{
ClusterTester tester(cluster);
FanModeEnum fanMode{};
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kOff);
}
TEST_F(TestFanControlCluster, SetFanMode_ReturnsConstraintError_WhenValueIsUnknown)
{
ClusterTester tester(cluster);
auto status = tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kUnknownEnumValue);
EXPECT_EQ(status, Protocols::InteractionModel::Status::ConstraintError);
}
TEST_F(TestFanControlCluster, WriteFanMode)
{
ClusterTester tester(cluster);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kHigh), CHIP_NO_ERROR);
FanModeEnum fanMode{};
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kHigh);
}
TEST_F(TestFanControlCluster, WritePercentSetting)
{
ClusterTester tester(cluster);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(50);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
DataModel::Nullable<chip::Percent> readBack;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::PercentSetting::Id, readBack), CHIP_NO_ERROR);
ASSERT_FALSE(readBack.IsNull());
EXPECT_EQ(readBack.Value(), 50);
}
TEST_F(TestFanControlCluster, WritePercentSettingNull_ReturnsInvalidInState_WhenNotInAuto)
{
ClusterTester tester(cluster);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(50);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
DataModel::Nullable<chip::Percent> nullWrite(DataModel::NullNullable);
EXPECT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, nullWrite),
Protocols::InteractionModel::Status::InvalidInState);
DataModel::Nullable<chip::Percent> readBack;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::PercentSetting::Id, readBack), CHIP_NO_ERROR);
ASSERT_FALSE(readBack.IsNull());
EXPECT_EQ(readBack.Value(), 50);
}
TEST_F(TestFanControlClusterWithMultiSpeedAndAuto, WritePercentSettingNull_WhenAlreadyNull_Succeeds)
{
ClusterTester tester(cluster);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(50);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kAuto), CHIP_NO_ERROR);
DataModel::Nullable<chip::Percent> readPercent;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::PercentSetting::Id, readPercent), CHIP_NO_ERROR);
EXPECT_TRUE(readPercent.IsNull());
DataModel::Nullable<chip::Percent> nullWrite(DataModel::NullNullable);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, nullWrite), CHIP_NO_ERROR);
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::PercentSetting::Id, readPercent), CHIP_NO_ERROR);
EXPECT_TRUE(readPercent.IsNull());
}
TEST_F(TestFanControlClusterWithMultiSpeed, WriteSpeedSettingNull_ReturnsInvalidInState_WhenNotInAuto)
{
ClusterTester tester(cluster);
DataModel::Nullable<uint8_t> speedSetting;
speedSetting.SetNonNull(5);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::SpeedSetting::Id, speedSetting), CHIP_NO_ERROR);
DataModel::Nullable<uint8_t> nullWrite(DataModel::NullNullable);
EXPECT_EQ(tester.WriteAttribute(FanControl::Attributes::SpeedSetting::Id, nullWrite),
Protocols::InteractionModel::Status::InvalidInState);
DataModel::Nullable<uint8_t> readBack;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::SpeedSetting::Id, readBack), CHIP_NO_ERROR);
ASSERT_FALSE(readBack.IsNull());
EXPECT_EQ(readBack.Value(), 5);
}
TEST_F(TestFanControlCluster, StepCommandUnsupportedWithoutFeature)
{
ClusterTester tester(cluster);
Commands::Step::Type commandData;
commandData.direction = StepDirectionEnum::kIncrease;
auto result = tester.Invoke(commandData);
ASSERT_TRUE(result.status.has_value());
if (result.status.has_value())
{
EXPECT_FALSE(result.status.value().IsSuccess());
}
}
TEST_F(TestFanControlClusterWithStep, StepCommandSupportedWithFeature)
{
ClusterTester tester(cluster);
Commands::Step::Type commandData;
commandData.direction = StepDirectionEnum::kIncrease;
auto result = tester.Invoke(commandData);
ASSERT_TRUE(result.status.has_value());
if (result.status.has_value())
{
EXPECT_TRUE(result.status.value().IsSuccess());
}
}
TEST_F(TestFanControlClusterWithMultiSpeed, FanModeOff_ZeroesPercentSettingAndSpeedSetting)
{
ClusterTester tester(cluster);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(50);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kOff), CHIP_NO_ERROR);
DataModel::Nullable<chip::Percent> readPercent;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::PercentSetting::Id, readPercent), CHIP_NO_ERROR);
ASSERT_FALSE(readPercent.IsNull());
EXPECT_EQ(readPercent.Value(), 0);
DataModel::Nullable<uint8_t> readSpeed;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::SpeedSetting::Id, readSpeed), CHIP_NO_ERROR);
ASSERT_FALSE(readSpeed.IsNull());
EXPECT_EQ(readSpeed.Value(), 0);
}
TEST_F(TestFanControlClusterWithMultiSpeedAndAuto, FanModeAuto_NullsPercentSettingAndSpeedSetting)
{
ClusterTester tester(cluster);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(50);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kAuto), CHIP_NO_ERROR);
DataModel::Nullable<chip::Percent> readPercent;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::PercentSetting::Id, readPercent), CHIP_NO_ERROR);
EXPECT_TRUE(readPercent.IsNull());
DataModel::Nullable<uint8_t> readSpeed;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::SpeedSetting::Id, readSpeed), CHIP_NO_ERROR);
EXPECT_TRUE(readSpeed.IsNull());
chip::Percent percentCurrent;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::PercentCurrent::Id, percentCurrent), CHIP_NO_ERROR);
EXPECT_EQ(percentCurrent, 0);
uint8_t speedCurrent;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::SpeedCurrent::Id, speedCurrent), CHIP_NO_ERROR);
EXPECT_EQ(speedCurrent, 0);
}
TEST_F(TestFanControlCluster, PercentSettingZero_SetsFanModeOff)
{
ClusterTester tester(cluster);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kHigh), CHIP_NO_ERROR);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(0);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
FanModeEnum fanMode{};
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kOff);
}
TEST_F(TestFanControlClusterWithMultiSpeed, SpeedSettingZero_SetsFanModeOff)
{
ClusterTester tester(cluster);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(50);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kHigh), CHIP_NO_ERROR);
DataModel::Nullable<uint8_t> speedSetting;
speedSetting.SetNonNull(0);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::SpeedSetting::Id, speedSetting), CHIP_NO_ERROR);
FanModeEnum fanMode{};
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kOff);
}
TEST_F(TestFanControlClusterWithMultiSpeed, SetSpeedSetting_ReturnsConstraintError_WhenValueExceedsSpeedMax)
{
ClusterTester tester(cluster);
DataModel::Nullable<uint8_t> speedSetting;
speedSetting.SetNonNull(11); // SpeedMax is 10 in MakeTestConfigWithMultiSpeed
auto status = tester.WriteAttribute(FanControl::Attributes::SpeedSetting::Id, speedSetting);
EXPECT_EQ(status, Protocols::InteractionModel::Status::ConstraintError);
}
TEST_F(TestFanControlClusterWithMultiSpeed, PercentSettingUpdatesSpeedSetting)
{
ClusterTester tester(cluster);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(50);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
DataModel::Nullable<uint8_t> readSpeed;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::SpeedSetting::Id, readSpeed), CHIP_NO_ERROR);
ASSERT_FALSE(readSpeed.IsNull());
EXPECT_EQ(readSpeed.Value(), 5); // 50% of SpeedMax=10
}
TEST_F(TestFanControlClusterWithMultiSpeed, SpeedSettingUpdatesPercentSetting)
{
ClusterTester tester(cluster);
DataModel::Nullable<uint8_t> speedSetting;
speedSetting.SetNonNull(5);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::SpeedSetting::Id, speedSetting), CHIP_NO_ERROR);
DataModel::Nullable<chip::Percent> readPercent;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::PercentSetting::Id, readPercent), CHIP_NO_ERROR);
ASSERT_FALSE(readPercent.IsNull());
EXPECT_EQ(readPercent.Value(), 50); // speed 5 of 10 = 50%
}
TEST_F(TestFanControlCluster, FanModeOn_MapsToHigh)
{
ClusterTester tester(cluster);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kOn), CHIP_NO_ERROR);
FanModeEnum fanMode{};
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kHigh);
}
TEST_F(TestFanControlClusterWithAuto, FanModeSmart_MapsToAuto)
{
ClusterTester tester(cluster);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kSmart), CHIP_NO_ERROR);
FanModeEnum fanMode{};
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kAuto);
}
TEST_F(TestFanControlCluster, FanModeSmart_MapsToHigh_WhenAutoNotSupported)
{
ClusterTester tester(cluster);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kSmart), CHIP_NO_ERROR);
FanModeEnum fanMode{};
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kHigh);
}
TEST_F(TestFanControlClusterOffHighAuto, FanModeSmart_MapsToAuto)
{
ClusterTester tester(cluster);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kSmart), CHIP_NO_ERROR);
FanModeEnum fanMode{};
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kAuto);
}
TEST_F(TestFanControlClusterOffHigh, FanModeLow_ReturnsInvalidInState)
{
ClusterTester tester(cluster);
auto status = tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kLow);
EXPECT_EQ(status, Protocols::InteractionModel::Status::InvalidInState);
}
TEST_F(TestFanControlClusterOffHigh, FanModeMedium_ReturnsInvalidInState)
{
ClusterTester tester(cluster);
auto status = tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kMedium);
EXPECT_EQ(status, Protocols::InteractionModel::Status::InvalidInState);
}
TEST_F(TestFanControlClusterOffHighAuto, FanModeAuto_Succeeds)
{
ClusterTester tester(cluster);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kAuto), CHIP_NO_ERROR);
FanModeEnum fanMode{};
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kAuto);
}
TEST_F(TestFanControlCluster, FanModeAuto_ReturnsInvalidInState_WhenAutoNotSupported)
{
ClusterTester tester(cluster);
auto status = tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kAuto);
EXPECT_EQ(status, Protocols::InteractionModel::Status::InvalidInState);
}
TEST_F(TestFanControlCluster, FanModeMedium_ReturnsInvalidInState)
{
ClusterTester tester(cluster);
auto status = tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kMedium);
EXPECT_EQ(status, Protocols::InteractionModel::Status::InvalidInState);
}
TEST_F(TestFanControlClusterOffLowMedHigh, FanModeLowAndMedium_Succeed)
{
ClusterTester tester(cluster);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kLow), CHIP_NO_ERROR);
FanModeEnum fanMode{};
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kLow);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kMedium), CHIP_NO_ERROR);
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::FanMode::Id, fanMode), CHIP_NO_ERROR);
EXPECT_EQ(fanMode, FanModeEnum::kMedium);
}
TEST_F(TestFanControlCluster, SetRockSetting_ReturnsUnsupportedAttribute_WhenRockingNotSupported)
{
ClusterTester tester(cluster);
BitMask<RockBitmap> rockSetting(RockBitmap::kRockLeftRight);
auto status = tester.WriteAttribute(FanControl::Attributes::RockSetting::Id, rockSetting);
EXPECT_EQ(status, Protocols::InteractionModel::Status::UnsupportedAttribute);
}
TEST_F(TestFanControlCluster, SetWindSetting_ReturnsUnsupportedAttribute_WhenWindNotSupported)
{
ClusterTester tester(cluster);
BitMask<WindBitmap> windSetting(WindBitmap::kSleepWind);
auto status = tester.WriteAttribute(FanControl::Attributes::WindSetting::Id, windSetting);
EXPECT_EQ(status, Protocols::InteractionModel::Status::UnsupportedAttribute);
}
TEST_F(TestFanControlClusterWithRocking, SetRockSetting_Succeeds_WhenValueIsSubsetOfSupport)
{
ClusterTester tester(cluster);
BitMask<RockBitmap> rockSetting(RockBitmap::kRockLeftRight);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::RockSetting::Id, rockSetting), CHIP_NO_ERROR);
BitMask<RockBitmap> readBack;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::RockSetting::Id, readBack), CHIP_NO_ERROR);
EXPECT_EQ(readBack.Raw(), rockSetting.Raw());
}
TEST_F(TestFanControlClusterWithRocking, SetRockSetting_ReturnsConstraintError_WhenValueHasUnsupportedBits)
{
ClusterTester tester(cluster);
BitMask<RockBitmap> unsupportedBits(RockBitmap::kRockUpDown);
auto status = tester.WriteAttribute(FanControl::Attributes::RockSetting::Id, unsupportedBits);
EXPECT_EQ(status, Protocols::InteractionModel::Status::ConstraintError);
}
TEST_F(TestFanControlClusterWithWind, SetWindSetting_Succeeds_WhenValueIsSubsetOfSupport)
{
ClusterTester tester(cluster);
BitMask<WindBitmap> windSetting(WindBitmap::kSleepWind);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::WindSetting::Id, windSetting), CHIP_NO_ERROR);
BitMask<WindBitmap> readBack;
ASSERT_EQ(tester.ReadAttribute(FanControl::Attributes::WindSetting::Id, readBack), CHIP_NO_ERROR);
EXPECT_EQ(readBack.Raw(), windSetting.Raw());
}
TEST_F(TestFanControlClusterWithWind, SetWindSetting_ReturnsConstraintError_WhenValueHasUnsupportedBits)
{
ClusterTester tester(cluster);
BitMask<WindBitmap> unsupportedBits(WindBitmap::kNaturalWind);
auto status = tester.WriteAttribute(FanControl::Attributes::WindSetting::Id, unsupportedBits);
EXPECT_EQ(status, Protocols::InteractionModel::Status::ConstraintError);
}
TEST_F(TestFanControlClusterWithAirflowDirection, SetAirflowDirection_ReturnsConstraintError_WhenValueIsUnknown)
{
ClusterTester tester(cluster);
auto status = tester.WriteAttribute(FanControl::Attributes::AirflowDirection::Id, AirflowDirectionEnum::kUnknownEnumValue);
EXPECT_EQ(status, Protocols::InteractionModel::Status::ConstraintError);
}
TEST_F(TestFanControlDelegateCallbacks, WritePercentSetting_NotifiesDelegate)
{
TestServerClusterContext testContext;
NotifyingFanControlDelegate delegate(kTestEndpointId);
FanControlCluster cluster(
FanControlCluster::Config(kTestEndpointId, delegate).WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
delegate.mFanDriveStateNotifyCount = 0;
ClusterTester tester(cluster);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(40);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
EXPECT_EQ(delegate.mFanDriveStateNotifyCount, 1);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// A delegate that writes an attribute back from within OnFanDriveStateChanged must not mutate the
// cluster's state mid-update: the cluster's in-progress cascade is authoritative, so the nested write
// is ignored (and triggers no second notification). This is what keeps applications that reflexively
// write attributes from their change callbacks (e.g. air-purifier) from clobbering the values the
// cluster just computed. See FanControlCluster::SetPercentSetting.
TEST_F(TestFanControlDelegateCallbacks, NestedSetPercentFromDelegate_IsIgnoredAndSuppressesNotify)
{
TestServerClusterContext testContext;
ReentrantFanDriveDelegate delegate(kTestEndpointId);
FanControlCluster cluster(
FanControlCluster::Config(kTestEndpointId, delegate).WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh));
delegate.mCluster = &cluster;
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(40);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
// The outer write notifies once; the nested SetPercentSetting(50) is suppressed (no second notify).
EXPECT_EQ(delegate.mFanDriveStateNotifyCount, 1);
ASSERT_FALSE(cluster.GetPercentSetting().IsNull());
// The nested write of 50 is ignored; the controller's value (40) stands.
EXPECT_EQ(cluster.GetPercentSetting().Value(), static_cast<chip::Percent>(40));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestFanControlDelegateCallbacks, WritePercentSettingSameValue_DoesNotNotifyDelegateAgain)
{
TestServerClusterContext testContext;
NotifyingFanControlDelegate delegate(kTestEndpointId);
FanControlCluster cluster(
FanControlCluster::Config(kTestEndpointId, delegate).WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(40);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
EXPECT_EQ(delegate.mFanDriveStateNotifyCount, 1);
delegate.mFanDriveStateNotifyCount = 0;
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
EXPECT_EQ(delegate.mFanDriveStateNotifyCount, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestFanControlDelegateCallbacks, WriteFanModeSameValue_DoesNotNotifyDelegateAgain)
{
TestServerClusterContext testContext;
NotifyingFanControlDelegate delegate(kTestEndpointId);
FanControlCluster cluster(
FanControlCluster::Config(kTestEndpointId, delegate).WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
delegate.mFanDriveStateNotifyCount = 0;
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kLow), CHIP_NO_ERROR);
EXPECT_EQ(delegate.mFanDriveStateNotifyCount, 1);
delegate.mFanDriveStateNotifyCount = 0;
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kLow), CHIP_NO_ERROR);
EXPECT_EQ(delegate.mFanDriveStateNotifyCount, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestFanControlDelegateCallbacks, WritePercentSettingToZero_NotifiesFanDriveStateOnce)
{
TestServerClusterContext testContext;
NotifyingFanControlDelegate delegate(kTestEndpointId);
FanControlCluster cluster(
FanControlCluster::Config(kTestEndpointId, delegate).WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kHigh), CHIP_NO_ERROR);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(80);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
delegate.mFanDriveStateNotifyCount = 0;
percentSetting.SetNonNull(0);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
EXPECT_EQ(delegate.mFanDriveStateNotifyCount, 1);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestFanControlDelegateCallbacks, WriteSpeedSettingToZero_NotifiesFanDriveStateOnce)
{
TestServerClusterContext testContext;
NotifyingFanControlDelegate delegate(kTestEndpointId);
FanControlCluster cluster(FanControlCluster::Config(kTestEndpointId, delegate)
.WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh)
.WithSpeedMax(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::FanMode::Id, FanModeEnum::kHigh), CHIP_NO_ERROR);
DataModel::Nullable<chip::Percent> percentSetting;
percentSetting.SetNonNull(50);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::PercentSetting::Id, percentSetting), CHIP_NO_ERROR);
delegate.mFanDriveStateNotifyCount = 0;
DataModel::Nullable<uint8_t> speedSetting;
speedSetting.SetNonNull(0);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::SpeedSetting::Id, speedSetting), CHIP_NO_ERROR);
EXPECT_EQ(delegate.mFanDriveStateNotifyCount, 1);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestFanControlDelegateCallbacks, WriteRockSetting_NotifiesDelegate)
{
TestServerClusterContext testContext;
NotifyingFanControlDelegate delegate(kTestEndpointId);
FanControlCluster cluster(FanControlCluster::Config(kTestEndpointId, delegate)
.WithFanModeSequence(FanModeSequenceEnum::kOffLowHigh)
.WithRockSupport(BitMask<RockBitmap>(RockBitmap::kRockLeftRight)));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
delegate.mRockSettingNotifyCount = 0;
ClusterTester tester(cluster);
BitMask<RockBitmap> rockSetting(RockBitmap::kRockLeftRight);
ASSERT_EQ(tester.WriteAttribute(FanControl::Attributes::RockSetting::Id, rockSetting), CHIP_NO_ERROR);
EXPECT_EQ(delegate.mRockSettingNotifyCount, 1);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}