blob: d4ae59307cfea1090c3930b2a9b22499dbb23838 [file] [log] [blame]
/*
* 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 <app/AttributeAccessInterface.h>
#include <app/AttributeAccessInterfaceRegistry.h>
#include <app/CommandHandlerInterfaceRegistry.h>
#include <app/ConcreteAttributePath.h>
#include <app/InteractionModelEngine.h>
#include <app/util/attribute-storage.h>
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<Disable::DecodableType>(
handlerContext, [this](HandlerContext & ctx, const auto & commandData) { HandleDisable(ctx, commandData); });
return;
case EnableCharging::Id:
HandleCommand<EnableCharging::DecodableType>(
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<EnableDischarging::DecodableType>(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<StartDiagnostics::DecodableType>(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<SetTargets::DecodableType>(
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<GetTargets::DecodableType>(
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<ClearTargets::DecodableType>(
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<Structs::ChargingTargetScheduleStruct::DecodableType> & 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<TargetDayOfWeekBitmap>(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<short unsigned int>(minutesPastMidnight));
if (minutesPastMidnight > 1439)
{
ChipLogError(AppServer, "MinutesPastMidnight has invalid value (%d)", static_cast<int>(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<int>(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<signed long int>(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() {}