): number | undefined => {
let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
// Get charging profiles for connector id and sort by stack level
- const chargingProfiles =
- cloneObject<ChargingProfile[]>(
- chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
- )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
- // Get charging profiles on connector 0 and sort by stack level
- if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) {
- chargingProfiles.push(
- ...cloneObject<ChargingProfile[]>(
- chargingStation.getConnectorStatus(0)!.chargingProfiles!,
- ).sort((a, b) => b.stackLevel - a.stackLevel),
- );
- }
+ const chargingProfiles = cloneObject<ChargingProfile[]>(
+ (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? []).concat(
+ chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [],
+ ),
+ ).sort((a, b) => b.stackLevel - a.stackLevel);
if (isNotEmptyArray(chargingProfiles)) {
const result = getLimitFromChargingProfiles(
chargingStation,
const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
const currentDate = new Date();
const connectorStatus = chargingStation.getConnectorStatus(connectorId);
+ if (!isArraySorted(chargingProfiles, (a, b) => b.stackLevel - a.stackLevel)) {
+ logger.warn(
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profiles are not sorted by stack level. Trying to sort them`,
+ );
+ chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel);
+ }
for (const chargingProfile of chargingProfiles) {
const chargingSchedule = chargingProfile.chargingSchedule;
if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
}
};
-const canProceedChargingProfile = (
+export const canProceedChargingProfile = (
chargingProfile: ChargingProfile,
currentDate: Date,
logPrefix: string,
const chargingSchedule = chargingProfile.chargingSchedule;
if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
logger.error(
- `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
+ `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
);
return false;
}
return true;
};
-const canProceedRecurringChargingProfile = (
+export const canProceedRecurringChargingProfile = (
chargingProfile: ChargingProfile,
logPrefix: string,
): boolean => {
* @param currentDate -
* @param logPrefix -
*/
-const prepareRecurringChargingProfile = (
+export const prepareRecurringChargingProfile = (
chargingProfile: ChargingProfile,
currentDate: Date,
logPrefix: string,
import type { JSONSchemaType } from 'ajv';
import { Client, type FTPResponse } from 'basic-ftp';
-import { addSeconds, isWithinInterval, max, secondsToMilliseconds } from 'date-fns';
+import {
+ addSeconds,
+ isDate,
+ isWithinInterval,
+ maxTime,
+ min,
+ secondsToMilliseconds,
+} from 'date-fns';
import { create } from 'tar';
import { OCPP16Constants } from './OCPP16Constants';
import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
import {
type ChargingStation,
+ canProceedChargingProfile,
+ canProceedRecurringChargingProfile,
checkChargingStation,
getConfigurationKey,
+ prepareRecurringChargingProfile,
removeExpiredReservations,
setConfigurationKeyValue,
} from '../../../charging-station';
import {
type ChangeConfigurationRequest,
type ChangeConfigurationResponse,
+ ChargingProfileKindType,
+ ChargingRateUnitType,
type ClearChargingProfileRequest,
type ClearChargingProfileResponse,
ErrorType,
} from '../../../types';
import {
Constants,
+ cloneObject,
convertToDate,
convertToInt,
formatDurationMilliSeconds,
isNotEmptyString,
isNullOrUndefined,
isUndefined,
+ isValidTime,
logger,
sleep,
} from '../../../utils';
);
return OCPP16Constants.OCPP_RESPONSE_REJECTED;
}
- if (isEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId);
+ if (
+ isEmptyArray(
+ connectorStatus?.chargingProfiles &&
+ isEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles),
+ )
+ ) {
return OCPP16Constants.OCPP_RESPONSE_REJECTED;
}
- const startDate = new Date();
+ const currentDate = new Date();
const interval: Interval = {
- start: startDate,
- end: addSeconds(startDate, duration),
+ start: currentDate,
+ end: addSeconds(currentDate, duration),
};
- let compositeSchedule: OCPP16ChargingSchedule | undefined;
- for (const chargingProfile of chargingStation.getConnectorStatus(connectorId)!
- .chargingProfiles!) {
+ const chargingProfiles: OCPP16ChargingProfile[] = [];
+ for (const chargingProfile of cloneObject<OCPP16ChargingProfile[]>(
+ (connectorStatus?.chargingProfiles ?? []).concat(
+ chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [],
+ ),
+ ).sort((a, b) => b.stackLevel - a.stackLevel)) {
if (
- compositeSchedule?.chargingRateUnit &&
- compositeSchedule.chargingRateUnit !== chargingProfile.chargingSchedule.chargingRateUnit
+ connectorStatus?.transactionStarted &&
+ isNullOrUndefined(chargingProfile.chargingSchedule?.startSchedule)
) {
- logger.error(
- `${chargingStation.logPrefix()} Building composite schedule with different charging rate units is not yet supported, skipping charging profile id ${
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
chargingProfile.chargingProfileId
- }`,
+ } has no startSchedule defined. Trying to set it to the connector current transaction start date`,
);
- continue;
+ // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
+ chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
+ }
+ if (!isDate(chargingProfile.chargingSchedule?.startSchedule)) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
+ chargingProfile.chargingProfileId
+ } startSchedule property is not a Date object. Trying to convert it to a Date object`,
+ );
+ chargingProfile.chargingSchedule.startSchedule = convertToDate(
+ chargingProfile.chargingSchedule?.startSchedule,
+ )!;
+ }
+ switch (chargingProfile.chargingProfileKind) {
+ case ChargingProfileKindType.RECURRING:
+ if (!canProceedRecurringChargingProfile(chargingProfile, chargingStation.logPrefix())) {
+ continue;
+ }
+ prepareRecurringChargingProfile(
+ chargingProfile,
+ interval.start as Date,
+ chargingStation.logPrefix(),
+ );
+ break;
+ case ChargingProfileKindType.RELATIVE:
+ connectorStatus?.transactionStarted &&
+ (chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart);
+ break;
}
if (
- isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, interval) &&
- isWithinInterval(
- addSeconds(
- chargingProfile.chargingSchedule.startSchedule!,
- chargingProfile.chargingSchedule.duration!,
- ),
- interval,
+ !canProceedChargingProfile(
+ chargingProfile,
+ interval.start as Date,
+ chargingStation.logPrefix(),
)
) {
- compositeSchedule = {
- startSchedule: max([
- compositeSchedule?.startSchedule ?? interval.start,
- chargingProfile.chargingSchedule.startSchedule!,
- ]),
- duration: Math.max(
- compositeSchedule?.duration ?? -Infinity,
- chargingProfile.chargingSchedule.duration!,
- ),
- chargingRateUnit: chargingProfile.chargingSchedule.chargingRateUnit,
- ...(compositeSchedule?.chargingSchedulePeriod === undefined
- ? { chargingSchedulePeriod: [] }
- : {
- chargingSchedulePeriod: compositeSchedule.chargingSchedulePeriod.concat(
- ...chargingProfile.chargingSchedule.chargingSchedulePeriod,
- ),
- }),
- ...(chargingProfile.chargingSchedule.minChargeRate && {
- minChargeRate: Math.min(
- compositeSchedule?.minChargeRate ?? Infinity,
- chargingProfile.chargingSchedule.minChargeRate,
- ),
- }),
- };
+ continue;
+ }
+ // Add active charging profiles into chargingProfiles array
+ if (
+ isValidTime(chargingProfile.chargingSchedule?.startSchedule) &&
+ isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, interval)
+ ) {
+ chargingProfiles.push(chargingProfile);
}
}
+ const compositeSchedule: OCPP16ChargingSchedule = {
+ startSchedule: min(
+ chargingProfiles.map(
+ (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime,
+ ),
+ ),
+ duration: Math.max(
+ ...chargingProfiles.map(
+ (chargingProfile) => chargingProfile.chargingSchedule.duration ?? -Infinity,
+ ),
+ ),
+ chargingRateUnit: chargingProfiles.every(
+ (chargingProfile) =>
+ chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.AMPERE,
+ )
+ ? ChargingRateUnitType.AMPERE
+ : chargingProfiles.every(
+ (chargingProfile) =>
+ chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT,
+ )
+ ? ChargingRateUnitType.WATT
+ : ChargingRateUnitType.AMPERE,
+ // FIXME: remove overlapping charging schedule periods
+ chargingSchedulePeriod: chargingProfiles
+ .map((chargingProfile) => chargingProfile.chargingSchedule.chargingSchedulePeriod)
+ .reduce(
+ (accumulator, value) =>
+ accumulator.concat(value).sort((a, b) => a.startPeriod - b.startPeriod),
+ [],
+ ),
+ minChargeRate: Math.min(
+ ...chargingProfiles.map(
+ (chargingProfile) => chargingProfile.chargingSchedule.minChargeRate ?? Infinity,
+ ),
+ ),
+ };
return {
status: GenericStatus.Accepted,
- scheduleStart: compositeSchedule?.startSchedule,
+ scheduleStart: compositeSchedule.startSchedule!,
connectorId,
chargingSchedule: compositeSchedule,
};