blob: 70d306162be3c4a90369c7c3887b2db3cb1fca7e [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 <app-common/zap-generated/attributes/Accessors.h>
#include <app-common/zap-generated/callback.h>
#include <app/clusters/window-covering-server/CodegenIntegration.h>
#include <app/clusters/window-covering-server/WindowCoveringCluster.h>
#include <app/clusters/window-covering-server/WindowCoveringDelegate.h>
#include <app/data-model-provider/AttributeChangeListener.h>
#include <app/static-cluster-config/WindowCovering.h>
#include <app/util/attribute-storage.h>
#include <data-model-providers/codegen/ClusterIntegration.h>
#include <data-model-providers/codegen/CodegenDataModelProvider.h>
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::WindowCovering;
using namespace chip::app::Clusters::WindowCovering::Attributes;
using chip::Protocols::InteractionModel::Status;
namespace {
constexpr size_t kWindowCoveringFixedClusterCount = WindowCovering::StaticApplicationConfig::kFixedClusterConfig.size();
constexpr size_t kWindowCoveringMaxClusterCount = kWindowCoveringFixedClusterCount + CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT;
// Helper subclass where Setters for fixed attributes Type and EndProductType are enabled
// This is done only for keeping the backwards compatibility with example apps
class CodegenWindowCoveringCluster : public WindowCoveringCluster
{
public:
using WindowCoveringCluster::SetEndProductType;
using WindowCoveringCluster::SetType;
using WindowCoveringCluster::WindowCoveringCluster;
};
LazyRegisteredServerCluster<CodegenWindowCoveringCluster> gServers[kWindowCoveringMaxClusterCount];
class WindowCoveringIntegrationDelegateWrapper final : public WindowCoveringDelegate
{
public:
void Init(EndpointId ep, WindowCoveringDelegate * wrapped)
{
mEndpoint = ep;
mWrapped = wrapped;
}
CHIP_ERROR HandleMovement(WindowCoveringType type) override
{
if (mWrapped)
{
return mWrapped->HandleMovement(type);
}
ChipLogProgress(Zcl, "WindowCovering has no delegate set for endpoint:%u", mEndpoint);
return CHIP_NO_ERROR;
}
CHIP_ERROR HandleStopMotion() override
{
if (mWrapped)
{
return mWrapped->HandleStopMotion();
}
ChipLogProgress(Zcl, "WindowCovering has no delegate set for endpoint:%u", mEndpoint);
return CHIP_NO_ERROR;
}
private:
WindowCoveringDelegate * mWrapped = nullptr;
};
WindowCoveringIntegrationDelegateWrapper gDelegateWrappers[kWindowCoveringMaxClusterCount];
// Helper for the derived class
CodegenWindowCoveringCluster * FindCodegenCluster(chip::EndpointId endpoint)
{
WindowCoveringCluster * baseCluster = WindowCovering::FindClusterOnEndpoint(endpoint);
return static_cast<CodegenWindowCoveringCluster *>(baseCluster);
}
class IntegrationDelegate : public CodegenClusterIntegration::Delegate
{
public:
ServerClusterRegistration & CreateRegistration(EndpointId endpointId, unsigned clusterInstanceIndex,
uint32_t optionalAttributeBits, uint32_t featureMap) override
{
BitFlags<WindowCovering::Feature> features;
features.SetRaw(featureMap);
// Build OptionalAttributeSet: only set feature-dependent attributes when the feature is present.
WindowCovering::OptionalAttributeSet optionalAttributes;
app::AttributeSet emberOptionals(optionalAttributeBits);
if (features.Has(Feature::kLift) && emberOptionals.IsSet(Attributes::NumberOfActuationsLift::Id))
{
optionalAttributes.Set<Attributes::NumberOfActuationsLift::Id>();
}
if (features.Has(Feature::kTilt) && emberOptionals.IsSet(Attributes::NumberOfActuationsTilt::Id))
{
optionalAttributes.Set<Attributes::NumberOfActuationsTilt::Id>();
}
if (features.HasAll(Feature::kLift, Feature::kPositionAwareLift) &&
emberOptionals.IsSet(Attributes::CurrentPositionLiftPercentage::Id))
{
optionalAttributes.Set<Attributes::CurrentPositionLiftPercentage::Id>();
}
if (features.HasAll(Feature::kTilt, Feature::kPositionAwareTilt) &&
emberOptionals.IsSet(Attributes::CurrentPositionTiltPercentage::Id))
{
optionalAttributes.Set<Attributes::CurrentPositionTiltPercentage::Id>();
}
if (emberOptionals.IsSet(Attributes::SafetyStatus::Id))
{
optionalAttributes.Set<Attributes::SafetyStatus::Id>();
}
gDelegateWrappers[clusterInstanceIndex].SetEndpoint(endpointId);
WindowCoveringCluster::Config config(gDelegateWrappers[clusterInstanceIndex]);
config.WithFeatures(features).WithOptionalAttributes(optionalAttributes);
// RAM attributes whose default is defined per-endpoint in the application Matter files
// are passed to the constructor via Config (per the Default Value Rule). If a default is
// not present in ZAP, the Config's own default (spec conformance value) is used.
WindowCovering::Type type{};
if (Attributes::Type::GetDefault(endpointId, &type) == Status::Success)
{
config.WithType(type);
}
WindowCovering::EndProductType endProductType{};
if (Attributes::EndProductType::GetDefault(endpointId, &endProductType) == Status::Success)
{
config.WithEndProductType(endProductType);
}
gServers[clusterInstanceIndex].Create(endpointId, config);
auto & cluster = gServers[clusterInstanceIndex].Cluster();
chip::BitMask<WindowCovering::ConfigStatus> configStatus;
if (Attributes::ConfigStatus::GetDefault(endpointId, &configStatus) == Status::Success)
{
cluster.SetConfigStatus(configStatus);
}
chip::BitMask<WindowCovering::Mode> mode;
if (Attributes::Mode::GetDefault(endpointId, &mode) == Status::Success)
{
cluster.SetMode(mode);
}
// Load feature-gated position attributes from ZAP defaults.
if (features.HasAll(Feature::kLift, Feature::kPositionAwareLift))
{
DataModel::Nullable<Percent100ths> percent100ths;
if (Attributes::CurrentPositionLiftPercent100ths::GetDefault(endpointId, percent100ths) == Status::Success)
{
cluster.SetCurrentPositionLiftPercent100ths(percent100ths);
}
if (Attributes::TargetPositionLiftPercent100ths::GetDefault(endpointId, percent100ths) == Status::Success)
{
cluster.SetTargetPositionLiftPercent100ths(percent100ths);
}
}
if (features.HasAll(Feature::kTilt, Feature::kPositionAwareTilt))
{
DataModel::Nullable<Percent100ths> percent100ths;
if (Attributes::CurrentPositionTiltPercent100ths::GetDefault(endpointId, percent100ths) == Status::Success)
{
cluster.SetCurrentPositionTiltPercent100ths(percent100ths);
}
if (Attributes::TargetPositionTiltPercent100ths::GetDefault(endpointId, percent100ths) == Status::Success)
{
cluster.SetTargetPositionTiltPercent100ths(percent100ths);
}
}
return gServers[clusterInstanceIndex].Registration();
}
ServerClusterInterface * FindRegistration(unsigned clusterInstanceIndex) override
{
VerifyOrReturnValue(gServers[clusterInstanceIndex].IsConstructed(), nullptr);
return &gServers[clusterInstanceIndex].Cluster();
}
void ReleaseRegistration(unsigned clusterInstanceIndex) override { gServers[clusterInstanceIndex].Destroy(); }
};
class WindowCoveringAttributeChangeListener final : public chip::app::DataModel::AttributeChangeListener
{
public:
void OnAttributeChanged(const chip::app::ConcreteAttributePath & path, chip::app::DataModel::AttributeChangeType type) override
{
if (path.mClusterId == chip::app::Clusters::WindowCovering::Id)
{
MatterWindowCoveringClusterServerAttributeChangedCallback(path);
}
}
};
WindowCoveringAttributeChangeListener gAttributeChangeListener;
uint16_t gActiveWindowCoveringEndpoints = 0;
} // namespace
void MatterWindowCoveringClusterInitCallback(EndpointId endpointId)
{
IntegrationDelegate integrationDelegate;
CodegenClusterIntegration::RegisterServer(
{
.endpointId = endpointId,
.clusterId = WindowCovering::Id,
.fixedClusterInstanceCount = kWindowCoveringFixedClusterCount,
.maxClusterInstanceCount = kWindowCoveringMaxClusterCount,
.fetchFeatureMap = true,
.fetchOptionalAttributes = true,
},
integrationDelegate);
if (gActiveWindowCoveringEndpoints == 0)
{
CodegenDataModelProvider::Instance().RegisterAttributeChangeListener(gAttributeChangeListener);
}
gActiveWindowCoveringEndpoints++;
}
void MatterWindowCoveringClusterShutdownCallback(EndpointId endpointId, MatterClusterShutdownType shutdownType)
{
IntegrationDelegate integrationDelegate;
CodegenClusterIntegration::UnregisterServer(
{
.endpointId = endpointId,
.clusterId = WindowCovering::Id,
.fixedClusterInstanceCount = kWindowCoveringFixedClusterCount,
.maxClusterInstanceCount = kWindowCoveringMaxClusterCount,
},
integrationDelegate, shutdownType);
if (gActiveWindowCoveringEndpoints > 0)
{
gActiveWindowCoveringEndpoints--;
if (gActiveWindowCoveringEndpoints == 0)
{
CodegenDataModelProvider::Instance().UnregisterAttributeChangeListener(gAttributeChangeListener);
}
}
}
namespace chip::app::Clusters::WindowCovering {
WindowCoveringCluster * FindClusterOnEndpoint(EndpointId endpointId)
{
IntegrationDelegate integrationDelegate;
ServerClusterInterface * cluster = CodegenClusterIntegration::FindClusterOnEndpoint(
{
.endpointId = endpointId,
.clusterId = WindowCovering::Id,
.fixedClusterInstanceCount = kWindowCoveringFixedClusterCount,
.maxClusterInstanceCount = kWindowCoveringMaxClusterCount,
},
integrationDelegate);
return static_cast<WindowCoveringCluster *>(cluster);
}
void SetDefaultDelegate(EndpointId endpointId, WindowCoveringDelegate * delegate)
{
uint16_t ep = emberAfGetClusterServerEndpointIndex(endpointId, WindowCovering::Id,
MATTER_DM_WINDOW_COVERING_CLUSTER_SERVER_ENDPOINT_COUNT);
if (ep < kWindowCoveringMaxClusterCount)
{
gDelegateWrappers[ep].Init(endpointId, delegate);
}
else
{
ChipLogProgress(Zcl, "Failed to set WindowCovering delegate for endpoint:%u", endpointId);
}
}
void TypeSet(chip::EndpointId endpoint, Type type)
{
auto cluster = FindCodegenCluster(endpoint);
VerifyOrDie(cluster != nullptr);
cluster->SetType(type);
}
Type TypeGet(chip::EndpointId endpoint)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
return cluster->GetType();
}
void ConfigStatusPrint(const chip::BitMask<ConfigStatus> & configStatus)
{
ChipLogProgress(Zcl, "ConfigStatus 0x%02X Operational=%u OnlineReserved=%u", configStatus.Raw(),
configStatus.Has(ConfigStatus::kOperational), configStatus.Has(ConfigStatus::kOnlineReserved));
ChipLogProgress(Zcl, "Lift(PA=%u Encoder=%u Reversed=%u) Tilt(PA=%u Encoder=%u)",
configStatus.Has(ConfigStatus::kLiftPositionAware), configStatus.Has(ConfigStatus::kLiftEncoderControlled),
configStatus.Has(ConfigStatus::kLiftMovementReversed), configStatus.Has(ConfigStatus::kTiltPositionAware),
configStatus.Has(ConfigStatus::kTiltEncoderControlled));
}
void ConfigStatusSet(chip::EndpointId endpoint, const chip::BitMask<ConfigStatus> & configStatus)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
cluster->SetConfigStatus(configStatus);
}
chip::BitMask<ConfigStatus> ConfigStatusGet(chip::EndpointId endpoint)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
return cluster->GetConfigStatus();
}
void ConfigStatusUpdateFeatures(chip::EndpointId endpoint)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
chip::BitMask<ConfigStatus> configStatus = ConfigStatusGet(endpoint);
configStatus.Set(ConfigStatus::kLiftPositionAware, cluster->GetFeatureMap().Has(Feature::kPositionAwareLift));
configStatus.Set(ConfigStatus::kTiltPositionAware, cluster->GetFeatureMap().Has(Feature::kPositionAwareTilt));
if (!cluster->GetFeatureMap().Has(Feature::kPositionAwareLift))
{
configStatus.Clear(ConfigStatus::kLiftEncoderControlled);
}
if (!cluster->GetFeatureMap().Has(Feature::kPositionAwareTilt))
{
configStatus.Clear(ConfigStatus::kTiltEncoderControlled);
}
ConfigStatusSet(endpoint, configStatus);
}
void OperationalStatusPrint(const chip::BitMask<OperationalStatus> & opStatus)
{
ChipLogProgress(Zcl, "OperationalStatus raw=0x%02X global=%u lift=%u tilt=%u", opStatus.Raw(),
opStatus.GetField(OperationalStatus::kGlobal), opStatus.GetField(OperationalStatus::kLift),
opStatus.GetField(OperationalStatus::kTilt));
}
chip::BitMask<OperationalStatus> OperationalStatusGet(chip::EndpointId endpoint)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
return cluster->GetOperationalStatus();
}
void OperationalStatusSet(chip::EndpointId endpoint, chip::BitMask<OperationalStatus> newStatus)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
chip::BitMask<OperationalStatus> prevStatus = cluster->GetOperationalStatus();
// Filter changes
if (newStatus != prevStatus)
{
OperationalStatusPrint(newStatus);
cluster->SetOperationalStatus(newStatus);
}
}
void OperationalStateSet(chip::EndpointId endpoint, const chip::BitMask<OperationalStatus> field, OperationalState state)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
chip::BitMask<OperationalStatus> status = cluster->GetOperationalStatus();
/* Filter only Lift or Tilt action since we cannot allow global reflecting a state alone */
if ((OperationalStatus::kLift == field) || (OperationalStatus::kTilt == field))
{
status.SetField(field, static_cast<uint8_t>(state));
/* Global Always follow Lift by priority or therefore fallback to Tilt */
chip::BitMask<OperationalStatus> opGlobal =
status.HasAny(OperationalStatus::kLift) ? OperationalStatus::kLift : OperationalStatus::kTilt;
status.SetField(OperationalStatus::kGlobal, status.GetField(opGlobal));
OperationalStatusSet(endpoint, status);
}
}
OperationalState OperationalStateGet(chip::EndpointId endpoint, const chip::BitMask<OperationalStatus> field)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
chip::BitMask<OperationalStatus> status = cluster->GetOperationalStatus();
return static_cast<OperationalState>(status.GetField(field));
}
void EndProductTypeSet(chip::EndpointId endpoint, EndProductType type)
{
auto cluster = FindCodegenCluster(endpoint);
VerifyOrDie(cluster != nullptr);
cluster->SetEndProductType(type);
}
EndProductType EndProductTypeGet(chip::EndpointId endpoint)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
return cluster->GetEndProductType();
}
void ModeSet(chip::EndpointId endpoint, chip::BitMask<Mode> & newMode)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
cluster->SetMode(newMode);
}
chip::BitMask<Mode> ModeGet(chip::EndpointId endpoint)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
return cluster->GetMode();
}
void ModePrint(const chip::BitMask<Mode> & mode)
{
ChipLogProgress(Zcl, "Mode 0x%02X MotorDirReversed=%u LedFeedback=%u Maintenance=%u Calibration=%u", mode.Raw(),
mode.Has(Mode::kMotorDirectionReversed), mode.Has(Mode::kLedFeedback), mode.Has(Mode::kMaintenanceMode),
mode.Has(Mode::kCalibrationMode));
}
void LiftPositionSet(chip::EndpointId endpoint, NPercent100ths percent100ths)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
if (percent100ths.IsNull())
{
ChipLogProgress(Zcl, "Lift[%u] Position Set to Null", endpoint);
}
else
{
ChipLogProgress(Zcl, "Lift[%u] Position Set: %u", endpoint, percent100ths.Value());
}
cluster->SetCurrentPositionLiftPercent100ths(percent100ths);
}
void TiltPositionSet(chip::EndpointId endpoint, NPercent100ths percent100ths)
{
auto cluster = FindClusterOnEndpoint(endpoint);
VerifyOrDie(cluster != nullptr);
if (percent100ths.IsNull())
{
ChipLogProgress(Zcl, "Tilt[%u] Position Set to Null", endpoint);
}
else
{
ChipLogProgress(Zcl, "Tilt[%u] Position Set: %u", endpoint, percent100ths.Value());
}
cluster->SetCurrentPositionTiltPercent100ths(percent100ths);
}
OperationalState ComputeOperationalState(uint16_t target, uint16_t current)
{
OperationalState opState = OperationalState::Stall;
if (current != target)
{
opState = (current < target) ? OperationalState::MovingDownOrClose : OperationalState::MovingUpOrOpen;
}
return opState;
}
OperationalState ComputeOperationalState(NPercent100ths target, NPercent100ths current)
{
if (!current.IsNull() && !target.IsNull())
{
return ComputeOperationalState(target.Value(), current.Value());
}
return OperationalState::Stall;
}
LimitStatus CheckLimitState(uint16_t position, AbsoluteLimits limits)
{
if (limits.open > limits.closed)
{
return LimitStatus::Inverted;
}
if (position == limits.open)
{
return LimitStatus::IsUpOrOpen;
}
if (position == limits.closed)
{
return LimitStatus::IsDownOrClose;
}
if ((limits.open > 0) && (position < limits.open))
{
return LimitStatus::IsPastUpOrOpen;
}
if ((limits.closed > 0) && (position > limits.closed))
{
return LimitStatus::IsPastDownOrClose;
}
return LimitStatus::Intermediate;
}
/*
* ConvertValue: Converts values from one range to another
* Range In -> from inputLowValue to inputHighValue
* Range Out -> from outputLowValue to outputHighValue
*/
uint16_t ConvertValue(uint16_t inputLowValue, uint16_t inputHighValue, uint16_t outputLowValue, uint16_t outputHighValue,
uint16_t value)
{
uint16_t inputMin = inputLowValue, inputMax = inputHighValue, inputRange = UINT16_MAX;
uint16_t outputMin = outputLowValue, outputMax = outputHighValue, outputRange = UINT16_MAX;
if (inputLowValue > inputHighValue)
{
inputMin = inputHighValue;
inputMax = inputLowValue;
}
if (outputLowValue > outputHighValue)
{
outputMin = outputHighValue;
outputMax = outputLowValue;
}
inputRange = static_cast<uint16_t>(inputMax - inputMin);
outputRange = static_cast<uint16_t>(outputMax - outputMin);
if (value < inputMin)
{
return outputMin;
}
if (value > inputMax)
{
return outputMax;
}
if (inputRange > 0)
{
return static_cast<uint16_t>(outputMin + ((outputRange * (value - inputMin) / inputRange)));
}
return outputMax;
}
uint16_t Percent100thsToValue(AbsoluteLimits limits, Percent100ths relative)
{
return ConvertValue(kWcPercent100thsMinOpen, kWcPercent100thsMaxClosed, limits.open, limits.closed, relative);
}
Percent100ths ComputePercent100thsStep(OperationalState direction, Percent100ths previous, Percent100ths delta)
{
Percent100ths percent100ths = previous;
switch (direction)
{
case OperationalState::MovingDownOrClose:
if (percent100ths < (kWcPercent100thsMaxClosed - delta))
{
percent100ths = static_cast<Percent100ths>(percent100ths + delta);
}
else
{
percent100ths = kWcPercent100thsMaxClosed;
}
break;
case OperationalState::MovingUpOrOpen:
if (percent100ths > (kWcPercent100thsMinOpen + delta))
{
percent100ths = static_cast<Percent100ths>(percent100ths - delta);
}
else
{
percent100ths = kWcPercent100thsMinOpen;
}
break;
default:
// nothing to do we keep previous value, simple passthrough
break;
}
if (percent100ths > kWcPercent100thsMaxClosed)
{
return kWcPercent100thsMaxClosed;
}
return percent100ths;
}
void PostAttributeChange(chip::EndpointId endpoint, chip::AttributeId attributeId)
{
BitMask<Mode> mode;
BitMask<ConfigStatus> configStatus;
ChipLogProgress(Zcl, "WC POST ATTRIBUTE=%u", (unsigned int) attributeId);
switch (attributeId)
{
case Attributes::Mode::Id:
mode = ModeGet(endpoint);
ModePrint(mode);
ModeSet(endpoint, mode);
break;
case Attributes::ConfigStatus::Id:
configStatus = ConfigStatusGet(endpoint);
ConfigStatusPrint(configStatus);
break;
default:
break;
}
}
} // namespace chip::app::Clusters::WindowCovering
/**
* @brief Cluster Plugin Init Callback
*/
void MatterWindowCoveringPluginServerInitCallback() {}
void MatterWindowCoveringPluginServerShutdownCallback() {}
void __attribute__((weak))
MatterWindowCoveringClusterServerAttributeChangedCallback(const chip::app::ConcreteAttributePath & attributePath)
{
chip::app::Clusters::WindowCovering::PostAttributeChange(attributePath.mEndpointId, attributePath.mAttributeId);
}