| /* |
| * |
| * 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 |