blob: dcfaa7ead33a8a5b2b29603a0badb7be376ab834 [file]
/**
*
* Copyright (c) 2024 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 "ClosureDimensionCluster.h"
#include <app/server-cluster/AttributeListBuilder.h>
#include <clusters/ClosureDimension/Attributes.h>
#include <clusters/ClosureDimension/Commands.h>
#include <clusters/ClosureDimension/Metadata.h>
#include <lib/support/logging/CHIPLogging.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::ClosureDimension;
using namespace chip::app::Clusters::ClosureDimension::Attributes;
namespace chip {
namespace app {
namespace Clusters {
namespace ClosureDimension {
using namespace Protocols::InteractionModel;
namespace {
constexpr Percent100ths kPercents100thsMaxValue = 10000;
constexpr uint64_t kPositionQuietReportingInterval = 5000;
} // namespace
ClosureDimensionCluster::ClosureDimensionCluster(const Config & config) :
DefaultServerCluster({ config.mEndpointId, ClosureDimension::Id }), mDelegate(config.mDelegate),
mResolution(config.mResolution), mStepValue(config.mStepValue), mUnit(config.mUnit), mUnitRange(config.mUnitRange),
mLimitRange(config.mLimitRange), mTranslationDirection(config.mTranslationDirection), mRotationAxis(config.mRotationAxis),
mOverflow(config.mOverflow), mModulationType(config.mModulationType), mLatchControlModes(config.mLatchControlModes),
mFeatureMap(config.mFeatureMap)
{
// Positioning or MotionLatching must be enabled
VerifyOrDieWithMsg(mFeatureMap.Has(Feature::kPositioning) || mFeatureMap.Has(Feature::kMotionLatching), AppServer,
"Validation failed: Neither Positioning nor MotionLatching is enabled.");
// If Unit, Limitation or speed is enabled, Positioning must be enabled
if (mFeatureMap.Has(Feature::kUnit) || mFeatureMap.Has(Feature::kLimitation) || mFeatureMap.Has(Feature::kSpeed))
{
VerifyOrDieWithMsg(mFeatureMap.Has(Feature::kPositioning), AppServer,
"Validation failed: Unit, Limitation, and speed requires the Positioning feature.");
}
// If Translation, Rotation or Modulation is enabled, Positioning must be enabled.
if (mFeatureMap.Has(Feature::kTranslation) || mFeatureMap.Has(Feature::kRotation) || mFeatureMap.Has(Feature::kModulation))
{
VerifyOrDieWithMsg(mFeatureMap.Has(Feature::kPositioning), AppServer,
"Validation failed: Translation, Rotation or Modulation requires Positioning enabled.");
}
// Only one of Translation, Rotation or Modulation features must be enabled. Return false otherwise.
if ((mFeatureMap.Has(Feature::kTranslation) && mFeatureMap.Has(Feature::kRotation)) ||
(mFeatureMap.Has(Feature::kRotation) && mFeatureMap.Has(Feature::kModulation)) ||
(mFeatureMap.Has(Feature::kModulation) && mFeatureMap.Has(Feature::kTranslation)))
{
VerifyOrDieWithMsg(false, AppServer,
"Validation failed: Only one of Translation, Rotation or Modulation feature can be enabled.");
}
}
ClosureDimensionCluster::~ClosureDimensionCluster() {}
CHIP_ERROR ClosureDimensionCluster::Attributes(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
using OptionalEntry = AttributeListBuilder::OptionalAttributeEntry;
OptionalEntry optionalAttributes[] = {
{ mFeatureMap.Has(Feature::kPositioning), Attributes::Resolution::kMetadataEntry },
{ mFeatureMap.Has(Feature::kPositioning), Attributes::StepValue::kMetadataEntry },
{ mFeatureMap.Has(Feature::kUnit), Attributes::Unit::kMetadataEntry },
{ mFeatureMap.Has(Feature::kUnit), Attributes::UnitRange::kMetadataEntry },
{ mFeatureMap.Has(Feature::kLimitation), Attributes::LimitRange::kMetadataEntry },
{ mFeatureMap.Has(Feature::kTranslation), Attributes::TranslationDirection::kMetadataEntry },
{ mFeatureMap.Has(Feature::kRotation), Attributes::RotationAxis::kMetadataEntry },
{ mFeatureMap.Has(Feature::kRotation), Attributes::Overflow::kMetadataEntry },
{ mFeatureMap.Has(Feature::kModulation), Attributes::ModulationType::kMetadataEntry },
{ mFeatureMap.Has(Feature::kMotionLatching), Attributes::LatchControlModes::kMetadataEntry },
};
AttributeListBuilder listBuilder(builder);
return listBuilder.Append(Span(Attributes::kMandatoryMetadata), Span(optionalAttributes));
}
CHIP_ERROR ClosureDimensionCluster::AcceptedCommands(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
{
static constexpr DataModel::AcceptedCommandEntry kMandatoryCommands[] = {
Commands::SetTarget::kMetadataEntry,
};
static constexpr DataModel::AcceptedCommandEntry kStepCommand[] = {
Commands::Step::kMetadataEntry,
};
static constexpr DataModel::AcceptedCommandEntry kGroupedSetTargetCommand[] = {
Commands::GroupedSetTarget::kMetadataEntry,
};
static constexpr DataModel::AcceptedCommandEntry kGroupedStepCommand[] = {
Commands::GroupedStep::kMetadataEntry,
};
ReturnErrorOnFailure(builder.ReferenceExisting(kMandatoryCommands));
if (mFeatureMap.Has(Feature::kPositioning))
{
ReturnErrorOnFailure(builder.ReferenceExisting(kStepCommand));
}
if (!mFeatureMap.Has(Feature::kAccess))
{
ReturnErrorOnFailure(builder.ReferenceExisting(kGroupedSetTargetCommand));
if (mFeatureMap.Has(Feature::kPositioning))
{
ReturnErrorOnFailure(builder.ReferenceExisting(kGroupedStepCommand));
}
}
return CHIP_NO_ERROR;
}
DataModel::ActionReturnStatus ClosureDimensionCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder)
{
switch (request.path.mAttributeId)
{
case Attributes::CurrentState::Id:
return encoder.Encode(GetCurrentState());
case Attributes::TargetState::Id:
return encoder.Encode(GetTargetState());
case Attributes::Resolution::Id:
return encoder.Encode(GetResolution());
case Attributes::StepValue::Id:
return encoder.Encode(GetStepValue());
case Attributes::Unit::Id:
return encoder.Encode(GetUnit());
case Attributes::UnitRange::Id:
return encoder.Encode(GetUnitRange());
case Attributes::LimitRange::Id:
return encoder.Encode(GetLimitRange());
case Attributes::TranslationDirection::Id:
return encoder.Encode(GetTranslationDirection());
case Attributes::RotationAxis::Id:
return encoder.Encode(GetRotationAxis());
case Attributes::Overflow::Id:
return encoder.Encode(GetOverflow());
case Attributes::ModulationType::Id:
return encoder.Encode(GetModulationType());
case Attributes::LatchControlModes::Id:
return encoder.Encode(GetLatchControlModes());
case Attributes::FeatureMap::Id:
return encoder.Encode(GetFeatureMap());
case Attributes::ClusterRevision::Id:
return encoder.Encode(kRevision);
default:
return Status::UnsupportedAttribute;
}
}
std::optional<DataModel::ActionReturnStatus> ClosureDimensionCluster::InvokeCommand(const DataModel::InvokeRequest & request,
chip::TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
switch (request.path.mCommandId)
{
case Commands::SetTarget::Id: {
Commands::SetTarget::DecodableType commandData;
ReturnErrorOnFailure(commandData.Decode(input_arguments));
return HandleSetTargetCommand(commandData.position, commandData.latch, commandData.speed);
}
case Commands::Step::Id: {
Commands::Step::DecodableType commandData;
ReturnErrorOnFailure(commandData.Decode(input_arguments));
return HandleStepCommand(commandData.direction, commandData.numberOfSteps, commandData.speed);
}
case Commands::GroupedSetTarget::Id: {
Commands::GroupedSetTarget::DecodableType commandData;
ReturnErrorOnFailure(commandData.Decode(input_arguments));
return HandleSetTargetCommand(commandData.position, commandData.latch, commandData.speed);
}
case Commands::GroupedStep::Id: {
Commands::GroupedStep::DecodableType commandData;
ReturnErrorOnFailure(commandData.Decode(input_arguments));
return HandleStepCommand(commandData.direction, commandData.numberOfSteps, commandData.speed);
}
default:
return Status::UnsupportedCommand;
}
}
// Specification rules for CurrentState quiet reporting:
// Changes to this attribute SHALL only be marked as reportable in the following cases:
// When the Position changes from null to any other value and vice versa, or
// At most once every 5 seconds when the Position changes from one non-null value to another non-null value, or
// When Target.Position is reached, or
// When CurrentState.Speed changes, or
// When CurrentState.Latch changes.
// At present, QuieterReportingAttribute class does not support Structs.
// so each field of current state struct has to be handled independently.
// At present, we are using QuieterReportingAttribute class for Position only.
// Latch and Speed changes are directly handled by the cluster logic separately.
// i.e Speed and latch changes are not considered when calculating the at most 5 seconds quiet reportable changes for Position.
CHIP_ERROR ClosureDimensionCluster::SetCurrentState(const DataModel::Nullable<GenericDimensionStateStruct> & incomingCurrentState)
{
assertChipStackLockedByCurrentThread();
VerifyOrReturnError(mCurrentState != incomingCurrentState, CHIP_NO_ERROR);
bool markDirty = false;
if (!incomingCurrentState.IsNull())
{
// Validate the incoming Position value has valid input parameters and FeatureMap conformance.
if (incomingCurrentState.Value().position.HasValue())
{
// If the position member is present in the incoming CurrentState, we need to check if the Positioning
// feature is supported by the closure. If the Positioning feature is not supported, return an error.
VerifyOrReturnError(mFeatureMap.Has(Feature::kPositioning), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
if (!incomingCurrentState.Value().position.Value().IsNull())
{
VerifyOrReturnError(incomingCurrentState.Value().position.Value().Value() <= kPercents100thsMaxValue,
CHIP_ERROR_INVALID_ARGUMENT);
}
bool targetPositionReached = false;
auto now = System::SystemClock().GetMonotonicTimestamp();
// Logic to determine if target position is reached.
// If the target position is reached, current state attribute will be marked dirty and reported.
if (!mTargetState.IsNull() && mTargetState.Value().position.HasValue() &&
!mTargetState.Value().position.Value().IsNull() &&
mTargetState.Value().position == incomingCurrentState.Value().position)
{
targetPositionReached = true;
}
if (targetPositionReached)
{
auto predicate =
[](const decltype(quietReportableCurrentStatePosition)::SufficientChangePredicateCandidate &) -> bool {
return true;
};
markDirty |= (quietReportableCurrentStatePosition.SetValue(incomingCurrentState.Value().position.Value(), now,
predicate) == AttributeDirtyState::kMustReport);
}
else
{
// Predicate to report at most once every 5 seconds when the Position changes from one non-null value to another
// non-null value, or when the Position changes from null to any other value and vice versa
System::Clock::Milliseconds64 reportInterval = System::Clock::Milliseconds64(kPositionQuietReportingInterval);
auto predicate = quietReportableCurrentStatePosition.GetPredicateForSufficientTimeSinceLastDirty(reportInterval);
markDirty |= (quietReportableCurrentStatePosition.SetValue(incomingCurrentState.Value().position.Value(), now,
predicate) == AttributeDirtyState::kMustReport);
}
}
// Validate the incoming latch value has valid FeatureMap conformance.
if (incomingCurrentState.Value().latch.HasValue())
{
// If the latching member is present in the incoming CurrentState, we need to check if the MotionLatching
// feature is supported by the closure. If the MotionLatching feature is not supported, return an error.
VerifyOrReturnError(mFeatureMap.Has(Feature::kMotionLatching), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
}
// Changes to this attribute SHALL only be marked as reportable when latch changes.
if (!mCurrentState.IsNull() && mCurrentState.Value().latch != incomingCurrentState.Value().latch)
{
markDirty = true;
}
// Validate the incoming Speed value has valid input parameters and FeatureMap conformance.
if (incomingCurrentState.Value().speed.HasValue())
{
// If the speed member is present in the incoming CurrentState, we need to check if the Speed feature is
// supported by the closure. If the Speed feature is not supported, return an error.
VerifyOrReturnError(mFeatureMap.Has(Feature::kSpeed), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
VerifyOrReturnError(EnsureKnownEnumValue(incomingCurrentState.Value().speed.Value()) !=
Globals::ThreeLevelAutoEnum::kUnknownEnumValue,
CHIP_ERROR_INVALID_ARGUMENT);
}
// Changes to this attribute SHALL be marked as reportable when speed changes.
if (!mCurrentState.IsNull() && mCurrentState.Value().speed != incomingCurrentState.Value().speed)
{
markDirty = true;
}
}
// If the current state is null and the incoming current state is not null and vice versa, we need to mark dirty.
if (mCurrentState.IsNull() != incomingCurrentState.IsNull())
{
markDirty = true;
}
mCurrentState = incomingCurrentState;
NotifyAttributeChanged(Attributes::CurrentState::Id,
markDirty ? DataModel::AttributeChangeType::kReportable : DataModel::AttributeChangeType::kQuiet);
return CHIP_NO_ERROR;
}
CHIP_ERROR ClosureDimensionCluster::SetTargetState(const DataModel::Nullable<GenericDimensionStateStruct> & incomingTargetState)
{
assertChipStackLockedByCurrentThread();
VerifyOrReturnError(mTargetState != incomingTargetState, CHIP_NO_ERROR);
if (!incomingTargetState.IsNull())
{
// Validate the incoming Position value has valid input parameters and FeatureMap conformance.
if (incomingTargetState.Value().position.HasValue())
{
// If the position member is present in the incoming TargetState, we need to check if the Positioning
// feature is supported by the closure. If the Positioning feature is not supported, return an error.
VerifyOrReturnError(mFeatureMap.Has(Feature::kPositioning), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
if (!incomingTargetState.Value().position.Value().IsNull())
{
VerifyOrReturnError(incomingTargetState.Value().position.Value().Value() <= kPercents100thsMaxValue,
CHIP_ERROR_INVALID_ARGUMENT);
// Incoming TargetState Position value SHALL follow the scaling from Resolution Attribute.
Percent100ths resolution = GetResolution();
VerifyOrReturnError(
incomingTargetState.Value().position.Value().Value() % resolution == 0, CHIP_ERROR_INVALID_ARGUMENT,
ChipLogError(AppServer, "TargetState Position value SHALL follow the scaling from Resolution Attribute"));
}
}
// Validate the incoming latch value has valid FeatureMap conformance.
if (incomingTargetState.Value().latch.HasValue())
{
// If the latching member is present in the incoming TargetState, we need to check if the MotionLatching
// feature is supported by the closure. If the MotionLatching feature is not supported, return an error.
VerifyOrReturnError(mFeatureMap.Has(Feature::kMotionLatching), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
}
// Validate the incoming Speed value has valid input parameters and FeatureMap conformance.
if (incomingTargetState.Value().speed.HasValue())
{
// If the speed member is present in the incoming TargetState, we need to check if the Speed feature is
// supported by the closure. If the Speed feature is not supported, return an error.
VerifyOrReturnError(mFeatureMap.Has(Feature::kSpeed), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
VerifyOrReturnError(EnsureKnownEnumValue(incomingTargetState.Value().speed.Value()) !=
Globals::ThreeLevelAutoEnum::kUnknownEnumValue,
CHIP_ERROR_INVALID_ARGUMENT);
}
}
SetAttributeValue(mTargetState, incomingTargetState, Attributes::TargetState::Id);
return CHIP_NO_ERROR;
}
CHIP_ERROR ClosureDimensionCluster::SetUnitRange(const DataModel::Nullable<Structs::UnitRangeStruct::Type> & unitRange)
{
VerifyOrReturnError(mFeatureMap.Has(Feature::kUnit), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
if (unitRange.IsNull())
{
SetAttributeValue(mUnitRange, DataModel::NullNullable, Attributes::UnitRange::Id);
return CHIP_NO_ERROR;
}
// Return error if unitRange is invalid
VerifyOrReturnError(unitRange.Value().min <= unitRange.Value().max, CHIP_ERROR_INVALID_ARGUMENT);
ClosureUnitEnum unit = GetUnit();
// If Unit is Millimeter , Range values SHALL contain unsigned values from 0 to 32767 only
if (unit == ClosureUnitEnum::kMillimeter)
{
VerifyOrReturnError(unitRange.Value().min >= 0 && unitRange.Value().min <= 32767, CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError(unitRange.Value().max >= 0 && unitRange.Value().max <= 32767, CHIP_ERROR_INVALID_ARGUMENT);
}
// If Unit is Degrees the maximum span range is 360 degrees.
if (unit == ClosureUnitEnum::kDegree)
{
VerifyOrReturnError(unitRange.Value().min >= -360 && unitRange.Value().min <= 360, CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError(unitRange.Value().max >= -360 && unitRange.Value().max <= 360, CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError((unitRange.Value().max - unitRange.Value().min) <= 360, CHIP_ERROR_INVALID_ARGUMENT);
}
// If the current unitRange is null, we need to set it to the new value
if (mUnitRange.IsNull())
{
mUnitRange.SetNonNull(unitRange.Value());
NotifyAttributeChanged(Attributes::UnitRange::Id);
return CHIP_NO_ERROR;
}
// If both the current unitRange and incoming unitRange are not null, we need to update if the values are different
if ((unitRange.Value().min != mUnitRange.Value().min) || (unitRange.Value().max != mUnitRange.Value().max))
{
mUnitRange.Value().min = unitRange.Value().min;
mUnitRange.Value().max = unitRange.Value().max;
NotifyAttributeChanged(Attributes::UnitRange::Id);
}
return CHIP_NO_ERROR;
}
CHIP_ERROR ClosureDimensionCluster::SetLimitRange(const Structs::RangePercent100thsStruct::Type & limitRange)
{
VerifyOrReturnError(mFeatureMap.Has(Feature::kLimitation), CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE);
// If the limit range is invalid, we need to return an error
VerifyOrReturnError(limitRange.min <= limitRange.max, CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError(limitRange.min <= kPercents100thsMaxValue, CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError(limitRange.max <= kPercents100thsMaxValue, CHIP_ERROR_INVALID_ARGUMENT);
// LimitRange.Min and LimitRange.Max SHALL be equal to an integer multiple of the Resolution attribute.
Percent100ths resolution = GetResolution();
VerifyOrReturnError(
limitRange.min % resolution == 0, CHIP_ERROR_INVALID_ARGUMENT,
ChipLogError(AppServer, "LimitRange.Min SHALL be equal to an integer multiple of the Resolution attribute."));
VerifyOrReturnError(
limitRange.max % resolution == 0, CHIP_ERROR_INVALID_ARGUMENT,
ChipLogError(AppServer, "LimitRange.Max SHALL be equal to an integer multiple of the Resolution attribute."));
if ((limitRange.min != mLimitRange.min) || (limitRange.max != mLimitRange.max))
{
mLimitRange.min = limitRange.min;
mLimitRange.max = limitRange.max;
NotifyAttributeChanged(Attributes::LimitRange::Id);
}
return CHIP_NO_ERROR;
}
Status ClosureDimensionCluster::HandleSetTargetCommand(Optional<Percent100ths> position, Optional<bool> latch,
Optional<Globals::ThreeLevelAutoEnum> speed)
{
// If all command parameters don't have a value, return InvalidCommand
VerifyOrReturnError(position.HasValue() || latch.HasValue() || speed.HasValue(), Status::InvalidCommand);
// TODO: If this command is sent while the closure is in a non-compatible internal-state, a status code of
// INVALID_IN_STATE SHALL be returned.
DataModel::Nullable<GenericDimensionStateStruct> targetState = GetTargetState();
// If targetState is null, we need to initialize to default value.
// This is to ensure that we can set the position, latch, and speed values in the targetState.
if (targetState.IsNull())
{
targetState.SetNonNull(GenericDimensionStateStruct{});
}
// If position field is present and Positioning(PS) feature is not supported, we should not set targetState.position value.
if (position.HasValue() && mFeatureMap.Has(Feature::kPositioning))
{
VerifyOrReturnError((position.Value() <= kPercents100thsMaxValue), Status::ConstraintError);
// If the Limitation Feature is active, the closure will automatically offset the TargetState.Position value to fit within
// LimitRange.Min and LimitRange.Max.
if (mFeatureMap.Has(Feature::kLimitation))
{
Structs::RangePercent100thsStruct::Type limitRange = GetLimitRange();
if (position.Value() > limitRange.max)
{
position.Value() = limitRange.max;
}
else if (position.Value() < limitRange.min)
{
position.Value() = limitRange.min;
}
}
Percent100ths resolution = GetResolution();
// Check if position.Value() is an integer multiple of resolution, else round to nearest valid value
if (position.Value() % resolution != 0)
{
Percent100ths roundedPosition =
static_cast<Percent100ths>(((position.Value() + resolution / 2) / resolution) * resolution);
ChipLogProgress(AppServer, "Rounding position from %u to nearest valid value %u based on resolution %u",
position.Value(), roundedPosition, resolution);
position.SetValue(roundedPosition);
}
targetState.Value().position.SetValue(DataModel::MakeNullable(position.Value()));
}
// If latch field is present and MotionLatching feature is not supported, we should not set targetState.latch value.
if (latch.HasValue() && mFeatureMap.Has(Feature::kMotionLatching))
{
// If latch value is true and the Remote Latching feature is not supported, or
// if latch value is false and the Remote Unlatching feature is not supported, return InvalidInState.
if ((latch.Value() && !mLatchControlModes.Has(LatchControlModesBitmap::kRemoteLatching)) ||
(!latch.Value() && !mLatchControlModes.Has(LatchControlModesBitmap::kRemoteUnlatching)))
{
return Status::InvalidInState;
}
targetState.Value().latch.SetValue(DataModel::MakeNullable(latch.Value()));
}
// If speed field is present and Speed feature is not supported, we should not set targetState.speed value.
if (speed.HasValue() && mFeatureMap.Has(Feature::kSpeed))
{
VerifyOrReturnError(speed.Value() != Globals::ThreeLevelAutoEnum::kUnknownEnumValue, Status::ConstraintError);
targetState.Value().speed.SetValue(speed.Value());
}
// Check if the current position is valid or else return InvalidInState
DataModel::Nullable<GenericDimensionStateStruct> currentState = GetCurrentState();
VerifyOrReturnError(!currentState.IsNull(), Status::InvalidInState);
if (mFeatureMap.Has(Feature::kPositioning))
{
VerifyOrReturnError(currentState.Value().position.HasValue() && !currentState.Value().position.Value().IsNull(),
Status::InvalidInState);
}
// If this command requests a position change while the Latch field of the CurrentState is True (Latched), and the Latch field
// of this command is not set to False (Unlatched), a status code of INVALID_IN_STATE SHALL be returned.
if (mFeatureMap.Has(Feature::kMotionLatching))
{
if (position.HasValue() && currentState.Value().latch.HasValue() && !currentState.Value().latch.Value().IsNull() &&
currentState.Value().latch.Value().Value())
{
VerifyOrReturnError(latch.HasValue() && !latch.Value(), Status::InvalidInState,
ChipLogError(AppServer,
"Latch is True in State, but SetTarget command does not set latch to False"
"when position change is requested on endpoint : %d",
GetEndpointId()));
}
}
// Target should only be set when delegate function returns status as Success. Return failure otherwise
VerifyOrReturnError(mDelegate.HandleSetTarget(position, latch, speed) == Status::Success, Status::Failure);
VerifyOrReturnError(SetTargetState(targetState) == CHIP_NO_ERROR, Status::Failure);
return Status::Success;
}
Status ClosureDimensionCluster::HandleStepCommand(StepDirectionEnum direction, uint16_t numberOfSteps,
Optional<Globals::ThreeLevelAutoEnum> speed)
{
// Return ConstraintError if command parameters are out of bounds
VerifyOrReturnError(direction != StepDirectionEnum::kUnknownEnumValue, Status::ConstraintError);
VerifyOrReturnError(numberOfSteps > 0, Status::ConstraintError);
DataModel::Nullable<GenericDimensionStateStruct> stepTarget = GetTargetState();
if (stepTarget.IsNull())
{
// If stepTarget is null, we need to initialize to default value.
// This is to ensure that we can set the position, latch, and speed values in the stepTarget.
stepTarget.SetNonNull(GenericDimensionStateStruct{});
}
// If speed field is present and Speed feature is not supported, we should not set stepTarget.speed value.
if (speed.HasValue() && mFeatureMap.Has(Feature::kSpeed))
{
VerifyOrReturnError(speed.Value() != Globals::ThreeLevelAutoEnum::kUnknownEnumValue, Status::ConstraintError);
stepTarget.Value().speed.SetValue(speed.Value());
}
// TODO: If the server is in a state where it cannot support the command, the server SHALL respond with an
// INVALID_IN_STATE response and the TargetState attribute value SHALL remain unchanged.
// Check if the current position is valid or else return InvalidInState
DataModel::Nullable<GenericDimensionStateStruct> currentState = GetCurrentState();
VerifyOrReturnError(!currentState.IsNull(), Status::InvalidInState);
VerifyOrReturnError(currentState.Value().position.HasValue() && !currentState.Value().position.Value().IsNull(),
Status::InvalidInState);
if (mFeatureMap.Has(Feature::kMotionLatching))
{
if (currentState.Value().latch.HasValue() && !currentState.Value().latch.Value().IsNull())
{
VerifyOrReturnError(!currentState.Value().latch.Value().Value(), Status::InvalidInState,
ChipLogError(AppServer,
"Step command cannot be processed when current latch is True"
"on endpoint : %d",
GetEndpointId()));
}
// Return InvalidInState if currentState is latched
}
// Derive TargetState Position from StepValue and NumberOfSteps.
Percent100ths stepValue = GetStepValue();
// Convert step to position delta.
// As StepValue can only take maxvalue of kPercents100thsMaxValue(which is 10000). Below product will be within limits of
// int32_t
uint32_t delta = numberOfSteps * stepValue;
uint32_t newPosition = 0;
// check if closure supports Limitation feature, if yes fetch the LimitRange values
bool limitSupported = mFeatureMap.Has(Feature::kLimitation) ? true : false;
Structs::RangePercent100thsStruct::Type limitRange;
if (limitSupported)
{
limitRange = GetLimitRange();
}
// Position = Position - NumberOfSteps * StepValue
uint32_t currentPosition = static_cast<uint32_t>(currentState.Value().position.Value().Value());
switch (direction)
{
case StepDirectionEnum::kDecrease:
// To avoid underflow, newPosition will be set to 0 if currentPosition is less than or equal to delta
newPosition = (currentPosition > delta) ? currentPosition - delta : 0;
// Position value SHALL be clamped to 0.00% if the LM feature is not supported or LimitRange.Min if the LM feature is
// supported.
newPosition = limitSupported ? std::max(newPosition, static_cast<uint32_t>(limitRange.min)) : newPosition;
break;
case StepDirectionEnum::kIncrease:
// To avoid overflow, newPosition will be set to UINT32_MAX if sum of currentPosition and delta is greater than UINT32_MAX
newPosition = (currentPosition > UINT32_MAX - delta) ? UINT32_MAX : currentPosition + delta;
// Position value SHALL be clamped to 0.00% if the LM feature is not supported or LimitRange.Max if the LM feature is
// supported.
newPosition = limitSupported ? std::min(newPosition, static_cast<uint32_t>(limitRange.max))
: std::min(newPosition, static_cast<uint32_t>(kPercents100thsMaxValue));
break;
default:
// Should never reach here due to earlier VerifyOrReturnError check
ChipLogError(AppServer, "Unhandled StepDirectionEnum value");
return Status::ConstraintError;
}
// TargetState should only be set when delegate function returns status as Success. Return failure otherwise
VerifyOrReturnError(mDelegate.HandleStep(direction, numberOfSteps, speed) == Status::Success, Status::Failure);
stepTarget.Value().position.SetValue(DataModel::MakeNullable(static_cast<Percent100ths>(newPosition)));
VerifyOrReturnError(SetTargetState(stepTarget) == CHIP_NO_ERROR, Status::Failure);
return Status::Success;
}
} // namespace ClosureDimension
} // namespace Clusters
} // namespace app
} // namespace chip