/* * Copyright (c) 2023 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 "device-energy-management-server.h" #include #include #include #include #include #include using namespace chip; using namespace chip::app; using namespace chip::app::Clusters; using namespace chip::app::Clusters::DeviceEnergyManagement; using namespace chip::app::Clusters::DeviceEnergyManagement::Attributes; using chip::Protocols::InteractionModel::Status; namespace chip { namespace app { namespace Clusters { namespace DeviceEnergyManagement { CHIP_ERROR Instance::Init() { ReturnErrorOnFailure(CommandHandlerInterfaceRegistry::Instance().RegisterCommandHandler(this)); VerifyOrReturnError(AttributeAccessInterfaceRegistry::Instance().Register(this), CHIP_ERROR_INCORRECT_STATE); return CHIP_NO_ERROR; } void Instance::Shutdown() { CommandHandlerInterfaceRegistry::Instance().UnregisterCommandHandler(this); AttributeAccessInterfaceRegistry::Instance().Unregister(this); } bool Instance::HasFeature(Feature aFeature) const { return mFeature.Has(aFeature); } // AttributeAccessInterface CHIP_ERROR Instance::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) { switch (aPath.mAttributeId) { case ESAType::Id: return aEncoder.Encode(mDelegate.GetESAType()); case ESACanGenerate::Id: return aEncoder.Encode(mDelegate.GetESACanGenerate()); case ESAState::Id: return aEncoder.Encode(mDelegate.GetESAState()); case AbsMinPower::Id: return aEncoder.Encode(mDelegate.GetAbsMinPower()); case AbsMaxPower::Id: return aEncoder.Encode(mDelegate.GetAbsMaxPower()); case PowerAdjustmentCapability::Id: /* PA - PowerAdjustment */ if (!HasFeature(Feature::kPowerAdjustment)) { return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); } return aEncoder.Encode(mDelegate.GetPowerAdjustmentCapability()); case Forecast::Id: /* PFR | SFR - Power Forecast Reporting or State Forecast Reporting */ if (!HasFeature(Feature::kPowerForecastReporting) && !HasFeature(Feature::kStateForecastReporting)) { return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); } return aEncoder.Encode(mDelegate.GetForecast()); case OptOutState::Id: /* PA | STA | PAU | FA | CON */ if (!HasFeature(Feature::kPowerAdjustment) && !HasFeature(Feature::kStartTimeAdjustment) && !HasFeature(Feature::kPausable) && !HasFeature(Feature::kForecastAdjustment) && !HasFeature(Feature::kConstraintBasedAdjustment)) { return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); } return aEncoder.Encode(mDelegate.GetOptOutState()); /* FeatureMap - is held locally */ case FeatureMap::Id: return aEncoder.Encode(mFeature); } /* Allow all other unhandled attributes to fall through to Ember */ return CHIP_NO_ERROR; } // CommandHandlerInterface CHIP_ERROR Instance::EnumerateAcceptedCommands(const ConcreteClusterPath & cluster, CommandIdCallback callback, void * context) { using namespace Commands; if (HasFeature(Feature::kPowerAdjustment)) { for (auto && cmd : { PowerAdjustRequest::Id, CancelPowerAdjustRequest::Id, }) { VerifyOrExit(callback(cmd, context) == Loop::Continue, /**/); } } if (HasFeature(Feature::kStartTimeAdjustment)) { VerifyOrExit(callback(StartTimeAdjustRequest::Id, context) == Loop::Continue, /**/); } if (HasFeature(Feature::kPausable)) { VerifyOrExit(callback(PauseRequest::Id, context) == Loop::Continue, /**/); VerifyOrExit(callback(ResumeRequest::Id, context) == Loop::Continue, /**/); } if (HasFeature(Feature::kForecastAdjustment)) { VerifyOrExit(callback(ModifyForecastRequest::Id, context) == Loop::Continue, /**/); } if (HasFeature(Feature::kConstraintBasedAdjustment)) { VerifyOrExit(callback(RequestConstraintBasedForecast::Id, context) == Loop::Continue, /**/); } if (HasFeature(Feature::kStartTimeAdjustment) || HasFeature(Feature::kForecastAdjustment) || HasFeature(Feature::kConstraintBasedAdjustment)) { VerifyOrExit(callback(CancelRequest::Id, context) == Loop::Continue, /**/); } exit: return CHIP_NO_ERROR; } void Instance::InvokeCommand(HandlerContext & handlerContext) { using namespace Commands; switch (handlerContext.mRequestPath.mCommandId) { case PowerAdjustRequest::Id: if (!HasFeature(Feature::kPowerAdjustment)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandlePowerAdjustRequest(ctx, commandData); }); } return; case CancelPowerAdjustRequest::Id: if (!HasFeature(Feature::kPowerAdjustment)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleCancelPowerAdjustRequest(ctx, commandData); }); } return; case StartTimeAdjustRequest::Id: if (!HasFeature(Feature::kStartTimeAdjustment)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleStartTimeAdjustRequest(ctx, commandData); }); } return; case PauseRequest::Id: if (!HasFeature(Feature::kPausable)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandlePauseRequest(ctx, commandData); }); } return; case ResumeRequest::Id: if (!HasFeature(Feature::kPausable)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleResumeRequest(ctx, commandData); }); } return; case ModifyForecastRequest::Id: if (!HasFeature(Feature::kForecastAdjustment)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleModifyForecastRequest(ctx, commandData); }); } return; case RequestConstraintBasedForecast::Id: if (!HasFeature(Feature::kConstraintBasedAdjustment)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleRequestConstraintBasedForecast(ctx, commandData); }); } return; case CancelRequest::Id: if (!HasFeature(Feature::kStartTimeAdjustment) && !HasFeature(Feature::kForecastAdjustment) && !HasFeature(Feature::kConstraintBasedAdjustment)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleCancelRequest(ctx, commandData); }); } return; } } Status Instance::CheckOptOutAllowsRequest(AdjustmentCauseEnum adjustmentCause) { OptOutStateEnum optOutState = mDelegate.GetOptOutState(); if (adjustmentCause == AdjustmentCauseEnum::kUnknownEnumValue) { ChipLogError(Zcl, "DEM: adjustment cause is invalid (%d)", static_cast(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(optOutState)); return Status::InvalidValue; } } void Instance::HandlePowerAdjustRequest(HandlerContext & ctx, const Commands::PowerAdjustRequest::DecodableType & commandData) { bool validArgs = false; int64_t power = commandData.power; uint32_t durationSec = commandData.duration; AdjustmentCauseEnum adjustmentCause = commandData.cause; // Notify the appliance if the appliance hardware cannot be adjusted, then return Failure if (!HasFeature(DeviceEnergyManagement::Feature::kPowerAdjustment)) { ChipLogError(Zcl, "PowerAdjust not supported"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } Status status = CheckOptOutAllowsRequest(adjustmentCause); if (status != Status::Success) { ChipLogError(Zcl, "DEM: PowerAdjustRequest command rejected"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); return; } DataModel::Nullable powerAdjustmentCapabilityStruct = mDelegate.GetPowerAdjustmentCapability(); if (powerAdjustmentCapabilityStruct.IsNull()) { ChipLogError(Zcl, "DEM: powerAdjustmentCapabilityStruct IsNull"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } if (powerAdjustmentCapabilityStruct.Value().powerAdjustCapability.IsNull()) { ChipLogError(Zcl, "DEM: powerAdjustmentCapabilityStruct.powerAdjustCapability IsNull"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } /* PowerAdjustmentCapability is a list - so iterate through checking if the command is within one of the offers */ for (auto pas : powerAdjustmentCapabilityStruct.Value().powerAdjustCapability.Value()) { if ((power >= pas.minPower) && (durationSec >= pas.minDuration) && (power <= pas.maxPower) && (durationSec <= pas.maxDuration)) { ChipLogProgress(Zcl, "DEM: Good PowerAdjustment args"); validArgs = true; break; } } if (!validArgs) { ChipLogError(Zcl, "DEM: invalid request range"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } ChipLogProgress(Zcl, "DEM: Good PowerAdjustRequest() args."); status = mDelegate.PowerAdjustRequest(power, durationSec, adjustmentCause); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); if (status != Status::Success) { ChipLogError(Zcl, "DEM: Failed to PowerAdjustRequest() args."); } } void Instance::HandleCancelPowerAdjustRequest(HandlerContext & ctx, const Commands::CancelPowerAdjustRequest::DecodableType & commandData) { if (!HasFeature(DeviceEnergyManagement::Feature::kPowerAdjustment)) { ChipLogError(Zcl, "PowerAdjust not supported"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } /* Check that the ESA state is PowerAdjustActive */ ESAStateEnum esaStatus = mDelegate.GetESAState(); if (ESAStateEnum::kPowerAdjustActive != esaStatus) { ChipLogError(Zcl, "DEM: kPowerAdjustActive != esaStatus"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidInState); return; } Status status = mDelegate.CancelPowerAdjustRequest(); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); if (status != Status::Success) { ChipLogError(Zcl, "DEM: Failed to CancelPowerAdjustRequest()"); return; } } void Instance::HandleStartTimeAdjustRequest(HandlerContext & ctx, const Commands::StartTimeAdjustRequest::DecodableType & commandData) { Status status; uint32_t earliestStartTimeEpoch = 0; uint32_t latestEndTimeEpoch = 0; uint32_t duration; uint32_t requestedStartTimeEpoch = commandData.requestedStartTime; AdjustmentCauseEnum adjustmentCause = commandData.cause; status = CheckOptOutAllowsRequest(adjustmentCause); if (status != Status::Success) { ChipLogError(Zcl, "DEM: StartTimeAdjustRequest command rejected"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); return; } DataModel::Nullable forecastNullable = mDelegate.GetForecast(); if (forecastNullable.IsNull()) { ChipLogError(Zcl, "DEM: Forecast is Null"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } /* Temporary variable to save calling .Value() on forecastNullable */ auto & forecast = forecastNullable.Value(); /** * If the RequestedStartTime value resulted in a time shift which is * outside the time constraints of EarliestStartTime and * LatestEndTime, then the command SHALL be rejected with CONSTRAINT_ERROR; * in other failure scenarios the command SHALL be rejected with FAILURE */ /* earliestStartTime is optional based on the StartTimeAdjust (STA) feature AND is nullable */ if (!(forecast.earliestStartTime.HasValue()) || !(forecast.latestEndTime.HasValue())) { /* These should have values, since this command requires STA feature and these are mandatory for that */ ChipLogError(Zcl, "DEM: EarliestStartTime / LatestEndTime do not have values"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } /* Temporary variable to save keep calling .Value() on the Optional element */ DataModel::Nullable & earliestStartTimeNullable = forecast.earliestStartTime.Value(); /* Latest End Time is optional & cannot be null - unlike earliestStartTime! */ latestEndTimeEpoch = forecast.latestEndTime.Value(); if (earliestStartTimeNullable.IsNull()) { System::Clock::Milliseconds64 cTMs; CHIP_ERROR err = System::SystemClock().GetClock_RealTimeMS(cTMs); if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "DEM: Unable to get current time - err:%" CHIP_ERROR_FORMAT, err.Format()); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } auto unixEpoch = std::chrono::duration_cast(cTMs).count(); uint32_t chipEpoch = 0; if (!UnixEpochToChipEpochTime(unixEpoch, chipEpoch)) { ChipLogError(Zcl, "DEM: unable to convert Unix Epoch time to Matter Epoch Time"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } /* Null means - We can start immediately */ earliestStartTimeEpoch = chipEpoch; /* NOW */ } else { earliestStartTimeEpoch = earliestStartTimeNullable.Value(); } duration = forecast.endTime - forecast.startTime; // the current entire forecast duration if (requestedStartTimeEpoch < earliestStartTimeEpoch) { ChipLogError(Zcl, "DEM: Bad requestedStartTime %ld, earlier than earliestStartTime %ld.", static_cast(requestedStartTimeEpoch), static_cast(earliestStartTimeEpoch)); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } if ((requestedStartTimeEpoch + duration) > latestEndTimeEpoch) { ChipLogError(Zcl, "DEM: Bad requestedStartTimeEpoch + duration %ld, later than latestEndTime %ld.", static_cast(requestedStartTimeEpoch + duration), static_cast(latestEndTimeEpoch)); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } ChipLogProgress(Zcl, "DEM: Good requestedStartTimeEpoch %ld.", static_cast(requestedStartTimeEpoch)); status = mDelegate.StartTimeAdjustRequest(requestedStartTimeEpoch, adjustmentCause); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); if (status != Status::Success) { ChipLogError(Zcl, "DEM: StartTimeAdjustRequest(%ld) FAILURE", static_cast(requestedStartTimeEpoch)); return; } } void Instance::HandlePauseRequest(HandlerContext & ctx, const Commands::PauseRequest::DecodableType & commandData) { Status status = Status::Success; CHIP_ERROR err = CHIP_NO_ERROR; DataModel::Nullable forecast = mDelegate.GetForecast(); if (!HasFeature(DeviceEnergyManagement::Feature::kPausable)) { ChipLogError(AppServer, "Pause not supported"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } uint32_t duration = commandData.duration; AdjustmentCauseEnum adjustmentCause = commandData.cause; status = CheckOptOutAllowsRequest(adjustmentCause); if (status != Status::Success) { ChipLogError(Zcl, "DEM: PauseRequest command rejected"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); return; } if (forecast.IsNull()) { ChipLogError(Zcl, "DEM: Forecast is Null"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } /* value SHALL be between the MinPauseDuration and MaxPauseDuration indicated in the ActiveSlotNumber index in the Slots list in the Forecast. */ uint16_t activeSlotNumber; if (forecast.Value().activeSlotNumber.IsNull()) { ChipLogError(Zcl, "DEM: activeSlotNumber Is Null"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } activeSlotNumber = forecast.Value().activeSlotNumber.Value(); if (activeSlotNumber >= forecast.Value().slots.size()) { ChipLogError(Zcl, "DEM: Bad activeSlotNumber %d , size()=%d.", activeSlotNumber, static_cast(forecast.Value().slots.size())); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } /* We expect that there should be a slotIsPausable entry (but it is optional) */ if (!forecast.Value().slots[activeSlotNumber].slotIsPausable.HasValue()) { ChipLogError(Zcl, "DEM: activeSlotNumber %d does not include slotIsPausable.", activeSlotNumber); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } if (!forecast.Value().slots[activeSlotNumber].minPauseDuration.HasValue()) { ChipLogError(Zcl, "DEM: activeSlotNumber %d does not include minPauseDuration.", activeSlotNumber); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } if (!forecast.Value().slots[activeSlotNumber].maxPauseDuration.HasValue()) { ChipLogError(Zcl, "DEM: activeSlotNumber %d does not include minPauseDuration.", activeSlotNumber); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } if (!forecast.Value().slots[activeSlotNumber].slotIsPausable.Value()) { ChipLogError(Zcl, "DEM: activeSlotNumber %d is NOT pausable.", activeSlotNumber); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } if ((duration < forecast.Value().slots[activeSlotNumber].minPauseDuration.Value()) || (duration > forecast.Value().slots[activeSlotNumber].maxPauseDuration.Value())) { ChipLogError(Zcl, "DEM: out of range pause duration %ld", static_cast(duration)); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } err = mDelegate.SetESAState(ESAStateEnum::kPaused); if (CHIP_NO_ERROR != err) { ChipLogError(Zcl, "DEM: SetESAState(paused) FAILURE"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } status = mDelegate.PauseRequest(duration, adjustmentCause); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); if (status != Status::Success) { ChipLogError(Zcl, "DEM: PauseRequest(%ld) FAILURE", static_cast(duration)); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); return; } } void Instance::HandleResumeRequest(HandlerContext & ctx, const Commands::ResumeRequest::DecodableType & commandData) { if (!HasFeature(DeviceEnergyManagement::Feature::kPausable)) { ChipLogError(AppServer, "Pause not supported"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } if (ESAStateEnum::kPaused != mDelegate.GetESAState()) { ChipLogError(Zcl, "DEM: ESAState not Paused."); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidInState); return; } Status status = mDelegate.ResumeRequest(); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); if (status != Status::Success) { ChipLogError(Zcl, "DEM: ResumeRequest FAILURE"); return; } } void Instance::HandleModifyForecastRequest(HandlerContext & ctx, const Commands::ModifyForecastRequest::DecodableType & commandData) { if (!HasFeature(DeviceEnergyManagement::Feature::kForecastAdjustment)) { ChipLogError(Zcl, "ModifyForecast not supported"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } Status status; DataModel::Nullable forecast; uint32_t forecastID = commandData.forecastID; DataModel::DecodableList slotAdjustments = commandData.slotAdjustments; AdjustmentCauseEnum adjustmentCause = commandData.cause; status = CheckOptOutAllowsRequest(adjustmentCause); if (status != Status::Success) { ChipLogError(Zcl, "DEM: ModifyForecastRequest command rejected"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); return; } forecast = mDelegate.GetForecast(); if (forecast.IsNull()) { ChipLogError(Zcl, "DEM: Forecast is Null"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } // Check the various values in the slot structures auto iterator = 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); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } // 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); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } const Structs::SlotStruct::Type & slot = forecast.Value().slots[slotAdjustment.slotIndex]; // NominalPower is only relevant if PFR is supported if (HasFeature(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"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } } if (!slot.minDurationAdjustment.HasValue() || !slot.maxDurationAdjustment.HasValue() || slotAdjustment.duration < slot.minDurationAdjustment.Value() || slotAdjustment.duration > slot.maxDurationAdjustment.Value()) { ChipLogError(Zcl, "DEM: Bad min/max duration"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } } if (iterator.GetStatus() != CHIP_NO_ERROR) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); return; } status = mDelegate.ModifyForecastRequest(forecastID, slotAdjustments, adjustmentCause); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); if (status != Status::Success) { ChipLogError(Zcl, "DEM: ModifyForecastRequest FAILURE"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } } void Instance::HandleRequestConstraintBasedForecast(HandlerContext & ctx, const Commands::RequestConstraintBasedForecast::DecodableType & commandData) { if (!HasFeature(DeviceEnergyManagement::Feature::kConstraintBasedAdjustment)) { ChipLogError(AppServer, "RequestConstraintBasedForecast CON not supported"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::Failure); return; } Status status = Status::Success; DataModel::DecodableList constraints = commandData.constraints; AdjustmentCauseEnum adjustmentCause = commandData.cause; status = CheckOptOutAllowsRequest(adjustmentCause); if (status != Status::Success) { ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast command rejected"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); return; } uint32_t currentUtcTime = 0; status = GetMatterEpochTimeFromUnixTime(currentUtcTime); if (status != Status::Success) { ChipLogError(Zcl, "DEM: Failed to get UTC time"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); return; } // Check for invalid power levels and whether the constraint time/duration is in the past { auto iterator = 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) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } if (HasFeature(Feature::kPowerForecastReporting)) { if (!constraint.nominalPower.HasValue()) { ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast no nominalPower"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); return; } 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())); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } if (!constraint.maximumEnergy.HasValue()) { ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast no value for maximumEnergy"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); return; } } if (HasFeature(Feature::kStateForecastReporting)) { if (!constraint.loadControl.HasValue()) { ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast no loadControl"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); return; } if (constraint.loadControl.Value() < -100 || constraint.loadControl.Value() > 100) { ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast bad loadControl %d", constraint.loadControl.Value()); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } } } if (iterator.GetStatus() != CHIP_NO_ERROR) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); return; } } // Check for overlappping elements { auto iterator = 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"); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } prevConstraint = constraint; } } if (iterator.GetStatus() != CHIP_NO_ERROR) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); return; } } status = mDelegate.RequestConstraintBasedForecast(constraints, adjustmentCause); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); if (status != Status::Success) { ChipLogError(Zcl, "DEM: RequestConstraintBasedForecast FAILURE"); return; } } void Instance::HandleCancelRequest(HandlerContext & ctx, const Commands::CancelRequest::DecodableType & commandData) { Status status = Status::Failure; DataModel::Nullable forecast = mDelegate.GetForecast(); if (forecast.IsNull()) { ChipLogDetail(AppServer, "Cancelling on a Null forecast!"); status = Status::Failure; } else if (forecast.Value().forecastUpdateReason == ForecastUpdateReasonEnum::kInternalOptimization) { ChipLogDetail(AppServer, "Bad Cancel when ESA ForecastUpdateReason was already Internal Optimization!"); status = Status::InvalidInState; } else { status = mDelegate.CancelRequest(); } ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); } Status Instance::GetMatterEpochTimeFromUnixTime(uint32_t & currentUtcTime) const { currentUtcTime = 0; System::Clock::Milliseconds64 cTMs; CHIP_ERROR err = System::SystemClock().GetClock_RealTimeMS(cTMs); if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "DEM: Unable to get current time - err:%" CHIP_ERROR_FORMAT, err.Format()); return Status::Failure; } auto unixEpoch = std::chrono::duration_cast(cTMs).count(); if (!UnixEpochToChipEpochTime(unixEpoch, currentUtcTime)) { ChipLogError(Zcl, "DEM: unable to convert Unix Epoch time to Matter Epoch Time"); return Status::Failure; } return Status::Success; } } // namespace DeviceEnergyManagement } // namespace Clusters } // namespace app } // namespace chip void MatterDeviceEnergyManagementPluginServerInitCallback() {}