/* * 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 "energy-evse-server.h" #include #include #include #include #include #include using namespace chip; using namespace chip::app; using namespace chip::app::DataModel; using namespace chip::app::Clusters; using namespace chip::app::Clusters::EnergyEvse; using namespace chip::app::Clusters::EnergyEvse::Attributes; using chip::Protocols::InteractionModel::Status; namespace chip { namespace app { namespace Clusters { namespace EnergyEvse { 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); } bool Instance::SupportsOptAttr(OptionalAttributes aOptionalAttrs) const { return mOptionalAttrs.Has(aOptionalAttrs); } bool Instance::SupportsOptCmd(OptionalCommands aOptionalCmds) const { return mOptionalCmds.Has(aOptionalCmds); } // AttributeAccessInterface CHIP_ERROR Instance::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) { switch (aPath.mAttributeId) { case State::Id: return aEncoder.Encode(mDelegate.GetState()); case SupplyState::Id: return aEncoder.Encode(mDelegate.GetSupplyState()); case FaultState::Id: return aEncoder.Encode(mDelegate.GetFaultState()); case ChargingEnabledUntil::Id: return aEncoder.Encode(mDelegate.GetChargingEnabledUntil()); case DischargingEnabledUntil::Id: /* V2X */ return aEncoder.Encode(mDelegate.GetDischargingEnabledUntil()); case CircuitCapacity::Id: return aEncoder.Encode(mDelegate.GetCircuitCapacity()); case MinimumChargeCurrent::Id: return aEncoder.Encode(mDelegate.GetMinimumChargeCurrent()); case MaximumChargeCurrent::Id: return aEncoder.Encode(mDelegate.GetMaximumChargeCurrent()); case MaximumDischargeCurrent::Id: /* V2X */ return aEncoder.Encode(mDelegate.GetMaximumDischargeCurrent()); case UserMaximumChargeCurrent::Id: return aEncoder.Encode(mDelegate.GetUserMaximumChargeCurrent()); case RandomizationDelayWindow::Id: /* Optional */ return aEncoder.Encode(mDelegate.GetRandomizationDelayWindow()); /* PREF - ChargingPreferences attributes */ case NextChargeStartTime::Id: return aEncoder.Encode(mDelegate.GetNextChargeStartTime()); case NextChargeTargetTime::Id: return aEncoder.Encode(mDelegate.GetNextChargeTargetTime()); case NextChargeRequiredEnergy::Id: return aEncoder.Encode(mDelegate.GetNextChargeRequiredEnergy()); case NextChargeTargetSoC::Id: return aEncoder.Encode(mDelegate.GetNextChargeTargetSoC()); case ApproximateEVEfficiency::Id: return aEncoder.Encode(mDelegate.GetApproximateEVEfficiency()); /* SOC attributes */ case StateOfCharge::Id: return aEncoder.Encode(mDelegate.GetStateOfCharge()); case BatteryCapacity::Id: return aEncoder.Encode(mDelegate.GetBatteryCapacity()); /* PNC attributes*/ case VehicleID::Id: return aEncoder.Encode(mDelegate.GetVehicleID()); /* Session SESS attributes */ case SessionID::Id: return aEncoder.Encode(mDelegate.GetSessionID()); case SessionDuration::Id: return aEncoder.Encode(mDelegate.GetSessionDuration()); case SessionEnergyCharged::Id: return aEncoder.Encode(mDelegate.GetSessionEnergyCharged()); case SessionEnergyDischarged::Id: return aEncoder.Encode(mDelegate.GetSessionEnergyDischarged()); /* 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; } CHIP_ERROR Instance::Write(const ConcreteDataAttributePath & aPath, AttributeValueDecoder & aDecoder) { switch (aPath.mAttributeId) { case UserMaximumChargeCurrent::Id: { // Optional Attribute if (!SupportsOptAttr(OptionalAttributes::kSupportsUserMaximumChargingCurrent)) { return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); } int64_t newValue; ReturnErrorOnFailure(aDecoder.Decode(newValue)); ReturnErrorOnFailure(mDelegate.SetUserMaximumChargeCurrent(newValue)); return CHIP_NO_ERROR; } case RandomizationDelayWindow::Id: { // Optional Attribute if (!SupportsOptAttr(OptionalAttributes::kSupportsRandomizationWindow)) { return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); } uint32_t newValue; ReturnErrorOnFailure(aDecoder.Decode(newValue)); ReturnErrorOnFailure(mDelegate.SetRandomizationDelayWindow(newValue)); return CHIP_NO_ERROR; } case ApproximateEVEfficiency::Id: { // Optional Attribute if ChargingPreferences is supported if ((!HasFeature(Feature::kChargingPreferences)) || (!SupportsOptAttr(OptionalAttributes::kSupportsApproximateEvEfficiency))) { return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); } uint16_t newValue; ReturnErrorOnFailure(aDecoder.Decode(newValue)); ReturnErrorOnFailure(mDelegate.SetApproximateEVEfficiency(MakeNullable(newValue))); return CHIP_NO_ERROR; } default: // Unknown attribute; return error. None of the other attributes for // this cluster are writable, so should not be ending up in this code to // start with. return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); } } // CommandHandlerInterface CHIP_ERROR Instance::EnumerateAcceptedCommands(const ConcreteClusterPath & cluster, CommandIdCallback callback, void * context) { using namespace Commands; for (auto && cmd : { Disable::Id, EnableCharging::Id, }) { VerifyOrExit(callback(cmd, context) == Loop::Continue, /**/); } if (HasFeature(Feature::kV2x)) { VerifyOrExit(callback(EnableDischarging::Id, context) == Loop::Continue, /**/); } if (HasFeature(Feature::kChargingPreferences)) { for (auto && cmd : { SetTargets::Id, GetTargets::Id, ClearTargets::Id, }) { VerifyOrExit(callback(cmd, context) == Loop::Continue, /**/); } } if (SupportsOptCmd(OptionalCommands::kSupportsStartDiagnostics)) { callback(StartDiagnostics::Id, context); } exit: return CHIP_NO_ERROR; } void Instance::InvokeCommand(HandlerContext & handlerContext) { using namespace Commands; switch (handlerContext.mRequestPath.mCommandId) { case Disable::Id: HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleDisable(ctx, commandData); }); return; case EnableCharging::Id: HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleEnableCharging(ctx, commandData); }); return; case EnableDischarging::Id: if (!HasFeature(Feature::kV2x)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand(handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleEnableDischarging(ctx, commandData); }); } return; case StartDiagnostics::Id: if (!SupportsOptCmd(OptionalCommands::kSupportsStartDiagnostics)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand(handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleStartDiagnostics(ctx, commandData); }); } return; case SetTargets::Id: if (!HasFeature(Feature::kChargingPreferences)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleSetTargets(ctx, commandData); }); } return; case GetTargets::Id: if (!HasFeature(Feature::kChargingPreferences)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleGetTargets(ctx, commandData); }); } return; case ClearTargets::Id: if (!HasFeature(Feature::kChargingPreferences)) { handlerContext.mCommandHandler.AddStatus(handlerContext.mRequestPath, Status::UnsupportedCommand); } else { HandleCommand( handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleClearTargets(ctx, commandData); }); } return; } } void Instance::HandleDisable(HandlerContext & ctx, const Commands::Disable::DecodableType & commandData) { // No parameters for this command // Call the delegate Status status = mDelegate.Disable(); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); } void Instance::HandleEnableCharging(HandlerContext & ctx, const Commands::EnableCharging::DecodableType & commandData) { auto & chargingEnabledUntil = commandData.chargingEnabledUntil; auto & minimumChargeCurrent = commandData.minimumChargeCurrent; auto & maximumChargeCurrent = commandData.maximumChargeCurrent; if (minimumChargeCurrent < kMinimumChargeCurrent) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } if (maximumChargeCurrent < kMinimumChargeCurrent) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } if (minimumChargeCurrent > maximumChargeCurrent) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } // Call the delegate Status status = mDelegate.EnableCharging(chargingEnabledUntil, minimumChargeCurrent, maximumChargeCurrent); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); } void Instance::HandleEnableDischarging(HandlerContext & ctx, const Commands::EnableDischarging::DecodableType & commandData) { auto & dischargingEnabledUntil = commandData.dischargingEnabledUntil; auto & maximumDischargeCurrent = commandData.maximumDischargeCurrent; if (maximumDischargeCurrent < kMinimumChargeCurrent) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::ConstraintError); return; } // Call the delegate Status status = mDelegate.EnableDischarging(dischargingEnabledUntil, maximumDischargeCurrent); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); } void Instance::HandleStartDiagnostics(HandlerContext & ctx, const Commands::StartDiagnostics::DecodableType & commandData) { // No parameters for this command // Call the delegate Status status = mDelegate.StartDiagnostics(); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); } void Instance::HandleSetTargets(HandlerContext & ctx, const Commands::SetTargets::DecodableType & commandData) { // Call the delegate auto & chargingTargetSchedules = commandData.chargingTargetSchedules; Status status = ValidateTargets(chargingTargetSchedules); if (status != Status::Success) { ChipLogError(AppServer, "SetTargets contained invalid data - Rejecting"); } else { status = mDelegate.SetTargets(chargingTargetSchedules); } ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); } Status Instance::ValidateTargets( const DataModel::DecodableList & chargingTargetSchedules) { /* A) check that the targets are valid * 1) each target must be within valid range (TargetTimeMinutesPastMidnight < 1440) * 2) each target must be within valid range (TargetSoC percent 0 - 100) * If SOC feature not supported then this MUST be 100 or not present * 3) each target must be within valid range (AddedEnergy >= 0) * B) Day of Week is only allowed to be included once */ uint8_t dayOfWeekBitmap = 0; auto iter = chargingTargetSchedules.begin(); while (iter.Next()) { auto & entry = iter.GetValue(); uint8_t bitmask = entry.dayOfWeekForSequence.GetField(static_cast(0x7F)); ChipLogProgress(AppServer, "DayOfWeekForSequence = 0x%02x", bitmask); if ((dayOfWeekBitmap & bitmask) != 0) { // A bit has already been set - Return ConstraintError ChipLogError(AppServer, "DayOfWeekForSequence has a bit set which has already been set in another entry."); return Status::ConstraintError; } dayOfWeekBitmap |= bitmask; // add this day Of week to the previously seen days auto iterInner = entry.chargingTargets.begin(); uint8_t innerIdx = 0; while (iterInner.Next()) { auto & targetStruct = iterInner.GetValue(); uint16_t minutesPastMidnight = targetStruct.targetTimeMinutesPastMidnight; ChipLogProgress(AppServer, "[%d] MinutesPastMidnight : %d", innerIdx, static_cast(minutesPastMidnight)); if (minutesPastMidnight > 1439) { ChipLogError(AppServer, "MinutesPastMidnight has invalid value (%d)", static_cast(minutesPastMidnight)); return Status::ConstraintError; } // If SocReporting is supported, targetSoc must have a value in the range [0, 100] if (HasFeature(Feature::kSoCReporting)) { if (!targetStruct.targetSoC.HasValue()) { ChipLogError(AppServer, "kSoCReporting is supported but TargetSoC does not have a value"); return Status::Failure; } if (targetStruct.targetSoC.Value() > 100) { ChipLogError(AppServer, "TargetSoC has invalid value (%d)", static_cast(targetStruct.targetSoC.Value())); return Status::ConstraintError; } } else if (targetStruct.targetSoC.HasValue() && targetStruct.targetSoC.Value() != 100) { // If SocReporting is not supported but targetSoc has a value, it must be 100 ChipLogError(AppServer, "TargetSoC has can only be 100%% if SOC feature is not supported"); return Status::ConstraintError; } // One or both of targetSoc and addedEnergy must be specified if (!(targetStruct.targetSoC.HasValue()) && !(targetStruct.addedEnergy.HasValue())) { ChipLogError(AppServer, "Must have one of AddedEnergy or TargetSoC"); return Status::Failure; } // Validate the value of addedEnergy, if specified is >= 0 if (targetStruct.addedEnergy.HasValue() && targetStruct.addedEnergy.Value() < 0) { ChipLogError(AppServer, "AddedEnergy has invalid value (%ld)", static_cast(targetStruct.addedEnergy.Value())); return Status::ConstraintError; } innerIdx++; } if (iterInner.GetStatus() != CHIP_NO_ERROR) { return Status::InvalidCommand; } } if (iter.GetStatus() != CHIP_NO_ERROR) { return Status::InvalidCommand; } return Status::Success; } void Instance::HandleGetTargets(HandlerContext & ctx, const Commands::GetTargets::DecodableType & commandData) { Commands::GetTargetsResponse::Type response; Status status = mDelegate.GetTargets(response.chargingTargetSchedules); if (status != Status::Success) { ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); return; } ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Protocols::InteractionModel::Status::Success); } void Instance::HandleClearTargets(HandlerContext & ctx, const Commands::ClearTargets::DecodableType & commandData) { // Call the delegate Status status = mDelegate.ClearTargets(); ctx.mCommandHandler.AddStatus(ctx.mRequestPath, status); } } // namespace EnergyEvse } // namespace Clusters } // namespace app } // namespace chip // ----------------------------------------------------------------------------- // Plugin initialization void MatterEnergyEvsePluginServerInitCallback() {}