import type { JSONSchemaType } from 'ajv';
import { Client, type FTPResponse } from 'basic-ftp';
-import { isWithinInterval, secondsToMilliseconds } from 'date-fns';
+import {
+ addSeconds,
+ differenceInSeconds,
+ isAfter,
+ isBefore,
+ isDate,
+ isWithinInterval,
+ max,
+ maxTime,
+ min,
+ minTime,
+ secondsToMilliseconds,
+} from 'date-fns';
import { create } from 'tar';
import { OCPP16Constants } from './OCPP16Constants';
import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
import {
type ChargingStation,
+ canProceedChargingProfile,
checkChargingStation,
getConfigurationKey,
+ prepareChargingProfileKind,
removeExpiredReservations,
setConfigurationKeyValue,
} from '../../../charging-station';
OCPP16ChargePointStatus,
type OCPP16ChargingProfile,
OCPP16ChargingProfilePurposeType,
+ OCPP16ChargingRateUnitType,
type OCPP16ChargingSchedule,
+ type OCPP16ChargingSchedulePeriod,
type OCPP16ClearCacheRequest,
type OCPP16DataTransferRequest,
type OCPP16DataTransferResponse,
} from '../../../types';
import {
Constants,
+ cloneObject,
convertToDate,
convertToInt,
formatDurationMilliSeconds,
isNotEmptyString,
isNullOrUndefined,
isUndefined,
+ isValidTime,
logger,
sleep,
} from '../../../utils';
) {
return OCPP16Constants.OCPP_RESPONSE_REJECTED;
}
- const { connectorId, duration } = commandPayload;
+ const { connectorId, duration, chargingRateUnit } = commandPayload;
if (chargingStation.hasConnector(connectorId) === false) {
logger.error(
`${chargingStation.logPrefix()} Trying to get composite schedule to a
);
return OCPP16Constants.OCPP_RESPONSE_REJECTED;
}
- if (isEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
+ if (connectorId === 0) {
+ logger.error(
+ `${chargingStation.logPrefix()} Get composite schedule on connector id ${connectorId} is not yet supported`,
+ );
return OCPP16Constants.OCPP_RESPONSE_REJECTED;
}
- const startDate = new Date();
- const endDate = new Date(startDate.getTime() + secondsToMilliseconds(duration));
- let compositeSchedule: OCPP16ChargingSchedule | undefined;
- for (const chargingProfile of chargingStation.getConnectorStatus(connectorId)!
- .chargingProfiles!) {
- // FIXME: build the composite schedule including the local power limit, the stack level, the charging rate unit, etc.
+ if (chargingRateUnit) {
+ logger.warn(
+ `${chargingStation.logPrefix()} Get composite schedule with a specified rate unit is not yet supported, no conversion will be done`,
+ );
+ }
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
+ if (
+ isEmptyArray(
+ connectorStatus?.chargingProfiles &&
+ isEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles),
+ )
+ ) {
+ return OCPP16Constants.OCPP_RESPONSE_REJECTED;
+ }
+ const currentDate = new Date();
+ const interval: Interval = {
+ start: currentDate,
+ end: addSeconds(currentDate, duration),
+ };
+ const storedChargingProfiles: OCPP16ChargingProfile[] = cloneObject<OCPP16ChargingProfile[]>(
+ (connectorStatus?.chargingProfiles ?? []).concat(
+ chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [],
+ ),
+ ).sort((a, b) => b.stackLevel - a.stackLevel);
+ const chargingProfiles: OCPP16ChargingProfile[] = [];
+ for (const storedChargingProfile of storedChargingProfiles) {
+ if (
+ connectorStatus?.transactionStarted &&
+ isNullOrUndefined(storedChargingProfile.chargingSchedule?.startSchedule)
+ ) {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
+ storedChargingProfile.chargingProfileId
+ } has no startSchedule defined. Trying to set it to the connector current transaction start date`,
+ );
+ // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
+ storedChargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
+ }
+ if (!isDate(storedChargingProfile.chargingSchedule?.startSchedule)) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
+ storedChargingProfile.chargingProfileId
+ } startSchedule property is not a Date object. Trying to convert it to a Date object`,
+ );
+ storedChargingProfile.chargingSchedule.startSchedule = convertToDate(
+ storedChargingProfile.chargingSchedule?.startSchedule,
+ )!;
+ }
if (
- isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, {
- start: startDate,
- end: endDate,
- })
+ !prepareChargingProfileKind(
+ connectorStatus,
+ storedChargingProfile,
+ interval.start as Date,
+ chargingStation.logPrefix(),
+ )
) {
- compositeSchedule = chargingProfile.chargingSchedule;
- break;
+ continue;
+ }
+ if (
+ !canProceedChargingProfile(
+ storedChargingProfile,
+ interval.start as Date,
+ chargingStation.logPrefix(),
+ )
+ ) {
+ continue;
+ }
+ // Add active charging profiles into chargingProfiles array
+ if (
+ isValidTime(storedChargingProfile.chargingSchedule?.startSchedule) &&
+ isWithinInterval(storedChargingProfile.chargingSchedule.startSchedule!, interval)
+ ) {
+ if (isEmptyArray(chargingProfiles)) {
+ if (
+ isAfter(
+ addSeconds(
+ storedChargingProfile.chargingSchedule.startSchedule!,
+ storedChargingProfile.chargingSchedule.duration!,
+ ),
+ interval.end,
+ )
+ ) {
+ storedChargingProfile.chargingSchedule.chargingSchedulePeriod =
+ storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter(
+ (schedulePeriod) =>
+ isWithinInterval(
+ addSeconds(
+ storedChargingProfile.chargingSchedule.startSchedule!,
+ schedulePeriod.startPeriod,
+ )!,
+ interval,
+ ),
+ );
+ storedChargingProfile.chargingSchedule.duration = differenceInSeconds(
+ interval.end,
+ storedChargingProfile.chargingSchedule.startSchedule!,
+ );
+ }
+ chargingProfiles.push(storedChargingProfile);
+ } else if (isNotEmptyArray(chargingProfiles)) {
+ const chargingProfilesInterval: Interval = {
+ start: min(
+ chargingProfiles.map(
+ (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime,
+ ),
+ ),
+ end: max(
+ chargingProfiles.map(
+ (chargingProfile) =>
+ addSeconds(
+ chargingProfile.chargingSchedule.startSchedule!,
+ chargingProfile.chargingSchedule.duration!,
+ ) ?? minTime,
+ ),
+ ),
+ };
+ let addChargingProfile = false;
+ if (
+ isBefore(interval.start, chargingProfilesInterval.start) &&
+ isBefore(
+ storedChargingProfile.chargingSchedule.startSchedule!,
+ chargingProfilesInterval.start,
+ )
+ ) {
+ // Remove charging schedule periods that are after the start of the active profiles interval
+ storedChargingProfile.chargingSchedule.chargingSchedulePeriod =
+ storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter(
+ (schedulePeriod) =>
+ isWithinInterval(
+ addSeconds(
+ storedChargingProfile.chargingSchedule.startSchedule!,
+ schedulePeriod.startPeriod,
+ ),
+ {
+ start: interval.start,
+ end: chargingProfilesInterval.start,
+ },
+ ),
+ );
+ addChargingProfile = true;
+ }
+ if (
+ isBefore(chargingProfilesInterval.end, interval.end) &&
+ isAfter(
+ addSeconds(
+ storedChargingProfile.chargingSchedule.startSchedule!,
+ storedChargingProfile.chargingSchedule.duration!,
+ ),
+ chargingProfilesInterval.end,
+ )
+ ) {
+ // Remove charging schedule periods that are before the end of the active profiles interval
+ // FIXME: can lead to a gap in the charging schedule: chargingProfilesInterval.end -> first matching schedulePeriod.startPeriod
+ storedChargingProfile.chargingSchedule.chargingSchedulePeriod =
+ storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter(
+ (schedulePeriod) =>
+ isWithinInterval(
+ addSeconds(
+ storedChargingProfile.chargingSchedule.startSchedule!,
+ schedulePeriod.startPeriod,
+ ),
+ {
+ start: chargingProfilesInterval.end,
+ end: interval.end,
+ },
+ ),
+ );
+ addChargingProfile = true;
+ }
+ addChargingProfile && chargingProfiles.push(storedChargingProfile);
+ }
}
}
+ const compositeScheduleStart: Date = min(
+ chargingProfiles.map(
+ (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime,
+ ),
+ );
+ const compositeScheduleDuration: number = Math.max(
+ ...chargingProfiles.map((chargingProfile) =>
+ isNaN(chargingProfile.chargingSchedule.duration!)
+ ? -Infinity
+ : chargingProfile.chargingSchedule.duration!,
+ ),
+ );
+ const compositeSchedulePeriods: OCPP16ChargingSchedulePeriod[] = chargingProfiles
+ .map((chargingProfile) => chargingProfile.chargingSchedule.chargingSchedulePeriod)
+ .reduce(
+ (accumulator, value) =>
+ accumulator.concat(value).sort((a, b) => a.startPeriod - b.startPeriod),
+ [],
+ );
+ const compositeSchedule: OCPP16ChargingSchedule = {
+ startSchedule: compositeScheduleStart,
+ duration: compositeScheduleDuration,
+ chargingRateUnit: chargingProfiles.every(
+ (chargingProfile) =>
+ chargingProfile.chargingSchedule.chargingRateUnit === OCPP16ChargingRateUnitType.AMPERE,
+ )
+ ? OCPP16ChargingRateUnitType.AMPERE
+ : chargingProfiles.every(
+ (chargingProfile) =>
+ chargingProfile.chargingSchedule.chargingRateUnit === OCPP16ChargingRateUnitType.WATT,
+ )
+ ? OCPP16ChargingRateUnitType.WATT
+ : OCPP16ChargingRateUnitType.AMPERE,
+ chargingSchedulePeriod: compositeSchedulePeriods,
+ minChargeRate: Math.min(
+ ...chargingProfiles.map((chargingProfile) =>
+ isNaN(chargingProfile.chargingSchedule.minChargeRate!)
+ ? Infinity
+ : chargingProfile.chargingSchedule.minChargeRate!,
+ ),
+ ),
+ };
return {
status: GenericStatus.Accepted,
- scheduleStart: compositeSchedule?.startSchedule,
+ scheduleStart: compositeSchedule.startSchedule!,
connectorId,
chargingSchedule: compositeSchedule,
};