blob: c276fff2efbde5d499164e9c7780ba0e7c2c8e16 [file] [log] [blame]
/*
* Copyright (c) 2025 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <app/clusters/occupancy-sensor-server/OccupancySensingCluster.h>
#include <clusters/OccupancySensing/Events.h>
#include <algorithm>
#include <app/InteractionModelEngine.h>
#include <app/persistence/AttributePersistence.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <clusters/OccupancySensing/Attributes.h>
#include <clusters/OccupancySensing/Metadata.h>
#include <cstring>
#include <lib/support/CodeUtils.h>
namespace chip::app::Clusters {
using namespace OccupancySensing::Attributes;
using namespace chip::app::Clusters::OccupancySensing;
namespace {
// Mapping table from spec:
//
// | Feature Flag Value | Value of OccupancySensorTypeBitmap | Value of OccupancySensorType
// | PHY | US | PIR | ===================================== | ============================
// | 0 | 0 | 0 | PIR ^*^ | PIR
// | 0 | 0 | 1 | PIR | PIR
// | 0 | 1 | 0 | Ultrasonic | Ultrasonic
// | 0 | 1 | 1 | PIR + Ultrasonic | PIRAndUltrasonic
// | 1 | 0 | 0 | PhysicalContact | PhysicalContact
// | 1 | 0 | 1 | PhysicalContact + PIR | PIR
// | 1 | 1 | 0 | PhysicalContact + Ultrasonic | Ultrasonic
// | 1 | 1 | 1 | PhysicalContact + PIR + Ultrasonic | PIRAndUltrasonic
BitMask<OccupancySensing::OccupancySensorTypeBitmap>
FeaturesToOccupancySensorTypeBitmap(BitFlags<OccupancySensing::Feature> features)
{
BitMask<OccupancySensing::OccupancySensorTypeBitmap> occupancySensorTypeBitmap{};
occupancySensorTypeBitmap.Set(OccupancySensing::OccupancySensorTypeBitmap::kPir,
features.Has(OccupancySensing::Feature::kPassiveInfrared));
occupancySensorTypeBitmap.Set(OccupancySensing::OccupancySensorTypeBitmap::kUltrasonic,
features.Has(OccupancySensing::Feature::kUltrasonic));
occupancySensorTypeBitmap.Set(OccupancySensing::OccupancySensorTypeBitmap::kPhysicalContact,
features.Has(OccupancySensing::Feature::kPhysicalContact));
return occupancySensorTypeBitmap;
}
OccupancySensing::OccupancySensorTypeEnum FeaturesToOccupancySensorType(BitFlags<Feature> features)
{
// Note how the truth table in the comment at the top of this section has
// the PIR/US/PHY columns not in MSB-descending order. This is OK as we apply the correct
// bit weighing to make the table equal below.
unsigned maskFromFeatures = (features.Has(Feature::kPhysicalContact) ? (1 << 2) : 0) |
(features.Has(Feature::kUltrasonic) ? (1 << 1) : 0) | (features.Has(Feature::kPassiveInfrared) ? (1 << 0) : 0);
const OccupancySensorTypeEnum mappingTable[8] = {
// | PIR | US | PHY | Type value
OccupancySensorTypeEnum::kPir, // | 0 | 0 | 0 | PIR
OccupancySensorTypeEnum::kPir, // | 1 | 0 | 0 | PIR
OccupancySensorTypeEnum::kUltrasonic, // | 0 | 1 | 0 | Ultrasonic
OccupancySensorTypeEnum::kPIRAndUltrasonic, // | 1 | 1 | 0 | PIRAndUltrasonic
OccupancySensorTypeEnum::kPhysicalContact, // | 0 | 0 | 1 | PhysicalContact
OccupancySensorTypeEnum::kPir, // | 1 | 0 | 1 | PIR
OccupancySensorTypeEnum::kUltrasonic, // | 0 | 1 | 1 | Ultrasonic
OccupancySensorTypeEnum::kPIRAndUltrasonic, // | 1 | 1 | 1 | PIRAndUltrasonic
};
// This check is to ensure that no changes to the mapping table in the future can overrun.
if (maskFromFeatures >= sizeof(mappingTable))
{
return OccupancySensorTypeEnum::kPir;
}
return mappingTable[maskFromFeatures];
}
} // namespace
OccupancySensingCluster::OccupancySensingCluster(const Config & config) :
DefaultServerCluster({ config.mEndpointId, OccupancySensing::Id }), mHoldTimeDelegate(config.mHoldTimeDelegate),
mDelegate(config.mDelegate), mFeatureMap(config.mFeatureMap), mShowDeprecatedAttributes(config.mShowDeprecatedAttributes)
{
SetHoldTimeLimits(config.mHoldTimeLimits);
mHoldTime = std::clamp(config.mHoldTime, mHoldTimeLimits.holdTimeMin, mHoldTimeLimits.holdTimeMax);
}
CHIP_ERROR OccupancySensingCluster::Startup(ServerClusterContext & context)
{
ReturnErrorOnFailure(DefaultServerCluster::Startup(context));
if (mHoldTimeDelegate)
{
AttributePersistence persistence(context.attributeStorage);
uint16_t storedHoldTime;
if (persistence.LoadNativeEndianValue({ mPath.mEndpointId, OccupancySensing::Id, Attributes::HoldTime::Id }, storedHoldTime,
mHoldTime))
{
// A value was found in persistence. If stored value is not valid,
// SetHoldTime will return an error and we keep the current mHoldTime.
RETURN_SAFELY_IGNORED SetHoldTime(storedHoldTime);
}
}
return CHIP_NO_ERROR;
}
void OccupancySensingCluster::Shutdown(ClusterShutdownType shutdownType)
{
if (mHoldTimeDelegate)
{
mHoldTimeDelegate->CancelTimer(this);
}
DefaultServerCluster::Shutdown(shutdownType);
}
DataModel::ActionReturnStatus OccupancySensingCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder)
{
switch (request.path.mAttributeId)
{
case Attributes::FeatureMap::Id:
return encoder.Encode(mFeatureMap);
case Attributes::ClusterRevision::Id:
return encoder.Encode(OccupancySensing::kRevision);
case Attributes::Occupancy::Id:
return encoder.Encode(mOccupancy);
case Attributes::OccupancySensorType::Id:
return encoder.Encode(FeaturesToOccupancySensorType(mFeatureMap));
case Attributes::OccupancySensorTypeBitmap::Id:
return encoder.Encode(FeaturesToOccupancySensorTypeBitmap(mFeatureMap));
case Attributes::HoldTime::Id:
case Attributes::PIROccupiedToUnoccupiedDelay::Id:
case Attributes::UltrasonicOccupiedToUnoccupiedDelay::Id:
case Attributes::PhysicalContactOccupiedToUnoccupiedDelay::Id:
return encoder.Encode(mHoldTime);
case Attributes::HoldTimeLimits::Id:
return encoder.Encode(mHoldTimeLimits);
default:
return Protocols::InteractionModel::Status::UnsupportedAttribute;
}
}
DataModel::ActionReturnStatus OccupancySensingCluster::WriteAttribute(const DataModel::WriteAttributeRequest & request,
AttributeValueDecoder & aDecoder)
{
VerifyOrDie(request.path.mClusterId == OccupancySensing::Id);
switch (request.path.mAttributeId)
{
case Attributes::HoldTime::Id:
case Attributes::PIROccupiedToUnoccupiedDelay::Id:
case Attributes::UltrasonicOccupiedToUnoccupiedDelay::Id:
case Attributes::PhysicalContactOccupiedToUnoccupiedDelay::Id: {
if (!mHoldTimeDelegate)
{
return Protocols::InteractionModel::Status::UnsupportedAttribute;
}
uint16_t newHoldTime;
ReturnErrorOnFailure(aDecoder.Decode(newHoldTime));
return SetHoldTime(newHoldTime);
}
default:
return Protocols::InteractionModel::Status::UnsupportedAttribute;
}
}
CHIP_ERROR OccupancySensingCluster::Attributes(const ConcreteClusterPath & clusterPath,
ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
AttributeListBuilder listBuilder(builder);
const AttributeListBuilder::OptionalAttributeEntry optionalAttributes[] = {
{ mHoldTimeDelegate != nullptr, Attributes::HoldTime::kMetadataEntry },
{ mHoldTimeDelegate != nullptr, Attributes::HoldTimeLimits::kMetadataEntry },
{ mHoldTimeDelegate != nullptr && mShowDeprecatedAttributes && mFeatureMap.Has(Feature::kPassiveInfrared),
Attributes::PIROccupiedToUnoccupiedDelay::kMetadataEntry },
{ mHoldTimeDelegate != nullptr && mShowDeprecatedAttributes && mFeatureMap.Has(Feature::kUltrasonic),
Attributes::UltrasonicOccupiedToUnoccupiedDelay::kMetadataEntry },
{ mHoldTimeDelegate != nullptr && mShowDeprecatedAttributes && mFeatureMap.Has(Feature::kPhysicalContact),
Attributes::PhysicalContactOccupiedToUnoccupiedDelay::kMetadataEntry },
};
return listBuilder.Append(Span(OccupancySensing::Attributes::kMandatoryMetadata), Span(optionalAttributes));
}
DataModel::ActionReturnStatus OccupancySensingCluster::SetHoldTime(uint16_t holdTime)
{
VerifyOrReturnError(mHoldTimeDelegate, Protocols::InteractionModel::Status::UnsupportedAttribute);
VerifyOrReturnError(holdTime >= mHoldTimeLimits.holdTimeMin, Protocols::InteractionModel::Status::ConstraintError);
VerifyOrReturnError(holdTime <= mHoldTimeLimits.holdTimeMax, Protocols::InteractionModel::Status::ConstraintError);
if (mHoldTime == holdTime)
{
return DataModel::ActionReturnStatus::FixedStatus::kWriteSuccessNoOp;
}
mHoldTime = holdTime;
NotifyAttributeChanged(Attributes::HoldTime::Id);
// If a timer is currently active, we need to adjust its duration to reflect the new hold time.
if (mHoldTimeDelegate->IsTimerActive(this))
{
// Calculate the time that has already elapsed since the timer was started.
System::Clock::Timestamp now = mHoldTimeDelegate->GetCurrentMonotonicTimestamp();
System::Clock::Timestamp elapsedTime = now - mTimerStartedTimestamp;
System::Clock::Seconds16 newHoldTimeSeconds = System::Clock::Seconds16(mHoldTime);
// If the elapsed time is already greater than or equal to the new hold time,
// the timer should have already fired. Cancel the current timer and immediately
// set the state to unoccupied.
if (elapsedTime >= newHoldTimeSeconds)
{
mHoldTimeDelegate->CancelTimer(this);
DoSetOccupancy(false);
}
else
{
// Otherwise, restart the timer for the remaining duration.
System::Clock::Timeout remainingTime = newHoldTimeSeconds - elapsedTime;
// There's not much we can do it the timer fails to start. The Application is responsible for
// providing a working timer delegate, so we ignore the return value here.
RETURN_SAFELY_IGNORED mHoldTimeDelegate->StartTimer(this, remainingTime);
}
}
if (mDelegate)
{
mDelegate->OnHoldTimeChanged(holdTime);
}
if (mContext != nullptr)
{
// There's not much we can do if persistence fails here, so we ignore the return value.
// The application must ensure that persistence is working correctly.
RETURN_SAFELY_IGNORED mContext->attributeStorage.WriteValue(
{ mPath.mEndpointId, OccupancySensing::Id, Attributes::HoldTime::Id },
{ reinterpret_cast<const uint8_t *>(&mHoldTime), sizeof(mHoldTime) });
}
return Protocols::InteractionModel::Status::Success;
}
void OccupancySensingCluster::SetOccupancy(bool occupied)
{
if (mHoldTimeDelegate)
{
if (occupied)
{
mHoldTimeDelegate->CancelTimer(this);
DoSetOccupancy(true);
}
else
{
mTimerStartedTimestamp = mHoldTimeDelegate->GetCurrentMonotonicTimestamp();
// There's not much we can do it the timer fails to start. The Application is responsible for
// providing a working timer delegate, so we ignore the return value here.
RETURN_SAFELY_IGNORED mHoldTimeDelegate->StartTimer(this, System::Clock::Seconds16(mHoldTime));
}
return;
}
DoSetOccupancy(occupied);
}
void OccupancySensingCluster::TimerFired()
{
DoSetOccupancy(false);
}
void OccupancySensingCluster::DoSetOccupancy(bool occupied)
{
VerifyOrReturn(mOccupancy.Has(OccupancySensing::OccupancyBitmap::kOccupied) != occupied);
mOccupancy.Set(OccupancySensing::OccupancyBitmap::kOccupied, occupied);
NotifyAttributeChanged(Attributes::Occupancy::Id);
if (mDelegate)
{
mDelegate->OnOccupancyChanged(occupied);
}
if (mContext != nullptr)
{
Events::OccupancyChanged::Type event;
event.occupancy = mOccupancy;
mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
}
}
bool OccupancySensingCluster::IsOccupied() const
{
return mOccupancy.Has(OccupancySensing::OccupancyBitmap::kOccupied);
}
bool OccupancySensingCluster::IsHoldTimeEnabled() const
{
return mHoldTimeDelegate != nullptr;
}
void OccupancySensingCluster::SetHoldTimeLimits(const OccupancySensing::Structs::HoldTimeLimitsStruct::Type & holdTimeLimits)
{
auto newHoldTimeLimits = holdTimeLimits;
// Here we sanitize the input limits to ensure they are valid, in case the caller
// provided invalid values.
newHoldTimeLimits.holdTimeMin = std::max(static_cast<uint16_t>(1), newHoldTimeLimits.holdTimeMin);
newHoldTimeLimits.holdTimeMax =
std::max({ static_cast<uint16_t>(10), newHoldTimeLimits.holdTimeMin, newHoldTimeLimits.holdTimeMax });
newHoldTimeLimits.holdTimeDefault =
std::clamp(newHoldTimeLimits.holdTimeDefault, newHoldTimeLimits.holdTimeMin, newHoldTimeLimits.holdTimeMax);
if (mHoldTimeLimits.holdTimeMin != newHoldTimeLimits.holdTimeMin ||
mHoldTimeLimits.holdTimeMax != newHoldTimeLimits.holdTimeMax ||
mHoldTimeLimits.holdTimeDefault != newHoldTimeLimits.holdTimeDefault)
{
mHoldTimeLimits = newHoldTimeLimits;
NotifyAttributeChanged(Attributes::HoldTimeLimits::Id);
}
}
uint16_t OccupancySensingCluster::GetHoldTime() const
{
return mHoldTime;
}
const OccupancySensing::Structs::HoldTimeLimitsStruct::Type & OccupancySensingCluster::GetHoldTimeLimits() const
{
return mHoldTimeLimits;
}
BitFlags<OccupancySensing::Feature> OccupancySensingCluster::GetFeatureMap() const
{
return mFeatureMap;
}
} // namespace chip::app::Clusters