addSeconds,
addWeeks,
differenceInDays,
+ differenceInSeconds,
differenceInWeeks,
- endOfDay,
- endOfWeek,
isAfter,
isBefore,
isWithinInterval,
- startOfDay,
- startOfWeek,
+ toDate,
} from 'date-fns';
import type { ChargingStation } from './ChargingStation';
cloneObject,
convertToDate,
convertToInt,
+ isArraySorted,
isEmptyObject,
isEmptyString,
isNotEmptyArray,
isNotEmptyString,
isNullOrUndefined,
isUndefined,
+ isValidDate,
logger,
secureRandom,
} from '../utils';
-import { isValidDate } from '../utils/Utils';
const moduleName = 'ChargingStationUtils';
chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
)?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
// Get charging profiles on connector 0 and sort by stack level
- if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
+ if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) {
chargingProfiles.push(
...cloneObject<ChargingProfile[]>(
chargingStation.getConnectorStatus(0)!.chargingProfiles!,
chargingStation.getMaximumPower() / chargingStation.powerDivider;
if (limit! > connectorMaximumPower) {
logger.error(
- `${chargingStation.logPrefix()} Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
+ `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
result,
);
limit = connectorMaximumPower;
}
/**
- * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
+ * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
*
+ * @param chargingStation -
+ * @param connectorId -
* @param chargingProfiles -
* @param logPrefix -
* @returns ChargingProfilesLimit
const connectorStatus = chargingStation.getConnectorStatus(connectorId);
for (const chargingProfile of chargingProfiles) {
if (
- chargingProfile.validFrom &&
- chargingProfile.validTo &&
- !isWithinInterval(currentDate, {
- start: chargingProfile.validFrom,
- end: chargingProfile.validTo,
- })
+ (isValidDate(chargingProfile.validFrom) &&
+ isBefore(currentDate, chargingProfile.validFrom!)) ||
+ (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
) {
logger.debug(
`${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${
const chargingSchedule = chargingProfile.chargingSchedule;
if (connectorStatus?.transactionStarted && !chargingSchedule?.startSchedule) {
logger.debug(
- `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}. Trying to set it to the connector transaction start date`,
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.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
chargingSchedule.startSchedule = connectorStatus?.transactionStart;
}
if (!(chargingSchedule?.startSchedule instanceof Date)) {
logger.warn(
- `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`,
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
);
- chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!;
+ chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
}
if (
chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
);
continue;
}
- // Adjust recurring start schedule
if (chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING) {
- switch (chargingProfile.recurrencyKind) {
- case RecurrencyKindType.DAILY:
- if (isBefore(chargingSchedule.startSchedule, startOfDay(currentDate))) {
- addDays(
- chargingSchedule.startSchedule,
- differenceInDays(chargingSchedule.startSchedule, endOfDay(currentDate)),
- );
- if (
- isBefore(chargingSchedule.startSchedule, startOfDay(currentDate)) ||
- isAfter(chargingSchedule.startSchedule, endOfDay(currentDate))
- ) {
- logger.error(
- `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring ${
- chargingProfile.recurrencyKind
- } charging profile id ${
- chargingProfile.chargingProfileId
- } startSchedule ${chargingSchedule.startSchedule.toISOString()} is not properly translated to the current day`,
- );
- }
- }
- break;
- case RecurrencyKindType.WEEKLY:
- if (isBefore(chargingSchedule.startSchedule, startOfWeek(currentDate))) {
- addWeeks(
- chargingSchedule.startSchedule,
- differenceInWeeks(chargingSchedule.startSchedule, endOfWeek(currentDate)),
- );
- if (
- isBefore(chargingSchedule.startSchedule, startOfWeek(currentDate)) ||
- isAfter(chargingSchedule.startSchedule, endOfWeek(currentDate))
- ) {
- logger.error(
- `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring ${
- chargingProfile.recurrencyKind
- } charging profile id ${
- chargingProfile.chargingProfileId
- } startSchedule ${chargingSchedule.startSchedule.toISOString()} is not properly translated to the current week`,
- );
- }
- }
- break;
- }
+ prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
} else if (
chargingProfile.chargingProfileKind === ChargingProfileKindType.RELATIVE &&
connectorStatus?.transactionStarted
) {
chargingSchedule.startSchedule = connectorStatus?.transactionStart;
}
+ if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
+ logger.error(
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
+ );
+ continue;
+ }
+ if (isNullOrUndefined(chargingSchedule?.duration)) {
+ logger.error(
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
+ );
+ continue;
+ }
// Check if the charging profile is active
if (
- isValidDate(chargingSchedule.startSchedule) &&
- isAfter(addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!), currentDate)
+ isValidDate(chargingSchedule?.startSchedule) &&
+ isWithinInterval(currentDate, {
+ start: chargingSchedule.startSchedule!,
+ end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
+ })
) {
if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
- // Handling of only one schedule period
+ const chargingSchedulePeriodCompareFn = (
+ a: ChargingSchedulePeriod,
+ b: ChargingSchedulePeriod,
+ ) => a.startPeriod - b.startPeriod;
if (
- chargingSchedule.chargingSchedulePeriod.length === 1 &&
- chargingSchedule.chargingSchedulePeriod[0].startPeriod === 0
+ isArraySorted<ChargingSchedulePeriod>(
+ chargingSchedule.chargingSchedulePeriod,
+ chargingSchedulePeriodCompareFn,
+ ) === false
) {
+ logger.warn(
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
+ );
+ chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
+ }
+ // Check if the first schedule period start period is equal to 0
+ if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
+ logger.error(
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
+ );
+ continue;
+ }
+ // Handle only one schedule period
+ if (chargingSchedule.chargingSchedulePeriod.length === 1) {
const result: ChargingProfilesLimit = {
limit: chargingSchedule.chargingSchedulePeriod[0].limit,
matchingChargingProfile: chargingProfile,
logger.debug(debugLogMsg, result);
return result;
}
- let lastButOneSchedule: ChargingSchedulePeriod | undefined;
+ let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
// Search for the right schedule period
- for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
+ for (const [
+ index,
+ chargingSchedulePeriod,
+ ] of chargingSchedule.chargingSchedulePeriod.entries()) {
// Find the right schedule period
if (
isAfter(
- addSeconds(chargingSchedule.startSchedule!, schedulePeriod.startPeriod),
+ addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
currentDate,
)
) {
- // Found the schedule period: last but one is the correct one
+ // Found the schedule period: previous is the correct one
const result: ChargingProfilesLimit = {
- limit: lastButOneSchedule!.limit,
+ limit: previousChargingSchedulePeriod!.limit,
matchingChargingProfile: chargingProfile,
};
logger.debug(debugLogMsg, result);
return result;
}
- // Keep it
- lastButOneSchedule = schedulePeriod;
- // Handle the last schedule period
+ // Keep a reference to previous one
+ previousChargingSchedulePeriod = chargingSchedulePeriod;
+ // Handle the last schedule period within the charging profile duration
if (
- schedulePeriod.startPeriod ===
- chargingSchedule.chargingSchedulePeriod[
- chargingSchedule.chargingSchedulePeriod.length - 1
- ].startPeriod
+ index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
+ (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
+ chargingSchedule.duration! >
+ differenceInSeconds(
+ addSeconds(
+ chargingSchedule.startSchedule!,
+ chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
+ ),
+ chargingSchedule.startSchedule!,
+ ))
) {
const result: ChargingProfilesLimit = {
- limit: lastButOneSchedule.limit,
+ limit: previousChargingSchedulePeriod.limit,
matchingChargingProfile: chargingProfile,
};
logger.debug(debugLogMsg, result);
}
};
+/**
+ * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
+ *
+ * @param chargingProfile -
+ * @param currentDate -
+ * @param logPrefix -
+ */
+const prepareRecurringChargingProfile = (
+ chargingProfile: ChargingProfile,
+ currentDate: Date,
+ logPrefix: string,
+): boolean => {
+ const chargingSchedule = chargingProfile.chargingSchedule;
+ let recurringIntervalTranslated = false;
+ let recurringInterval: Interval;
+ switch (chargingProfile.recurrencyKind) {
+ case RecurrencyKindType.DAILY:
+ recurringInterval = {
+ start: chargingSchedule.startSchedule!,
+ end: addDays(chargingSchedule.startSchedule!, 1),
+ };
+ checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
+ if (
+ !isWithinInterval(currentDate, recurringInterval) &&
+ isBefore(recurringInterval.end, currentDate)
+ ) {
+ chargingSchedule.startSchedule = addDays(
+ recurringInterval.start,
+ differenceInDays(currentDate, recurringInterval.start),
+ );
+ recurringInterval = {
+ start: chargingSchedule.startSchedule,
+ end: addDays(chargingSchedule.startSchedule, 1),
+ };
+ recurringIntervalTranslated = true;
+ }
+ break;
+ case RecurrencyKindType.WEEKLY:
+ recurringInterval = {
+ start: chargingSchedule.startSchedule!,
+ end: addWeeks(chargingSchedule.startSchedule!, 1),
+ };
+ checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
+ if (
+ !isWithinInterval(currentDate, recurringInterval) &&
+ isBefore(recurringInterval.end, currentDate)
+ ) {
+ chargingSchedule.startSchedule = addWeeks(
+ recurringInterval.start,
+ differenceInWeeks(currentDate, recurringInterval.start),
+ );
+ recurringInterval = {
+ start: chargingSchedule.startSchedule,
+ end: addWeeks(chargingSchedule.startSchedule, 1),
+ };
+ recurringIntervalTranslated = true;
+ }
+ break;
+ default:
+ logger.error(
+ `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
+ );
+ }
+ if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
+ logger.error(
+ `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
+ chargingProfile.recurrencyKind
+ } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
+ recurringInterval!.start,
+ ).toISOString()}, ${toDate(
+ recurringInterval!.end,
+ ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
+ );
+ }
+ return recurringIntervalTranslated;
+};
+
+const checkRecurringChargingProfileDuration = (
+ chargingProfile: ChargingProfile,
+ interval: Interval,
+ logPrefix: string,
+): void => {
+ if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
+ logger.warn(
+ `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
+ chargingProfile.chargingProfileKind
+ } charging profile id ${
+ chargingProfile.chargingProfileId
+ } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
+ interval.end,
+ interval.start,
+ )}`,
+ );
+ chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
+ } else if (
+ chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
+ ) {
+ logger.warn(
+ `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
+ chargingProfile.chargingProfileKind
+ } charging profile id ${chargingProfile.chargingProfileId} duration ${
+ chargingProfile.chargingSchedule.duration
+ } is greater than the recurrency time interval duration ${differenceInSeconds(
+ interval.end,
+ interval.start,
+ )}`,
+ );
+ chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
+ }
+};
+
const getRandomSerialNumberSuffix = (params?: {
randomBytesLength?: number;
upperCase?: boolean;