blob: aab8f49b516cc7a2236c3438259df20ed5afb44d [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 <pw_unit_test/framework.h>
#include <app/clusters/window-covering-server/WindowCoveringCluster.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/WindowCovering/Metadata.h>
#include <lib/support/BitFlags.h>
#include <lib/support/BitMask.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::WindowCovering;
using namespace chip::Testing;
using Status = chip::Protocols::InteractionModel::Status;
namespace {
constexpr EndpointId kTestEndpointId = 1;
// Records how it was called so tests can assert the cluster drives the delegate with the
// right parameters. Results are configurable to simulate delegate failure / in-progress motion.
class MockWindowCoveringDelegate : public WindowCoveringDelegate
{
public:
CHIP_ERROR HandleMovement(WindowCoveringType type) override
{
mHandleMovementCallCount++;
mLastMovementType = type;
return mHandleMovementResult;
}
CHIP_ERROR HandleStopMotion() override
{
mHandleStopMotionCallCount++;
return mHandleStopMotionResult;
}
void Reset()
{
mHandleMovementCallCount = 0;
mHandleStopMotionCallCount = 0;
}
int mHandleMovementCallCount = 0;
WindowCoveringType mLastMovementType = WindowCoveringType::Lift;
CHIP_ERROR mHandleMovementResult = CHIP_NO_ERROR;
int mHandleStopMotionCallCount = 0;
CHIP_ERROR mHandleStopMotionResult = CHIP_NO_ERROR;
};
// Exposes the protected `SetType` / `SetEndProductType` setters, which otherwise have no
// Interaction Model write path and are only set during application integration.
class TestableWindowCoveringCluster : public WindowCoveringCluster
{
public:
using WindowCoveringCluster::SetEndProductType;
using WindowCoveringCluster::SetType;
using WindowCoveringCluster::WindowCoveringCluster;
};
struct TestWindowCoveringCluster : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { Platform::MemoryShutdown(); }
};
} // namespace
TEST_F(TestWindowCoveringCluster, StartupShutdown)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kPositionAwareLift }));
ClusterTester tester(cluster);
EXPECT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, AttributesList_LiftOnly)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId, WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
EXPECT_TRUE(IsAttributesListEqualTo(cluster,
{
Attributes::Type::kMetadataEntry,
Attributes::ConfigStatus::kMetadataEntry,
Attributes::OperationalStatus::kMetadataEntry,
Attributes::EndProductType::kMetadataEntry,
Attributes::Mode::kMetadataEntry,
}));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// PositionAwareLift pulls in exactly the lift Percent100ths pair (Target + Current), nothing on tilt.
TEST_F(TestWindowCoveringCluster, AttributesList_PositionAwareLift)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kPositionAwareLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
EXPECT_TRUE(IsAttributesListEqualTo(cluster,
{
Attributes::Type::kMetadataEntry,
Attributes::ConfigStatus::kMetadataEntry,
Attributes::OperationalStatus::kMetadataEntry,
Attributes::EndProductType::kMetadataEntry,
Attributes::Mode::kMetadataEntry,
Attributes::TargetPositionLiftPercent100ths::kMetadataEntry,
Attributes::CurrentPositionLiftPercent100ths::kMetadataEntry,
}));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Symmetric case: PositionAwareTilt pulls in the tilt Percent100ths pair only.
TEST_F(TestWindowCoveringCluster, AttributesList_PositionAwareTilt)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kTilt, Feature::kPositionAwareTilt }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
EXPECT_TRUE(IsAttributesListEqualTo(cluster,
{
Attributes::Type::kMetadataEntry,
Attributes::ConfigStatus::kMetadataEntry,
Attributes::OperationalStatus::kMetadataEntry,
Attributes::EndProductType::kMetadataEntry,
Attributes::Mode::kMetadataEntry,
Attributes::TargetPositionTiltPercent100ths::kMetadataEntry,
Attributes::CurrentPositionTiltPercent100ths::kMetadataEntry,
}));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Both position-aware features plus every optional attribute: feature-gated and
// OptionalAttributeSet-gated attributes must coexist correctly.
TEST_F(TestWindowCoveringCluster, AttributesList_AllFeaturesAndOptionals)
{
WindowCovering::OptionalAttributeSet optionals;
optionals.Set<Attributes::NumberOfActuationsLift::Id>()
.Set<Attributes::NumberOfActuationsTilt::Id>()
.Set<Attributes::CurrentPositionLiftPercentage::Id>()
.Set<Attributes::CurrentPositionTiltPercentage::Id>()
.Set<Attributes::SafetyStatus::Id>();
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate)
.WithFeatures(
BitFlags<Feature>{ Feature::kLift, Feature::kTilt, Feature::kPositionAwareLift, Feature::kPositionAwareTilt })
.WithOptionalAttributes(optionals));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
EXPECT_TRUE(IsAttributesListEqualTo(cluster,
{
Attributes::Type::kMetadataEntry,
Attributes::ConfigStatus::kMetadataEntry,
Attributes::OperationalStatus::kMetadataEntry,
Attributes::EndProductType::kMetadataEntry,
Attributes::Mode::kMetadataEntry,
Attributes::NumberOfActuationsLift::kMetadataEntry,
Attributes::NumberOfActuationsTilt::kMetadataEntry,
Attributes::CurrentPositionLiftPercentage::kMetadataEntry,
Attributes::CurrentPositionTiltPercentage::kMetadataEntry,
Attributes::SafetyStatus::kMetadataEntry,
Attributes::TargetPositionLiftPercent100ths::kMetadataEntry,
Attributes::CurrentPositionLiftPercent100ths::kMetadataEntry,
Attributes::TargetPositionTiltPercent100ths::kMetadataEntry,
Attributes::CurrentPositionTiltPercent100ths::kMetadataEntry,
}));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, AcceptedCommands_LiftOnly)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId, WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
EXPECT_TRUE(IsAcceptedCommandsListEqualTo(cluster,
{
Commands::UpOrOpen::kMetadataEntry,
Commands::DownOrClose::kMetadataEntry,
Commands::StopMotion::kMetadataEntry,
Commands::GoToLiftPercentage::kMetadataEntry,
}));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, AcceptedCommands_TiltOnly)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId, WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kTilt }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
EXPECT_TRUE(IsAcceptedCommandsListEqualTo(cluster,
{
Commands::UpOrOpen::kMetadataEntry,
Commands::DownOrClose::kMetadataEntry,
Commands::StopMotion::kMetadataEntry,
Commands::GoToTiltPercentage::kMetadataEntry,
}));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, AcceptedCommands_LiftAndTilt)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId, WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kTilt }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
EXPECT_TRUE(IsAcceptedCommandsListEqualTo(cluster,
{
Commands::UpOrOpen::kMetadataEntry,
Commands::DownOrClose::kMetadataEntry,
Commands::StopMotion::kMetadataEntry,
Commands::GoToLiftPercentage::kMetadataEntry,
Commands::GoToTiltPercentage::kMetadataEntry,
}));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, ReadMandatoryDefaults)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId, WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kTilt }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
Type type{};
ASSERT_EQ(tester.ReadAttribute(Attributes::Type::Id, type), CHIP_NO_ERROR);
EXPECT_EQ(type, Type()); // default-constructed enum == kRollerShade (0)
EndProductType productType{};
ASSERT_EQ(tester.ReadAttribute(Attributes::EndProductType::Id, productType), CHIP_NO_ERROR);
EXPECT_EQ(productType, EndProductType());
chip::BitMask<ConfigStatus> configStatus{};
ASSERT_EQ(tester.ReadAttribute(Attributes::ConfigStatus::Id, configStatus), CHIP_NO_ERROR);
EXPECT_EQ(configStatus.Raw(), 0);
chip::BitMask<OperationalStatus> opStatus{};
ASSERT_EQ(tester.ReadAttribute(Attributes::OperationalStatus::Id, opStatus), CHIP_NO_ERROR);
EXPECT_EQ(opStatus.Raw(), 0);
chip::BitMask<Mode> mode{};
ASSERT_EQ(tester.ReadAttribute(Attributes::Mode::Id, mode), CHIP_NO_ERROR);
EXPECT_EQ(mode.Raw(), 0);
BitFlags<Feature> featureMap;
ASSERT_EQ(tester.ReadAttribute(Attributes::FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_TRUE(featureMap.HasAll(Feature::kLift, Feature::kTilt));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, ReadEnabledOptionalDefaults)
{
WindowCovering::OptionalAttributeSet optionals;
optionals.Set<Attributes::NumberOfActuationsLift::Id>().Set<Attributes::NumberOfActuationsTilt::Id>();
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(kTestEndpointId,
WindowCoveringCluster::Config(delegate)
.WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kTilt })
.WithOptionalAttributes(optionals));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
uint16_t actuationsLift = 0xFFFF;
ASSERT_EQ(tester.ReadAttribute(Attributes::NumberOfActuationsLift::Id, actuationsLift), CHIP_NO_ERROR);
EXPECT_EQ(actuationsLift, 0u);
uint16_t actuationsTilt = 0xFFFF;
ASSERT_EQ(tester.ReadAttribute(Attributes::NumberOfActuationsTilt::Id, actuationsTilt), CHIP_NO_ERROR);
EXPECT_EQ(actuationsTilt, 0u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// A disabled optional attribute is absent from Attributes(), so the IM (and therefore
// ClusterTester) rejects reading it as UnsupportedAttribute.
TEST_F(TestWindowCoveringCluster, ReadDisabledOptionalIsUnsupported)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId, WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
uint16_t actuationsLift = 0;
EXPECT_EQ(tester.ReadAttribute(Attributes::NumberOfActuationsLift::Id, actuationsLift), Status::UnsupportedAttribute);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, ReadNullablePositionRoundTrip)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kPositionAwareLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
NPercent100ths value;
ASSERT_EQ(tester.ReadAttribute(Attributes::CurrentPositionLiftPercent100ths::Id, value), CHIP_NO_ERROR);
EXPECT_TRUE(value.IsNull()); // default is null
cluster.SetCurrentPositionLiftPercent100ths(NPercent100ths(4200));
ASSERT_EQ(tester.ReadAttribute(Attributes::CurrentPositionLiftPercent100ths::Id, value), CHIP_NO_ERROR);
ASSERT_FALSE(value.IsNull());
EXPECT_EQ(value.Value(), 4200);
cluster.SetCurrentPositionLiftPercent100ths(NPercent100ths()); // back to null
ASSERT_EQ(tester.ReadAttribute(Attributes::CurrentPositionLiftPercent100ths::Id, value), CHIP_NO_ERROR);
EXPECT_TRUE(value.IsNull());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Mode is constrained to the low nibble (bits 0..3); raw values <= 0x0F are accepted,
// anything above is rejected with ConstraintError.
TEST_F(TestWindowCoveringCluster, WriteMode_ValidAndConstraint)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId, WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
chip::BitMask<Mode> minValue;
minValue.SetRaw(0x00);
EXPECT_EQ(tester.WriteAttribute(Attributes::Mode::Id, minValue), CHIP_NO_ERROR);
chip::BitMask<Mode> maxValid;
maxValid.SetRaw(0x0F);
EXPECT_EQ(tester.WriteAttribute(Attributes::Mode::Id, maxValid), CHIP_NO_ERROR);
chip::BitMask<Mode> tooLarge;
tooLarge.SetRaw(0x10);
EXPECT_EQ(tester.WriteAttribute(Attributes::Mode::Id, tooLarge), Status::ConstraintError);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Writing Mode derives ConfigStatus; a changed value marks both dirty, a no-op write marks neither.
TEST_F(TestWindowCoveringCluster, WriteMode_DirtyAndNoOp)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId, WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
auto & dirtyList = tester.GetDirtyList();
chip::BitMask<Mode> mode;
mode.Set(Mode::kLedFeedback);
ASSERT_EQ(tester.WriteAttribute(Attributes::Mode::Id, mode), CHIP_NO_ERROR);
EXPECT_TRUE(tester.IsAttributeDirty(Attributes::Mode::Id));
EXPECT_TRUE(tester.IsAttributeDirty(Attributes::ConfigStatus::Id));
EXPECT_EQ(dirtyList.size(), 2u);
dirtyList.clear();
ASSERT_EQ(tester.WriteAttribute(Attributes::Mode::Id, mode), CHIP_NO_ERROR);
EXPECT_TRUE(dirtyList.empty());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, SetterDirtyAndNoOp)
{
WindowCovering::OptionalAttributeSet optionals;
optionals.Set<Attributes::NumberOfActuationsLift::Id>();
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(kTestEndpointId,
WindowCoveringCluster::Config(delegate)
.WithFeatures(BitFlags<Feature>{ Feature::kLift })
.WithOptionalAttributes(optionals));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
auto & dirtyList = tester.GetDirtyList();
cluster.SetNumberOfActuationsLift(5);
EXPECT_TRUE(tester.IsAttributeDirty(Attributes::NumberOfActuationsLift::Id));
EXPECT_EQ(cluster.GetNumberOfActuationsLift(), 5u);
dirtyList.clear();
cluster.SetNumberOfActuationsLift(5); // same value -> no-op
EXPECT_TRUE(dirtyList.empty());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, Command_UpOrOpen_DownOrClose)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate).WithFeatures(
BitFlags<Feature>{ Feature::kLift, Feature::kTilt, Feature::kPositionAwareLift, Feature::kPositionAwareTilt }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
// UpOrOpen -> fully open (minimum).
delegate.Reset();
auto up = tester.Invoke<Commands::UpOrOpen::Type>(Commands::UpOrOpen::Type());
ASSERT_TRUE(up.IsSuccess());
EXPECT_EQ(delegate.mHandleMovementCallCount, 2); // once for Lift, once for Tilt
EXPECT_EQ(cluster.GetTargetPositionLiftPercent100ths().Value(), kWcPercent100thsMinOpen);
EXPECT_EQ(cluster.GetTargetPositionTiltPercent100ths().Value(), kWcPercent100thsMinOpen);
// DownOrClose -> fully closed (maximum).
delegate.Reset();
auto down = tester.Invoke<Commands::DownOrClose::Type>(Commands::DownOrClose::Type());
ASSERT_TRUE(down.IsSuccess());
EXPECT_EQ(delegate.mHandleMovementCallCount, 2);
EXPECT_EQ(cluster.GetTargetPositionLiftPercent100ths().Value(), kWcPercent100thsMaxClosed);
EXPECT_EQ(cluster.GetTargetPositionTiltPercent100ths().Value(), kWcPercent100thsMaxClosed);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, Command_StopMotion)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kPositionAwareLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
delegate.Reset();
auto result = tester.Invoke<Commands::StopMotion::Type>(Commands::StopMotion::Type());
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(delegate.mHandleStopMotionCallCount, 1);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestWindowCoveringCluster, Command_GoToLiftPercentage)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kPositionAwareLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
// Mid-range value.
{
Commands::GoToLiftPercentage::Type cmd;
cmd.liftPercent100thsValue = 5000;
delegate.Reset();
auto result = tester.Invoke<Commands::GoToLiftPercentage::Type>(cmd);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(delegate.mHandleMovementCallCount, 1);
EXPECT_EQ(delegate.mLastMovementType, WindowCoveringType::Lift);
EXPECT_EQ(cluster.GetTargetPositionLiftPercent100ths().Value(), 5000);
}
// Boundaries 0 and 10000 are valid.
{
Commands::GoToLiftPercentage::Type cmd;
cmd.liftPercent100thsValue = kWcPercent100thsMaxClosed;
auto result = tester.Invoke<Commands::GoToLiftPercentage::Type>(cmd);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(cluster.GetTargetPositionLiftPercent100ths().Value(), kWcPercent100thsMaxClosed);
}
// Above the maximum -> ConstraintError, target unchanged, delegate untouched.
{
Commands::GoToLiftPercentage::Type cmd;
cmd.liftPercent100thsValue = static_cast<Percent100ths>(kWcPercent100thsMaxClosed + 1);
delegate.Reset();
auto result = tester.Invoke<Commands::GoToLiftPercentage::Type>(cmd);
EXPECT_FALSE(result.IsSuccess());
auto statusCode = result.GetStatusCode();
ASSERT_TRUE(statusCode.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(statusCode->GetStatus(), Status::ConstraintError);
EXPECT_EQ(delegate.mHandleMovementCallCount, 0);
EXPECT_EQ(cluster.GetTargetPositionLiftPercent100ths().Value(), kWcPercent100thsMaxClosed);
}
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Symmetric coverage for the tilt axis.
TEST_F(TestWindowCoveringCluster, Command_GoToTiltPercentage)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kTilt, Feature::kPositionAwareTilt }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
{
Commands::GoToTiltPercentage::Type cmd;
cmd.tiltPercent100thsValue = 2500;
delegate.Reset();
auto result = tester.Invoke<Commands::GoToTiltPercentage::Type>(cmd);
ASSERT_TRUE(result.IsSuccess());
EXPECT_EQ(delegate.mHandleMovementCallCount, 1);
EXPECT_EQ(delegate.mLastMovementType, WindowCoveringType::Tilt);
EXPECT_EQ(cluster.GetTargetPositionTiltPercent100ths().Value(), 2500);
}
{
Commands::GoToTiltPercentage::Type cmd;
cmd.tiltPercent100thsValue = static_cast<Percent100ths>(kWcPercent100thsMaxClosed + 500);
delegate.Reset();
auto result = tester.Invoke<Commands::GoToTiltPercentage::Type>(cmd);
EXPECT_FALSE(result.IsSuccess());
auto statusCode = result.GetStatusCode();
ASSERT_TRUE(statusCode.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(statusCode->GetStatus(), Status::ConstraintError);
EXPECT_EQ(delegate.mHandleMovementCallCount, 0);
}
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Mode drives the operational bit of ConfigStatus, which gates motion:
// neither maintenance nor calibration -> Success, maintenance -> Busy, calibration -> Failure.
TEST_F(TestWindowCoveringCluster, MotionLock)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId, WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
// Default mode -> motion allowed.
{
auto result = tester.Invoke<Commands::UpOrOpen::Type>(Commands::UpOrOpen::Type());
EXPECT_TRUE(result.IsSuccess());
}
// Maintenance mode -> Busy.
{
chip::BitMask<Mode> maintenance;
maintenance.Set(Mode::kMaintenanceMode);
cluster.SetMode(maintenance);
auto result = tester.Invoke<Commands::UpOrOpen::Type>(Commands::UpOrOpen::Type());
ASSERT_FALSE(result.IsSuccess());
auto statusCode = result.GetStatusCode();
ASSERT_TRUE(statusCode.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(statusCode->GetStatus(), Status::Busy);
}
// Calibration mode -> Failure.
{
chip::BitMask<Mode> calibration;
calibration.Set(Mode::kCalibrationMode);
cluster.SetMode(calibration);
auto result = tester.Invoke<Commands::UpOrOpen::Type>(Commands::UpOrOpen::Type());
ASSERT_FALSE(result.IsSuccess());
auto statusCode = result.GetStatusCode();
ASSERT_TRUE(statusCode.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(statusCode->GetStatus(), Status::Failure);
}
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// A CHIP_ERROR_IN_PROGRESS result from HandleStopMotion means "still moving"; the target must
// not be snapped to the (still null) current position.
TEST_F(TestWindowCoveringCluster, Delegate_StopMotionInProgressKeepsTarget)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kPositionAwareLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
Commands::GoToLiftPercentage::Type goTo;
goTo.liftPercent100thsValue = 3000;
ASSERT_TRUE(tester.Invoke<Commands::GoToLiftPercentage::Type>(goTo).IsSuccess());
ASSERT_EQ(cluster.GetTargetPositionLiftPercent100ths().Value(), 3000);
delegate.mHandleStopMotionResult = CHIP_ERROR_IN_PROGRESS;
auto result = tester.Invoke<Commands::StopMotion::Type>(Commands::StopMotion::Type());
ASSERT_TRUE(result.IsSuccess());
ASSERT_FALSE(cluster.GetTargetPositionLiftPercent100ths().IsNull());
EXPECT_EQ(cluster.GetTargetPositionLiftPercent100ths().Value(), 3000);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// A failing HandleMovement is logged only, not propagated to the command caller, and the
// target state is still committed. Guards this legacy-preserved contract against regression.
TEST_F(TestWindowCoveringCluster, Delegate_MovementFailureIsNotPropagated)
{
MockWindowCoveringDelegate delegate;
TestableWindowCoveringCluster cluster(
kTestEndpointId,
WindowCoveringCluster::Config(delegate).WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kPositionAwareLift }));
ClusterTester tester(cluster);
ASSERT_EQ(cluster.Startup(tester.GetServerClusterContext()), CHIP_NO_ERROR);
delegate.mHandleMovementResult = CHIP_ERROR_INTERNAL;
Commands::GoToLiftPercentage::Type cmd;
cmd.liftPercent100thsValue = 6000;
auto result = tester.Invoke<Commands::GoToLiftPercentage::Type>(cmd);
EXPECT_TRUE(result.IsSuccess()); // failure swallowed (logged only)
EXPECT_EQ(delegate.mHandleMovementCallCount, 1); // delegate was still called
EXPECT_EQ(cluster.GetTargetPositionLiftPercent100ths().Value(), 6000); // target still committed
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Persistent attributes survive a restart; RAM-only attributes (Type) reset to default.
// A single TestServerClusterContext outlives two successive cluster instances.
TEST_F(TestWindowCoveringCluster, PersistenceRoundTrip)
{
TestServerClusterContext sharedContext;
MockWindowCoveringDelegate delegate;
WindowCoveringCluster::Config config(delegate);
config.WithFeatures(BitFlags<Feature>{ Feature::kLift, Feature::kTilt, Feature::kPositionAwareLift });
// First boot: mutate persistent state and a RAM-only attribute, then shut down.
{
TestableWindowCoveringCluster cluster(kTestEndpointId, config);
ASSERT_EQ(cluster.Startup(sharedContext.Get()), CHIP_NO_ERROR);
cluster.SetNumberOfActuationsLift(7);
cluster.SetNumberOfActuationsTilt(9);
cluster.SetCurrentPositionLiftPercent100ths(NPercent100ths(2500));
cluster.SetType(Type::kRollerShade2Motor); // RAM-only
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Second boot: a brand-new cluster against the same storage.
{
TestableWindowCoveringCluster cluster(kTestEndpointId, config);
ASSERT_EQ(cluster.Startup(sharedContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetNumberOfActuationsLift(), 7u);
EXPECT_EQ(cluster.GetNumberOfActuationsTilt(), 9u);
ASSERT_FALSE(cluster.GetCurrentPositionLiftPercent100ths().IsNull());
EXPECT_EQ(cluster.GetCurrentPositionLiftPercent100ths().Value(), 2500);
EXPECT_EQ(cluster.GetType(), Type()); // not persisted -> reset to default
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}