blob: c082900368c9991ec168a42716417fdcb9bb881b [file]
/*
*
* Copyright (c) 2023-2025 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/clusters/device-energy-management-server/DeviceEnergyManagementCluster.h>
#include <app/data-model/Decode.h>
#include <app/data-model/Encode.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <clusters/DeviceEnergyManagement/Attributes.h>
#include <clusters/DeviceEnergyManagement/Commands.h>
#include <clusters/DeviceEnergyManagement/Metadata.h>
#include <clusters/DeviceEnergyManagement/Structs.h>
#include <lib/support/logging/CHIPLogging.h>
#include <protocols/interaction_model/StatusCode.h>
#include <system/SystemClock.h>
using chip::Protocols::InteractionModel::Status;
namespace chip {
namespace app {
namespace Clusters {
namespace DeviceEnergyManagement {
ForecastUpdateReasonEnum AdjustmentCauseToForecastUpdateReason(AdjustmentCauseEnum cause)
{
switch (cause)
{
case AdjustmentCauseEnum::kLocalOptimization:
return ForecastUpdateReasonEnum::kLocalOptimization;
case AdjustmentCauseEnum::kGridOptimization:
return ForecastUpdateReasonEnum::kGridOptimization;
default:
return ForecastUpdateReasonEnum::kInternalOptimization;
}
}
PowerAdjustReasonEnum AdjustmentCauseToPowerAdjustReason(AdjustmentCauseEnum cause)
{
switch (cause)
{
case AdjustmentCauseEnum::kLocalOptimization:
return PowerAdjustReasonEnum::kLocalOptimizationAdjustment;
case AdjustmentCauseEnum::kGridOptimization:
return PowerAdjustReasonEnum::kGridOptimizationAdjustment;
default:
return PowerAdjustReasonEnum::kUnknownEnumValue;
}
}
} // namespace DeviceEnergyManagement
namespace {
bool IsWithinRange(const int64_t power, const uint32_t duration,
const DeviceEnergyManagement::Structs::PowerAdjustCapabilityStruct::Type & powerAdjustmentCapability)
{
if (powerAdjustmentCapability.powerAdjustCapability.IsNull())
{
return false;
}
for (const auto & pas : powerAdjustmentCapability.powerAdjustCapability.Value())
{
if ((power >= pas.minPower) && (duration >= pas.minDuration) && (power <= pas.maxPower) && (duration <= pas.maxDuration))
{
return true;
}
}
return false;
}
// Helper function to validate that the ESA is in the expected state before processing a command
DataModel::ActionReturnStatus ValidateESAState(const DataModel::InvokeRequest & request, CommandHandler * handler,
DeviceEnergyManagement::Delegate & delegate,
DeviceEnergyManagement::ESAStateEnum expectedState)
{
DeviceEnergyManagement::ESAStateEnum currentState = delegate.GetESAState();
if (currentState != expectedState)
{
ChipLogError(Zcl, "DEM: ESAState mismatch - expected %d, got %d", static_cast<int>(expectedState),
static_cast<int>(currentState));
return Status::InvalidInState;
}
return Status::Success;
}
} // namespace
using namespace DeviceEnergyManagement;
using namespace DeviceEnergyManagement::Attributes;
CHIP_ERROR DeviceEnergyManagementCluster::Startup(ServerClusterContext & context)
{
if (mDelegate.GetEndpointId() != mPath.mEndpointId)
{
ChipLogError(Zcl, "DEM: EndpointId mismatch - delegate has %d, cluster has %d", mDelegate.GetEndpointId(),
mPath.mEndpointId);
return CHIP_ERROR_INVALID_ARGUMENT;
}
return DefaultServerCluster::Startup(context);
}
DataModel::ActionReturnStatus DeviceEnergyManagementCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder)
{
switch (request.path.mAttributeId)
{
case FeatureMap::Id:
return encoder.Encode(mFeatureFlags);
case ClusterRevision::Id:
return encoder.Encode(kRevision);
case ESAType::Id:
return encoder.Encode(mDelegate.GetESAType());
case ESACanGenerate::Id:
return encoder.Encode(mDelegate.GetESACanGenerate());
case ESAState::Id:
return encoder.Encode(mDelegate.GetESAState());
case AbsMinPower::Id:
return encoder.Encode(mDelegate.GetAbsMinPower());
case AbsMaxPower::Id:
return encoder.Encode(mDelegate.GetAbsMaxPower());
case PowerAdjustmentCapability::Id:
return encoder.Encode(mDelegate.GetPowerAdjustmentCapability());
case Forecast::Id:
return encoder.Encode(mDelegate.GetForecast());
case OptOutState::Id:
return encoder.Encode(mDelegate.GetOptOutState());
default:
return Status::UnsupportedAttribute;
}
}
std::optional<DataModel::ActionReturnStatus> DeviceEnergyManagementCluster::InvokeCommand(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
using namespace Commands;
switch (request.path.mCommandId)
{
case PowerAdjustRequest::Id:
return HandlePowerAdjustRequest(request, input_arguments, handler);
case CancelPowerAdjustRequest::Id:
return HandleCancelPowerAdjustRequest(request, input_arguments, handler);
case StartTimeAdjustRequest::Id:
return HandleStartTimeAdjustRequest(request, input_arguments, handler);
case PauseRequest::Id:
return HandlePauseRequest(request, input_arguments, handler);
case ResumeRequest::Id:
return HandleResumeRequest(request, input_arguments, handler);
case ModifyForecastRequest::Id:
return HandleModifyForecastRequest(request, input_arguments, handler);
case RequestConstraintBasedForecast::Id:
return HandleRequestConstraintBasedForecast(request, input_arguments, handler);
case CancelRequest::Id:
return HandleCancelRequest(request, input_arguments, handler);
default:
return Status::UnsupportedCommand;
}
}
CHIP_ERROR DeviceEnergyManagementCluster::Attributes(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
static constexpr DataModel::AttributeEntry optionalAttributes[] = {
PowerAdjustmentCapability::kMetadataEntry,
Forecast::kMetadataEntry,
OptOutState::kMetadataEntry,
};
AttributeListBuilder listBuilder(builder);
return listBuilder.Append(Span(kMandatoryMetadata), Span(optionalAttributes), mEnabledOptionalAttributes);
}
CHIP_ERROR DeviceEnergyManagementCluster::AcceptedCommands(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
{
using namespace Commands;
if (mFeatureFlags.Has(Feature::kPowerAdjustment))
{
ReturnErrorOnFailure(builder.AppendElements({
PowerAdjustRequest::kMetadataEntry,
CancelPowerAdjustRequest::kMetadataEntry,
}));
}
if (mFeatureFlags.Has(Feature::kStartTimeAdjustment))
{
ReturnErrorOnFailure(builder.AppendElements({ StartTimeAdjustRequest::kMetadataEntry }));
}
if (mFeatureFlags.Has(Feature::kPausable))
{
ReturnErrorOnFailure(builder.AppendElements({
PauseRequest::kMetadataEntry,
ResumeRequest::kMetadataEntry,
}));
}
if (mFeatureFlags.Has(Feature::kForecastAdjustment))
{
ReturnErrorOnFailure(builder.AppendElements({ ModifyForecastRequest::kMetadataEntry }));
}
if (mFeatureFlags.Has(Feature::kConstraintBasedAdjustment))
{
ReturnErrorOnFailure(builder.AppendElements({ RequestConstraintBasedForecast::kMetadataEntry }));
}
if (mFeatureFlags.HasAny(Feature::kStartTimeAdjustment, Feature::kForecastAdjustment, Feature::kConstraintBasedAdjustment))
{
ReturnErrorOnFailure(builder.AppendElements({ CancelRequest::kMetadataEntry }));
}
return CHIP_NO_ERROR;
}
DataModel::ActionReturnStatus
DeviceEnergyManagementCluster::CheckOptOutAllowsRequest(DeviceEnergyManagement::AdjustmentCauseEnum adjustmentCause)
{
OptOutStateEnum optOutState = mDelegate.GetOptOutState();
if (adjustmentCause == AdjustmentCauseEnum::kUnknownEnumValue)
{
ChipLogError(Zcl, "DEM: adjustment cause is invalid (%d)", static_cast<int>(adjustmentCause));
return Status::InvalidValue;
}
switch (optOutState)
{
case OptOutStateEnum::kNoOptOut: /* User has NOT opted out so allow it */
ChipLogProgress(Zcl, "DEM: OptOutState = kNoOptOut");
return Status::Success;
case OptOutStateEnum::kLocalOptOut: /* User has opted out from Local only*/
ChipLogProgress(Zcl, "DEM: OptOutState = kLocalOptOut");
switch (adjustmentCause)
{
case AdjustmentCauseEnum::kGridOptimization:
return Status::Success;
case AdjustmentCauseEnum::kLocalOptimization:
default:
return Status::ConstraintError;
}
case OptOutStateEnum::kGridOptOut: /* User has opted out from Grid only */
ChipLogProgress(Zcl, "DEM: OptOutState = kGridOptOut");
switch (adjustmentCause)
{
case AdjustmentCauseEnum::kLocalOptimization:
return Status::Success;
case AdjustmentCauseEnum::kGridOptimization:
default:
return Status::ConstraintError;
}
case OptOutStateEnum::kOptOut: /* User has opted out from both local and grid */
ChipLogProgress(Zcl, "DEM: OptOutState = kOptOut");
return Status::ConstraintError;
default:
ChipLogError(Zcl, "DEM: invalid optOutState %d", static_cast<int>(optOutState));
return Status::InvalidValue;
}
}
DataModel::ActionReturnStatus DeviceEnergyManagementCluster::HandlePowerAdjustRequest(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
using namespace Commands;
PowerAdjustRequest::DecodableType commandData;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData));
ReturnErrorOnFailure(CheckOptOutAllowsRequest(commandData.cause).GetUnderlyingError());
DataModel::Nullable<Structs::PowerAdjustCapabilityStruct::Type> powerAdjustmentCapability =
mDelegate.GetPowerAdjustmentCapability();
if (powerAdjustmentCapability.IsNull())
{
ChipLogError(Zcl, "DEM: PowerAdjustmentCapability is Null");
return Status::ConstraintError;
}
if (!IsWithinRange(commandData.power, commandData.duration, powerAdjustmentCapability.Value()))
{
ChipLogError(Zcl, "DEM: Power is not within range- power: '%" PRIdLEAST64 "', duration: '%" PRIdLEAST32 "'",
commandData.power, commandData.duration);
return Status::ConstraintError;
}
ESAStateEnum ESAState = mDelegate.GetESAState();
if (ESAState != ESAStateEnum::kOnline && ESAState != ESAStateEnum::kPowerAdjustActive)
{
return Status::InvalidInState;
}
// Call on delegate to start the power adjustment if the ESAState PowerAdjustActive, the delegate might refuse the adjustment
// The delegate is responsible of updating its PowerAdjustmentCapability's cause to the new cause if the adjustment is accepted
ReturnErrorOnFailure(
DataModel::ActionReturnStatus(mDelegate.PowerAdjustRequest(commandData.power, commandData.duration, commandData.cause))
.GetUnderlyingError());
// Verify the delegate's PowerAdjustmentCapability's cause was updated to the new cause if the adjustment is accepted
powerAdjustmentCapability = mDelegate.GetPowerAdjustmentCapability();
if (powerAdjustmentCapability.IsNull())
{
ChipLogError(Zcl, "DEM: PowerAdjustmentCapability is Null");
return Status::ConstraintError;
}
PowerAdjustReasonEnum expectedCause = AdjustmentCauseToPowerAdjustReason(commandData.cause);
if (powerAdjustmentCapability.Value().cause != expectedCause)
{
ChipLogError(Zcl,
"DEM: PowerAdjustmentCapability's cause was not updated to the new cause- expected: '0x%" PRIx32
"', got: '0x%" PRIx32 "'",
static_cast<uint32_t>(expectedCause), static_cast<uint32_t>(powerAdjustmentCapability.Value().cause));
return Status::ConstraintError;
}
return Status::Success;
}
DataModel::ActionReturnStatus
DeviceEnergyManagementCluster::HandleCancelPowerAdjustRequest(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments, CommandHandler * handler)
{
using namespace Commands;
CancelPowerAdjustRequest::DecodableType commandData;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData));
ReturnErrorOnFailure(ValidateESAState(request, handler, mDelegate, ESAStateEnum::kPowerAdjustActive).GetUnderlyingError());
ReturnErrorOnFailure(DataModel::ActionReturnStatus(mDelegate.CancelPowerAdjustRequest()).GetUnderlyingError());
// Verify the delegate's PowerAdjustmentCapability's cause was updated to the new cause if the adjustment is accepted
DataModel::Nullable<Structs::PowerAdjustCapabilityStruct::Type> powerAdjustmentCapability =
mDelegate.GetPowerAdjustmentCapability();
if (powerAdjustmentCapability.IsNull())
{
ChipLogError(Zcl, "DEM: PowerAdjustmentCapability is Null");
return Status::ConstraintError;
}
if (powerAdjustmentCapability.Value().cause != PowerAdjustReasonEnum::kNoAdjustment)
{
ChipLogError(Zcl,
"DEM: PowerAdjustmentCapability's cause was not updated to the new cause- expected: '0x%" PRIx32
"', got: '0x%" PRIx32 "'",
static_cast<uint32_t>(PowerAdjustReasonEnum::kNoAdjustment),
static_cast<uint32_t>(powerAdjustmentCapability.Value().cause));
return Status::ConstraintError;
}
return Status::Success;
}
DataModel::ActionReturnStatus DeviceEnergyManagementCluster::HandleStartTimeAdjustRequest(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
using namespace Commands;
StartTimeAdjustRequest::DecodableType commandData;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData));
ReturnErrorOnFailure(CheckOptOutAllowsRequest(commandData.cause).GetUnderlyingError());
DataModel::Nullable<Structs::ForecastStruct::Type> forecastNullable = mDelegate.GetForecast();
if (forecastNullable.IsNull())
{
ChipLogError(Zcl, "DEM: Forecast is Null");
return Status::Failure;
}
auto & forecast = forecastNullable.Value();
// earliestStartTime is optional based on the StartTimeAdjust (STA) feature AND is nullable
if (!forecast.earliestStartTime.HasValue() || !forecast.latestEndTime.HasValue())
{
ChipLogError(Zcl, "DEM: EarliestStartTime / LatestEndTime do not have values");
return Status::Failure;
}
DataModel::Nullable<uint32_t> & earliestStartTimeNullable = forecast.earliestStartTime.Value();
uint32_t latestEndTimeEpoch = forecast.latestEndTime.Value();
uint32_t earliestStartTimeEpoch;
if (earliestStartTimeNullable.IsNull())
{
uint32_t matterEpoch = 0;
CHIP_ERROR err = System::Clock::GetClock_MatterEpochS(matterEpoch);
if (err != CHIP_NO_ERROR)
{
ChipLogError(Zcl, "DEM: Unable to get current time - err:%" CHIP_ERROR_FORMAT, err.Format());
return Status::Failure;
}
earliestStartTimeEpoch = matterEpoch; // Null means we can start immediately (NOW)
}
else
{
earliestStartTimeEpoch = earliestStartTimeNullable.Value();
}
uint32_t duration = forecast.endTime - forecast.startTime;
if (commandData.requestedStartTime < earliestStartTimeEpoch)
{
ChipLogError(Zcl, "DEM: Bad requestedStartTime %" PRIu32 ", earlier than earliestStartTime %" PRIu32 ".",
commandData.requestedStartTime, earliestStartTimeEpoch);
return Status::ConstraintError;
}
if ((commandData.requestedStartTime + duration) > latestEndTimeEpoch)
{
ChipLogError(Zcl, "DEM: Bad requestedStartTimeEpoch + duration %" PRIu32 ", later than latestEndTime %" PRIu32 ".",
commandData.requestedStartTime + duration, latestEndTimeEpoch);
return Status::ConstraintError;
}
// Store original forecastID to verify it was incremented
uint32_t originalForecastID = forecast.forecastID;
ReturnErrorOnFailure(
DataModel::ActionReturnStatus(mDelegate.StartTimeAdjustRequest(commandData.requestedStartTime, commandData.cause))
.GetUnderlyingError());
// Verify the delegate updated the Forecast attribute as required
DataModel::Nullable<Structs::ForecastStruct::Type> updatedForecastNullable = mDelegate.GetForecast();
if (updatedForecastNullable.IsNull())
{
ChipLogError(Zcl, "DEM: Forecast is Null after StartTimeAdjustRequest");
return Status::Failure;
}
auto & updatedForecast = updatedForecastNullable.Value();
// Verify Forecast.startTime was updated to requestedStartTime
if (updatedForecast.startTime != commandData.requestedStartTime)
{
ChipLogError(Zcl, "DEM: Forecast.startTime was not updated to requestedStartTime - expected: %" PRIu32 ", got: %" PRIu32,
commandData.requestedStartTime, updatedForecast.startTime);
return Status::ConstraintError;
}
// Verify Forecast.forecastID was incremented (new ForecastID)
if (updatedForecast.forecastID <= originalForecastID)
{
ChipLogError(Zcl, "DEM: Forecast.forecastID was not incremented - original: %" PRIu32 ", got: %" PRIu32, originalForecastID,
updatedForecast.forecastID);
return Status::ConstraintError;
}
// Verify Forecast.endTime was updated to requestedStartTime + duration
uint32_t expectedEndTime = commandData.requestedStartTime + duration;
if (updatedForecast.endTime != expectedEndTime)
{
ChipLogError(Zcl, "DEM: Forecast.endTime was not updated correctly - expected: %" PRIu32 ", got: %" PRIu32, expectedEndTime,
updatedForecast.endTime);
return Status::ConstraintError;
}
return Status::Success;
}
DataModel::ActionReturnStatus DeviceEnergyManagementCluster::HandlePauseRequest(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
using namespace Commands;
PauseRequest::DecodableType commandData;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData));
ReturnErrorOnFailure(CheckOptOutAllowsRequest(commandData.cause).GetUnderlyingError());
VerifyOrReturnError(mDelegate.GetESAState() == ESAStateEnum::kOnline ||
mDelegate.GetESAState() == ESAStateEnum::kPowerAdjustActive ||
mDelegate.GetESAState() == ESAStateEnum::kPaused,
Status::ConstraintError);
DataModel::Nullable<Structs::ForecastStruct::Type> forecast = mDelegate.GetForecast();
if (forecast.IsNull())
{
ChipLogError(Zcl, "DEM: Forecast is Null");
return Status::Failure;
}
// Value SHALL be between the MinPauseDuration and MaxPauseDuration indicated in the
// ActiveSlotNumber index in the Slots list in the Forecast
if (forecast.Value().activeSlotNumber.IsNull())
{
ChipLogError(Zcl, "DEM: activeSlotNumber Is Null");
return Status::Failure;
}
uint16_t activeSlotNumber = forecast.Value().activeSlotNumber.Value();
if (activeSlotNumber >= forecast.Value().slots.size())
{
ChipLogError(Zcl, "DEM: Bad activeSlotNumber %d, size()=%d.", activeSlotNumber,
static_cast<int>(forecast.Value().slots.size()));
return Status::Failure;
}
const auto & activeSlot = forecast.Value().slots[activeSlotNumber];
if (!activeSlot.slotIsPausable.HasValue())
{
ChipLogError(Zcl, "DEM: activeSlotNumber %d does not include slotIsPausable.", activeSlotNumber);
return Status::Failure;
}
if (!activeSlot.minPauseDuration.HasValue())
{
ChipLogError(Zcl, "DEM: activeSlotNumber %d does not include minPauseDuration.", activeSlotNumber);
return Status::Failure;
}
if (!activeSlot.maxPauseDuration.HasValue())
{
ChipLogError(Zcl, "DEM: activeSlotNumber %d does not include maxPauseDuration.", activeSlotNumber);
return Status::Failure;
}
if (!activeSlot.slotIsPausable.Value())
{
ChipLogError(Zcl, "DEM: activeSlotNumber %d is NOT pausable.", activeSlotNumber);
return Status::Failure;
}
if ((commandData.duration < activeSlot.minPauseDuration.Value()) ||
(commandData.duration > activeSlot.maxPauseDuration.Value()))
{
ChipLogError(Zcl, "DEM: out of range pause duration %" PRIu32, commandData.duration);
return Status::ConstraintError;
}
return mDelegate.PauseRequest(commandData.duration, commandData.cause);
}
DataModel::ActionReturnStatus DeviceEnergyManagementCluster::HandleResumeRequest(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
using namespace Commands;
ResumeRequest::DecodableType commandData;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData));
ReturnErrorOnFailure(ValidateESAState(request, handler, mDelegate, ESAStateEnum::kPaused).GetUnderlyingError());
ReturnErrorOnFailure(DataModel::ActionReturnStatus(mDelegate.ResumeRequest()).GetUnderlyingError());
// Verify the Delegate updated its state and ForecastUpdateReason
VerifyOrReturnError(mDelegate.GetESAState() != ESAStateEnum::kPaused, Status::InvalidInState);
DataModel::Nullable<Structs::ForecastStruct::Type> forecast = mDelegate.GetForecast();
VerifyOrReturnError(!forecast.IsNull(), Status::Failure);
VerifyOrReturnError(forecast.Value().forecastUpdateReason == ForecastUpdateReasonEnum::kInternalOptimization,
Status::InvalidInState);
return Status::Success;
}
DataModel::ActionReturnStatus DeviceEnergyManagementCluster::HandleModifyForecastRequest(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
using namespace Commands;
ModifyForecastRequest::DecodableType commandData;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData));
ReturnErrorOnFailure(CheckOptOutAllowsRequest(commandData.cause).GetUnderlyingError());
DataModel::Nullable<Structs::ForecastStruct::Type> forecast = mDelegate.GetForecast();
if (forecast.IsNull())
{
ChipLogError(Zcl, "DEM: Forecast is Null");
return Status::Failure;
}
// Check the various values in the slot structures
auto iterator = commandData.slotAdjustments.begin();
while (iterator.Next())
{
const Structs::SlotAdjustmentStruct::Type & slotAdjustment = iterator.GetValue();
// Check for an invalid slotIndex
if (slotAdjustment.slotIndex >= forecast.Value().slots.size())
{
ChipLogError(Zcl, "DEM: Bad slot index %d", slotAdjustment.slotIndex);
return Status::Failure;
}
// Check to see if trying to modify a slot which has already been run
if (!forecast.Value().activeSlotNumber.IsNull() && slotAdjustment.slotIndex < forecast.Value().activeSlotNumber.Value())
{
ChipLogError(Zcl, "DEM: Modifying already run slot index %d", slotAdjustment.slotIndex);
return Status::ConstraintError;
}
const Structs::SlotStruct::Type & slot = forecast.Value().slots[slotAdjustment.slotIndex];
// NominalPower is only relevant if PFR is supported
if (mFeatureFlags.Has(Feature::kPowerForecastReporting))
{
if (!slotAdjustment.nominalPower.HasValue() || !slot.minPowerAdjustment.HasValue() ||
!slot.maxPowerAdjustment.HasValue() || slotAdjustment.nominalPower.Value() < slot.minPowerAdjustment.Value() ||
slotAdjustment.nominalPower.Value() > slot.maxPowerAdjustment.Value())
{
ChipLogError(Zcl, "DEM: Bad nominalPower");
return Status::ConstraintError;
}
}
if (!slot.minDurationAdjustment.HasValue() || !slot.maxDurationAdjustment.HasValue() ||
slotAdjustment.duration < slot.minDurationAdjustment.Value() ||
slotAdjustment.duration > slot.maxDurationAdjustment.Value())
{
ChipLogError(Zcl, "DEM: Bad min/max duration");
return Status::ConstraintError;
}
}
if (iterator.GetStatus() != CHIP_NO_ERROR)
{
return Status::InvalidCommand;
}
return mDelegate.ModifyForecastRequest(commandData.forecastID, commandData.slotAdjustments, commandData.cause);
}
DataModel::ActionReturnStatus
DeviceEnergyManagementCluster::HandleRequestConstraintBasedForecast(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments, CommandHandler * handler)
{
using namespace Commands;
RequestConstraintBasedForecast::DecodableType commandData;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData));
ReturnErrorOnFailure(CheckOptOutAllowsRequest(commandData.cause).GetUnderlyingError());
uint32_t currentUtcTime = 0;
CHIP_ERROR err = System::Clock::GetClock_MatterEpochS(currentUtcTime);
if (err != CHIP_NO_ERROR)
{
ChipLogError(Zcl, "DEM: Failed to get UTC time");
return Status::Failure;
}
// Check for invalid power levels and whether the constraint time/duration is in the past
{
auto iterator = commandData.constraints.begin();
if (iterator.Next())
{
const Structs::ConstraintsStruct::DecodableType & constraint = iterator.GetValue();
// Check to see if this constraint is in the past
if (constraint.startTime < currentUtcTime)
{
return Status::ConstraintError;
}
if (mFeatureFlags.Has(Feature::kPowerForecastReporting))
{
if (!constraint.nominalPower.HasValue())
{
ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast no nominalPower");
return Status::InvalidCommand;
}
if (constraint.nominalPower.Value() < mDelegate.GetAbsMinPower() ||
constraint.nominalPower.Value() > mDelegate.GetAbsMaxPower())
{
ChipLogError(Zcl,
"DEM: RequestConstraintBasedForecast nominalPower " ChipLogFormatX64
" out of range [" ChipLogFormatX64 ", " ChipLogFormatX64 "]",
ChipLogValueX64(constraint.nominalPower.Value()), ChipLogValueX64(mDelegate.GetAbsMinPower()),
ChipLogValueX64(mDelegate.GetAbsMaxPower()));
return Status::ConstraintError;
}
if (!constraint.maximumEnergy.HasValue())
{
ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast no value for maximumEnergy");
return Status::InvalidCommand;
}
}
if (mFeatureFlags.Has(Feature::kStateForecastReporting))
{
if (!constraint.loadControl.HasValue())
{
ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast no loadControl");
return Status::InvalidCommand;
}
if (constraint.loadControl.Value() < -100 || constraint.loadControl.Value() > 100)
{
ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast bad loadControl %d", constraint.loadControl.Value());
return Status::ConstraintError;
}
}
}
if (iterator.GetStatus() != CHIP_NO_ERROR)
{
return Status::InvalidCommand;
}
}
// Check for overlapping elements
{
auto iterator = commandData.constraints.begin();
if (iterator.Next())
{
// Get the first constraint
Structs::ConstraintsStruct::DecodableType prevConstraint = iterator.GetValue();
// Start comparing next vs prev constraints
while (iterator.Next())
{
const Structs::ConstraintsStruct::DecodableType & constraint = iterator.GetValue();
if (constraint.startTime < prevConstraint.startTime ||
prevConstraint.startTime + prevConstraint.duration >= constraint.startTime)
{
ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast overlapping constraint times");
return Status::ConstraintError;
}
prevConstraint = constraint;
}
}
if (iterator.GetStatus() != CHIP_NO_ERROR)
{
return Status::InvalidCommand;
}
}
return mDelegate.RequestConstraintBasedForecast(commandData.constraints, commandData.cause);
}
DataModel::ActionReturnStatus DeviceEnergyManagementCluster::HandleCancelRequest(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
using namespace Commands;
CancelRequest::DecodableType commandData;
ReturnErrorOnFailure(DataModel::Decode(input_arguments, commandData));
DataModel::Nullable<Structs::ForecastStruct::Type> forecast = mDelegate.GetForecast();
if (forecast.IsNull())
{
ChipLogDetail(Zcl, "Cancelling on a Null forecast!");
return Status::Failure;
}
if (forecast.Value().forecastUpdateReason == ForecastUpdateReasonEnum::kInternalOptimization)
{
ChipLogDetail(Zcl, "Bad Cancel when ESA ForecastUpdateReason was already Internal Optimization!");
return Status::InvalidInState;
}
VerifyOrReturnError(mDelegate.CancelRequest() == Status::Success, Status::Failure);
VerifyOrReturnError(!mDelegate.GetForecast().IsNull(), Status::InvalidInState);
VerifyOrReturnError(mDelegate.GetForecast().Value().forecastUpdateReason == ForecastUpdateReasonEnum::kInternalOptimization,
Status::InvalidInState);
return Status::Success;
}
} // namespace Clusters
} // namespace app
} // namespace chip