X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStationUtils.ts;h=0eeb205189456c4d0ea44ec96d3cfdf434722075;hb=142a66c9948adf165706107e1fcadf9f87bae559;hp=868d0adbf23970b8157ad9ca03256ec4eeaae533;hpb=f26233c0fd267bc2ef00636ca7531a0877bb5dbe;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStationUtils.ts b/src/charging-station/ChargingStationUtils.ts index 868d0adb..0eeb2051 100644 --- a/src/charging-station/ChargingStationUtils.ts +++ b/src/charging-station/ChargingStationUtils.ts @@ -4,7 +4,18 @@ import { basename, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import chalk from 'chalk'; -import moment from 'moment'; +import { + addDays, + addSeconds, + addWeeks, + differenceInDays, + differenceInSeconds, + differenceInWeeks, + isAfter, + isBefore, + isWithinInterval, + toDate, +} from 'date-fns'; import type { ChargingStation } from './ChargingStation'; import { BaseError } from '../exception'; @@ -36,13 +47,16 @@ import { Constants, DCElectricUtils, cloneObject, + convertToDate, convertToInt, + isArraySorted, isEmptyObject, isEmptyString, isNotEmptyArray, isNotEmptyString, isNullOrUndefined, isUndefined, + isValidDate, logger, secureRandom, } from '../utils'; @@ -181,7 +195,7 @@ export const checkTemplate = ( logger.error(`${logPrefix} ${errorMsg}`); throw new BaseError(errorMsg); } - if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator)) { + if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) { stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION; logger.warn( `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`, @@ -206,9 +220,9 @@ export const checkConnectorsConfiguration = ( } => { const configuredMaxConnectors = getConfiguredNumberOfConnectors(stationTemplate); checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile); - const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors); + const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!); checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile); - const templateMaxAvailableConnectors = stationTemplate?.Connectors[0] + const templateMaxAvailableConnectors = stationTemplate.Connectors![0] ? templateMaxConnectors - 1 : templateMaxConnectors; if ( @@ -271,15 +285,15 @@ export const initializeConnectorsMapStatus = ( ); } if (connectorId === 0) { - connectors.get(connectorId).availability = AvailabilityType.Operative; + connectors.get(connectorId)!.availability = AvailabilityType.Operative; if (isUndefined(connectors.get(connectorId)?.chargingProfiles)) { - connectors.get(connectorId).chargingProfiles = []; + connectors.get(connectorId)!.chargingProfiles = []; } } else if ( connectorId > 0 && isNullOrUndefined(connectors.get(connectorId)?.transactionStarted) ) { - initializeConnectorStatus(connectors.get(connectorId)); + initializeConnectorStatus(connectors.get(connectorId)!); } } }; @@ -289,9 +303,10 @@ export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => 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; @@ -354,10 +369,10 @@ export const warnTemplateKeysDeprecation = ( logPrefix: string, templateFile: string, ) => { - const templateKeys: { key: string; deprecatedKey: string }[] = [ - { key: 'supervisionUrls', deprecatedKey: 'supervisionUrl' }, - { key: 'idTagsFile', deprecatedKey: 'authorizationFile' }, - { key: 'ocppStrictCompliance', deprecatedKey: 'payloadSchemaValidation' }, + const templateKeys: { deprecatedKey: string; key?: string }[] = [ + { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' }, + { deprecatedKey: 'authorizationFile', key: 'idTagsFile' }, + { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' }, ]; for (const templateKey of templateKeys) { warnDeprecatedTemplateKey( @@ -365,7 +380,7 @@ export const warnTemplateKeysDeprecation = ( templateKey.deprecatedKey, logPrefix, templateFile, - `Use '${templateKey.key}' instead`, + !isUndefined(templateKey.key) ? `Use '${templateKey.key}' instead` : undefined, ); convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key); } @@ -377,8 +392,8 @@ export const stationTemplateToStationInfo = ( stationTemplate = cloneObject(stationTemplate); delete stationTemplate.power; delete stationTemplate.powerUnit; - delete stationTemplate?.Connectors; - delete stationTemplate?.Evses; + delete stationTemplate.Connectors; + delete stationTemplate.Evses; delete stationTemplate.Configuration; delete stationTemplate.AutomaticTransactionGenerator; delete stationTemplate.chargeBoxSerialNumberPrefix; @@ -453,49 +468,54 @@ export const getChargingStationConnectorChargingProfilesPowerLimit = ( chargingStation: ChargingStation, connectorId: number, ): number | undefined => { - let limit: number, matchingChargingProfile: ChargingProfile; - // Get charging profiles for connector and sort by stack level + let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined; + // Get charging profiles for connector id and sort by stack level const chargingProfiles = cloneObject( - chargingStation.getConnectorStatus(connectorId)?.chargingProfiles, + chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!, )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? []; - // Get profiles on connector 0 - if (chargingStation.getConnectorStatus(0)?.chargingProfiles) { + // Get charging profiles on connector 0 and sort by stack level + if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) { chargingProfiles.push( ...cloneObject( - chargingStation.getConnectorStatus(0).chargingProfiles, + chargingStation.getConnectorStatus(0)!.chargingProfiles!, ).sort((a, b) => b.stackLevel - a.stackLevel), ); } 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; switch (chargingStation.getCurrentOutType()) { case CurrentType.AC: limit = - matchingChargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT + matchingChargingProfile?.chargingSchedule?.chargingRateUnit === + ChargingRateUnitType.WATT ? limit : ACElectricUtils.powerTotal( chargingStation.getNumberOfPhases(), chargingStation.getVoltageOut(), - limit, + limit!, ); break; case CurrentType.DC: limit = - matchingChargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT + matchingChargingProfile?.chargingSchedule?.chargingRateUnit === + ChargingRateUnitType.WATT ? limit - : DCElectricUtils.power(chargingStation.getVoltageOut(), limit); + : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!); } const connectorMaximumPower = chargingStation.getMaximumPower() / chargingStation.powerDivider; - if (limit > connectorMaximumPower) { + 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; @@ -538,7 +558,7 @@ export const waitChargingStationEvents = async ( event: ChargingStationWorkerMessageEvents, eventsToWait: number, ): Promise => { - return new Promise((resolve) => { + return new Promise((resolve) => { let events = 0; if (eventsToWait === 0) { resolve(events); @@ -553,7 +573,7 @@ export const waitChargingStationEvents = async ( }; const getConfiguredNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => { - let configuredMaxConnectors: number; + let configuredMaxConnectors = 0; if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) { const numberOfConnectors = stationTemplate.numberOfConnectors as number[]; configuredMaxConnectors = @@ -561,11 +581,10 @@ const getConfiguredNumberOfConnectors = (stationTemplate: ChargingStationTemplat } else if (isUndefined(stationTemplate.numberOfConnectors) === false) { configuredMaxConnectors = stationTemplate.numberOfConnectors as number; } else if (stationTemplate.Connectors && !stationTemplate.Evses) { - configuredMaxConnectors = stationTemplate?.Connectors[0] + configuredMaxConnectors = stationTemplate.Connectors[0] ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1 : getMaxNumberOfConnectors(stationTemplate.Connectors); } else if (stationTemplate.Evses && !stationTemplate.Connectors) { - configuredMaxConnectors = 0; for (const evse in stationTemplate.Evses) { if (evse === '0') { continue; @@ -624,7 +643,7 @@ const warnDeprecatedTemplateKey = ( templateFile: string, logMsgToAppend = '', ): void => { - if (!isUndefined(template[key])) { + if (!isUndefined(template[key as keyof ChargingStationTemplate])) { const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${ isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : '' }`; @@ -636,119 +655,275 @@ const warnDeprecatedTemplateKey = ( const convertDeprecatedTemplateKey = ( template: ChargingStationTemplate, deprecatedKey: string, - key: string, + key?: string, ): void => { - if (!isUndefined(template[deprecatedKey])) { - template[key] = template[deprecatedKey] as unknown; - delete template[deprecatedKey]; + if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) { + if (!isUndefined(key)) { + (template as unknown as Record)[key!] = + template[deprecatedKey as keyof ChargingStationTemplate]; + } + delete template[deprecatedKey as keyof ChargingStationTemplate]; } }; +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; -} | null => { +): ChargingProfilesLimit | undefined => { const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`; - const currentMoment = moment(); const currentDate = new Date(); + const connectorStatus = chargingStation.getConnectorStatus(connectorId); for (const chargingProfile of chargingProfiles) { - // Set helpers + if ( + (isValidDate(chargingProfile.validFrom) && + isBefore(currentDate, chargingProfile.validFrom!)) || + (isValidDate(chargingProfile.validTo) && isAfter(currentDate, 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) { + 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( - `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`, + `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`, ); + chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!; } - // Check type (recurring) and if it is already active - // Adjust the daily recurring schedule to today if ( chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING && - chargingProfile.recurrencyKind === RecurrencyKindType.DAILY && - currentMoment.isAfter(chargingSchedule.startSchedule) + isNullOrUndefined(chargingProfile.recurrencyKind) ) { - 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`, - ); - chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule); - } - chargingSchedule.startSchedule.setFullYear( - currentDate.getFullYear(), - currentDate.getMonth(), - currentDate.getDate(), + logger.error( + `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`, ); - // Check if the start of the schedule is yesterday - if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) { - chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1); - } - } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) { - return null; + continue; + } + if (chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING) { + prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix); + } else if ( + chargingProfile.chargingProfileKind === ChargingProfileKindType.RELATIVE && + connectorStatus?.transactionStarted + ) { + chargingSchedule.startSchedule = connectorStatus?.transactionStart; + } + 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 ( - moment(chargingSchedule.startSchedule) - .add(chargingSchedule.duration, 's') - .isAfter(currentMoment) + isValidDate(chargingSchedule?.startSchedule) && + isWithinInterval(currentDate, { + start: chargingSchedule.startSchedule!, + end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!), + }) ) { - let lastButOneSchedule: ChargingSchedulePeriod; - // Search the right schedule period - for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) { - // Handling of only one period + if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) { + const chargingSchedulePeriodCompareFn = ( + a: ChargingSchedulePeriod, + b: ChargingSchedulePeriod, + ) => a.startPeriod - b.startPeriod; if ( - chargingSchedule.chargingSchedulePeriod.length === 1 && - schedulePeriod.startPeriod === 0 + isArraySorted( + chargingSchedule.chargingSchedulePeriod, + chargingSchedulePeriodCompareFn, + ) === false ) { - const result = { - limit: schedulePeriod.limit, - matchingChargingProfile: chargingProfile, - }; - logger.debug(debugLogMsg, result); - return result; + logger.warn( + `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`, + ); + chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn); } - // Find the right schedule period - if ( - moment(chargingSchedule.startSchedule) - .add(schedulePeriod.startPeriod, 's') - .isAfter(currentMoment) - ) { - // Found the schedule: last but one is the correct one - const result = { - limit: lastButOneSchedule.limit, - matchingChargingProfile: chargingProfile, - }; - logger.debug(debugLogMsg, result); - return result; + // 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; } - // Keep it - lastButOneSchedule = schedulePeriod; - // Handle the last schedule period - if ( - schedulePeriod.startPeriod === - chargingSchedule.chargingSchedulePeriod[ - chargingSchedule.chargingSchedulePeriod.length - 1 - ].startPeriod - ) { - const result = { - limit: lastButOneSchedule.limit, + // 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; + // Search for the right schedule period + for (const [index, schedulePeriod] of chargingSchedule.chargingSchedulePeriod.entries()) { + // 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 within the charging profile duration + if ( + 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, + matchingChargingProfile: chargingProfile, + }; + logger.debug(debugLogMsg, result); + return result; + } + } } } } - return null; +}; + +/** + * 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), + }; + 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), + }; + } + 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), + }; + } + break; + } + if (!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()}] is not properly translated to current date ${currentDate.toISOString()} `, + ); + } +}; + +const checkRecurringChargingProfileDuration = ( + chargingProfile: ChargingProfile, + interval: Interval, + logPrefix: string, +) => { + 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?: {