import { fileURLToPath } from 'node:url';
import chalk from 'chalk';
-import { addSeconds, isAfter, isTomorrow, isYesterday } from 'date-fns';
+import {
+ addDays,
+ addSeconds,
+ addWeeks,
+ differenceInDays,
+ differenceInWeeks,
+ isAfter,
+ isBefore,
+ isWithinInterval,
+ toDate,
+} from 'date-fns';
import type { ChargingStation } from './ChargingStation';
import { BaseError } from '../exception';
isNotEmptyString,
isNullOrUndefined,
isUndefined,
+ isValidDate,
logger,
secureRandom,
} from '../utils';
connectorStatus.idTagAuthorized = false;
connectorStatus.transactionRemoteStarted = false;
connectorStatus.transactionStarted = false;
+ delete connectorStatus?.transactionStart;
+ delete connectorStatus?.transactionId;
delete connectorStatus?.localAuthorizeIdTag;
delete connectorStatus?.authorizeIdTag;
- delete connectorStatus?.transactionId;
delete connectorStatus?.transactionIdTag;
connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
delete connectorStatus?.transactionBeginMeterValue;
connectorId: number,
): number | undefined => {
let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
- // Get charging profiles for connector and sort by stack level
+ // 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 profiles on connector 0
+ // Get charging profiles on connector 0 and sort by stack level
if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
chargingProfiles.push(
...cloneObject<ChargingProfile[]>(
);
}
if (isNotEmptyArray(chargingProfiles)) {
- const result = getLimitFromChargingProfiles(chargingProfiles, chargingStation.logPrefix());
+ const result = getLimitFromChargingProfiles(
+ chargingStation,
+ connectorId,
+ chargingProfiles,
+ chargingStation.logPrefix(),
+ );
if (!isNullOrUndefined(result)) {
limit = result?.limit;
matchingChargingProfile = result?.matchingChargingProfile;
}
};
+interface ChargingProfilesLimit {
+ limit: number;
+ matchingChargingProfile: ChargingProfile;
+}
+
/**
* Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
*
* @param chargingProfiles -
* @param logPrefix -
- * @returns
+ * @returns ChargingProfilesLimit
*/
const getLimitFromChargingProfiles = (
+ chargingStation: ChargingStation,
+ connectorId: number,
chargingProfiles: ChargingProfile[],
logPrefix: string,
-):
- | {
- limit: number;
- matchingChargingProfile: ChargingProfile;
- }
- | undefined => {
+): ChargingProfilesLimit | undefined => {
const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
const currentDate = new Date();
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId);
for (const chargingProfile of chargingProfiles) {
- // Set helpers
+ if (
+ isValidDate(chargingProfile.validFrom) &&
+ isValidDate(chargingProfile.validTo) &&
+ !isWithinInterval(currentDate, {
+ start: chargingProfile.validFrom!,
+ end: chargingProfile.validTo!,
+ })
+ ) {
+ logger.debug(
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${
+ chargingProfile.chargingProfileId
+ } is not valid for the current date ${currentDate.toISOString()}`,
+ );
+ continue;
+ }
const chargingSchedule = chargingProfile.chargingSchedule;
- if (!chargingSchedule?.startSchedule) {
- logger.warn(
- `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`,
+ 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`,
);
+ // 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(
);
chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!;
}
- // Adjust the daily recurring schedule to today
if (
chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
- chargingProfile.recurrencyKind === RecurrencyKindType.DAILY
+ isNullOrUndefined(chargingProfile.recurrencyKind)
) {
- if (isYesterday(chargingSchedule.startSchedule)) {
- chargingSchedule.startSchedule.setFullYear(
- currentDate.getFullYear(),
- currentDate.getMonth(),
- currentDate.getDate(),
- );
- } else if (isTomorrow(chargingSchedule.startSchedule)) {
- chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
- }
+ logger.error(
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
+ );
+ continue;
+ }
+ if (chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING) {
+ prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
+ } else if (
+ chargingProfile.chargingProfileKind === ChargingProfileKindType.RELATIVE &&
+ connectorStatus?.transactionStarted
+ ) {
+ chargingSchedule.startSchedule = connectorStatus?.transactionStart;
}
// Check if the charging profile is active
if (
- isAfter(addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration!), currentDate)
+ isValidDate(chargingSchedule.startSchedule) &&
+ isAfter(addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!), currentDate)
) {
- let lastButOneSchedule: ChargingSchedulePeriod | undefined;
- // Search the right schedule period
- for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
- // Handling of only one period
+ if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
+ // Handling of only one schedule period
if (
chargingSchedule.chargingSchedulePeriod.length === 1 &&
- schedulePeriod.startPeriod === 0
+ chargingSchedule.chargingSchedulePeriod[0].startPeriod === 0
) {
- const result = {
- limit: schedulePeriod.limit,
+ const result: ChargingProfilesLimit = {
+ limit: chargingSchedule.chargingSchedulePeriod[0].limit,
matchingChargingProfile: chargingProfile,
};
logger.debug(debugLogMsg, result);
return result;
}
- // Find the right schedule period
- if (
- isAfter(
- addSeconds(chargingSchedule.startSchedule, schedulePeriod.startPeriod),
- currentDate,
- )
- ) {
- // Found the schedule: last but one is the correct one
- const result = {
- limit: lastButOneSchedule!.limit,
- matchingChargingProfile: chargingProfile,
- };
- logger.debug(debugLogMsg, result);
- return result;
- }
- // Keep it
- lastButOneSchedule = schedulePeriod;
- // Handle the last schedule period
- if (
- schedulePeriod.startPeriod ===
- chargingSchedule.chargingSchedulePeriod[
- chargingSchedule.chargingSchedulePeriod.length - 1
- ].startPeriod
- ) {
- const result = {
- limit: lastButOneSchedule.limit,
- matchingChargingProfile: chargingProfile,
- };
- logger.debug(debugLogMsg, result);
- return result;
+ let lastButOneSchedule: ChargingSchedulePeriod | undefined;
+ // Search for the right schedule period
+ for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
+ // Find the right schedule period
+ if (
+ isAfter(
+ addSeconds(chargingSchedule.startSchedule!, schedulePeriod.startPeriod),
+ currentDate,
+ )
+ ) {
+ // Found the schedule period: last but one is the correct one
+ const result: ChargingProfilesLimit = {
+ limit: lastButOneSchedule!.limit,
+ matchingChargingProfile: chargingProfile,
+ };
+ logger.debug(debugLogMsg, result);
+ return result;
+ }
+ // Keep it
+ lastButOneSchedule = schedulePeriod;
+ // Handle the last schedule period
+ if (
+ schedulePeriod.startPeriod ===
+ chargingSchedule.chargingSchedulePeriod[
+ chargingSchedule.chargingSchedulePeriod.length - 1
+ ].startPeriod
+ ) {
+ const result: ChargingProfilesLimit = {
+ limit: lastButOneSchedule.limit,
+ matchingChargingProfile: chargingProfile,
+ };
+ logger.debug(debugLogMsg, result);
+ return 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,
+) => {
+ const chargingSchedule = chargingProfile.chargingSchedule;
+ let recurringInterval: Interval;
+ switch (chargingProfile.recurrencyKind) {
+ case RecurrencyKindType.DAILY:
+ recurringInterval = {
+ start: chargingSchedule.startSchedule!,
+ end: addDays(chargingSchedule.startSchedule!, 1),
+ };
+ if (
+ !isWithinInterval(currentDate, recurringInterval) &&
+ isBefore(chargingSchedule.startSchedule!, currentDate)
+ ) {
+ chargingSchedule.startSchedule = addDays(
+ chargingSchedule.startSchedule!,
+ differenceInDays(chargingSchedule.startSchedule!, recurringInterval.end),
+ );
+ recurringInterval = {
+ start: chargingSchedule.startSchedule,
+ end: addDays(chargingSchedule.startSchedule, 1),
+ };
+ }
+ break;
+ case RecurrencyKindType.WEEKLY:
+ recurringInterval = {
+ start: chargingSchedule.startSchedule!,
+ end: addWeeks(chargingSchedule.startSchedule!, 1),
+ };
+ if (
+ !isWithinInterval(currentDate, recurringInterval) &&
+ isBefore(chargingSchedule.startSchedule!, currentDate)
+ ) {
+ chargingSchedule.startSchedule = addWeeks(
+ chargingSchedule.startSchedule!,
+ differenceInWeeks(chargingSchedule.startSchedule!, recurringInterval.end),
+ );
+ recurringInterval = {
+ start: chargingSchedule.startSchedule,
+ end: addWeeks(chargingSchedule.startSchedule, 1),
+ };
+ }
+ break;
+ }
+ if (!isWithinInterval(currentDate, recurringInterval!)) {
+ logger.error(
+ `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring ${
+ chargingProfile.recurrencyKind
+ } charging profile id ${
+ chargingProfile.chargingProfileId
+ } startSchedule ${chargingSchedule.startSchedule!.toISOString()} is not properly translated to current recurrency time interval [${toDate(
+ recurringInterval!.start,
+ ).toISOString()}, ${toDate(recurringInterval!.end).toISOString()}]`,
+ );
+ }
+};
+
const getRandomSerialNumberSuffix = (params?: {
randomBytesLength?: number;
upperCase?: boolean;