refactor: remove isUndefined() helper
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
CommitLineData
66a7748d
JB
1import { createHash, randomBytes } from 'node:crypto'
2import type { EventEmitter } from 'node:events'
3import { basename, dirname, join } from 'node:path'
4import { env } from 'node:process'
5import { fileURLToPath } from 'node:url'
8114d10e 6
66a7748d 7import chalk from 'chalk'
f924d466 8import {
f1e3871b 9 type Interval,
f924d466 10 addDays,
f924d466
JB
11 addSeconds,
12 addWeeks,
497588ef 13 differenceInDays,
d476bc1b 14 differenceInSeconds,
497588ef 15 differenceInWeeks,
f924d466
JB
16 isAfter,
17 isBefore,
0bd926c1 18 isDate,
90aceaf6 19 isPast,
252a7d22 20 isWithinInterval,
66a7748d
JB
21 toDate
22} from 'date-fns'
23import { maxTime } from 'date-fns/constants'
8114d10e 24
66a7748d
JB
25import type { ChargingStation } from './ChargingStation.js'
26import { getConfigurationKey } from './ConfigurationKeyUtils.js'
27import { BaseError } from '../exception/index.js'
981ebfbe 28import {
492cf6ab 29 AmpereUnits,
04b1261c 30 AvailabilityType,
268a74bb
JB
31 type BootNotificationRequest,
32 BootReasonEnumType,
15068be9 33 type ChargingProfile,
268a74bb 34 ChargingProfileKindType,
15068be9
JB
35 ChargingRateUnitType,
36 type ChargingSchedulePeriod,
73edcc94 37 type ChargingStationConfiguration,
268a74bb
JB
38 type ChargingStationInfo,
39 type ChargingStationTemplate,
66a7748d 40 type ChargingStationWorkerMessageEvents,
dd08d43d 41 ConnectorPhaseRotation,
a78ef5ed 42 type ConnectorStatus,
c3b83130 43 ConnectorStatusEnum,
268a74bb 44 CurrentType,
ae25f265 45 type EvseTemplate,
268a74bb
JB
46 type OCPP16BootNotificationRequest,
47 type OCPP20BootNotificationRequest,
48 OCPPVersion,
49 RecurrencyKindType,
90aceaf6
JB
50 type Reservation,
51 ReservationTerminationReason,
d8093be1 52 StandardParametersKey,
66a7748d
JB
53 type SupportedFeatureProfiles,
54 Voltage
55} from '../types/index.js'
9bf0ef23
JB
56import {
57 ACElectricUtils,
58 Constants,
59 DCElectricUtils,
60 cloneObject,
b85cef4c 61 convertToDate,
9bf0ef23 62 convertToInt,
80c58041 63 isArraySorted,
9bf0ef23
JB
64 isEmptyObject,
65 isEmptyString,
66 isNotEmptyArray,
67 isNotEmptyString,
0bd926c1 68 isValidTime,
9bf0ef23 69 logger,
66a7748d
JB
70 secureRandom
71} from '../utils/index.js'
17ac262c 72
66a7748d 73const moduleName = 'Helpers'
91a4f151 74
fba11dc6
JB
75export const getChargingStationId = (
76 index: number,
66a7748d 77 stationTemplate: ChargingStationTemplate | undefined
fba11dc6 78): string => {
a807045b 79 if (stationTemplate == null) {
66a7748d 80 return "Unknown 'chargingStationId'"
c1f16afd 81 }
fba11dc6 82 // In case of multiple instances: add instance index to charging station id
66a7748d
JB
83 const instanceIndex = env.CF_INSTANCE_INDEX ?? 0
84 const idSuffix = stationTemplate?.nameSuffix ?? ''
85 const idStr = `000000000${index.toString()}`
a807045b 86 return stationTemplate?.fixedName === true
fba11dc6 87 ? stationTemplate.baseName
f1bd9d19 88 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
66a7748d
JB
89 idStr.length - 4
90 )}${idSuffix}`
91}
fba11dc6 92
90aceaf6 93export const hasReservationExpired = (reservation: Reservation): boolean => {
66a7748d
JB
94 return isPast(reservation.expiryDate)
95}
90aceaf6
JB
96
97export const removeExpiredReservations = async (
66a7748d 98 chargingStation: ChargingStation
90aceaf6
JB
99): Promise<void> => {
100 if (chargingStation.hasEvses) {
101 for (const evseStatus of chargingStation.evses.values()) {
102 for (const connectorStatus of evseStatus.connectors.values()) {
66a7748d
JB
103 if (
104 connectorStatus.reservation != null &&
105 hasReservationExpired(connectorStatus.reservation)
106 ) {
90aceaf6
JB
107 await chargingStation.removeReservation(
108 connectorStatus.reservation,
66a7748d
JB
109 ReservationTerminationReason.EXPIRED
110 )
90aceaf6
JB
111 }
112 }
113 }
114 } else {
115 for (const connectorStatus of chargingStation.connectors.values()) {
66a7748d
JB
116 if (
117 connectorStatus.reservation != null &&
118 hasReservationExpired(connectorStatus.reservation)
119 ) {
90aceaf6
JB
120 await chargingStation.removeReservation(
121 connectorStatus.reservation,
66a7748d
JB
122 ReservationTerminationReason.EXPIRED
123 )
90aceaf6
JB
124 }
125 }
126 }
66a7748d 127}
90aceaf6
JB
128
129export const getNumberOfReservableConnectors = (
66a7748d 130 connectors: Map<number, ConnectorStatus>
90aceaf6 131): number => {
66a7748d 132 let numberOfReservableConnectors = 0
fba11dc6
JB
133 for (const [connectorId, connectorStatus] of connectors) {
134 if (connectorId === 0) {
66a7748d 135 continue
3fa7f799 136 }
fba11dc6 137 if (connectorStatus.status === ConnectorStatusEnum.Available) {
66a7748d 138 ++numberOfReservableConnectors
1bf29f5b 139 }
1bf29f5b 140 }
66a7748d
JB
141 return numberOfReservableConnectors
142}
fba11dc6
JB
143
144export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
145 const chargingStationInfo = {
146 chargePointModel: stationTemplate.chargePointModel,
147 chargePointVendor: stationTemplate.chargePointVendor,
300418e9 148 ...(stationTemplate.chargeBoxSerialNumberPrefix !== undefined && {
66a7748d 149 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix
fba11dc6 150 }),
300418e9 151 ...(stationTemplate.chargePointSerialNumberPrefix !== undefined && {
66a7748d 152 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix
fba11dc6 153 }),
300418e9 154 ...(stationTemplate.meterSerialNumberPrefix !== undefined && {
66a7748d 155 meterSerialNumber: stationTemplate.meterSerialNumberPrefix
fba11dc6 156 }),
300418e9 157 ...(stationTemplate.meterType !== undefined && {
66a7748d
JB
158 meterType: stationTemplate.meterType
159 })
160 }
fba11dc6
JB
161 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
162 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
66a7748d
JB
163 .digest('hex')
164}
fba11dc6
JB
165
166export const checkChargingStation = (
167 chargingStation: ChargingStation,
66a7748d 168 logPrefix: string
fba11dc6 169): boolean => {
66a7748d
JB
170 if (!chargingStation.started && !chargingStation.starting) {
171 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`)
172 return false
fba11dc6 173 }
66a7748d
JB
174 return true
175}
fba11dc6
JB
176
177export const getPhaseRotationValue = (
178 connectorId: number,
66a7748d 179 numberOfPhases: number
fba11dc6
JB
180): string | undefined => {
181 // AC/DC
182 if (connectorId === 0 && numberOfPhases === 0) {
66a7748d 183 return `${connectorId}.${ConnectorPhaseRotation.RST}`
fba11dc6 184 } else if (connectorId > 0 && numberOfPhases === 0) {
66a7748d 185 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
fba11dc6 186 // AC
d181b12b 187 } else if (connectorId >= 0 && numberOfPhases === 1) {
66a7748d 188 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
d181b12b 189 } else if (connectorId >= 0 && numberOfPhases === 3) {
66a7748d 190 return `${connectorId}.${ConnectorPhaseRotation.RST}`
fba11dc6 191 }
66a7748d 192}
fba11dc6
JB
193
194export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate>): number => {
66a7748d
JB
195 if (evses == null) {
196 return -1
fba11dc6 197 }
66a7748d
JB
198 return Object.keys(evses).length
199}
fba11dc6
JB
200
201const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
66a7748d
JB
202 if (connectors == null) {
203 return -1
fba11dc6 204 }
66a7748d
JB
205 return Object.keys(connectors).length
206}
fba11dc6
JB
207
208export const getBootConnectorStatus = (
209 chargingStation: ChargingStation,
210 connectorId: number,
66a7748d 211 connectorStatus: ConnectorStatus
fba11dc6 212): ConnectorStatusEnum => {
66a7748d 213 let connectorBootStatus: ConnectorStatusEnum
fba11dc6 214 if (
66a7748d
JB
215 connectorStatus?.status == null &&
216 (!chargingStation.isChargingStationAvailable() ||
217 !chargingStation.isConnectorAvailable(connectorId))
fba11dc6 218 ) {
66a7748d
JB
219 connectorBootStatus = ConnectorStatusEnum.Unavailable
220 } else if (connectorStatus?.status == null && connectorStatus?.bootStatus != null) {
fba11dc6 221 // Set boot status in template at startup
66a7748d
JB
222 connectorBootStatus = connectorStatus?.bootStatus
223 } else if (connectorStatus?.status != null) {
fba11dc6 224 // Set previous status at startup
66a7748d 225 connectorBootStatus = connectorStatus?.status
fba11dc6
JB
226 } else {
227 // Set default status
66a7748d 228 connectorBootStatus = ConnectorStatusEnum.Available
fba11dc6 229 }
66a7748d
JB
230 return connectorBootStatus
231}
fba11dc6
JB
232
233export const checkTemplate = (
234 stationTemplate: ChargingStationTemplate,
235 logPrefix: string,
66a7748d 236 templateFile: string
fba11dc6 237): void => {
66a7748d
JB
238 if (stationTemplate == null) {
239 const errorMsg = `Failed to read charging station template file ${templateFile}`
240 logger.error(`${logPrefix} ${errorMsg}`)
241 throw new BaseError(errorMsg)
fba11dc6
JB
242 }
243 if (isEmptyObject(stationTemplate)) {
66a7748d
JB
244 const errorMsg = `Empty charging station information from template file ${templateFile}`
245 logger.error(`${logPrefix} ${errorMsg}`)
246 throw new BaseError(errorMsg)
fba11dc6 247 }
66a7748d 248 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 249 if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
66a7748d 250 stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION
fba11dc6
JB
251 logger.warn(
252 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
66a7748d
JB
253 Constants.DEFAULT_ATG_CONFIGURATION
254 )
fa7bccf4 255 }
be9f397b 256 if (stationTemplate.idTagsFile == null || isEmptyString(stationTemplate.idTagsFile)) {
fba11dc6 257 logger.warn(
66a7748d
JB
258 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
259 )
c3b83130 260 }
66a7748d 261}
fba11dc6 262
73edcc94
JB
263export const checkConfiguration = (
264 stationConfiguration: ChargingStationConfiguration | undefined,
265 logPrefix: string,
66a7748d 266 configurationFile: string
73edcc94 267): void => {
a807045b 268 if (stationConfiguration == null) {
66a7748d
JB
269 const errorMsg = `Failed to read charging station configuration file ${configurationFile}`
270 logger.error(`${logPrefix} ${errorMsg}`)
271 throw new BaseError(errorMsg)
73edcc94 272 }
a807045b 273 if (isEmptyObject(stationConfiguration)) {
66a7748d
JB
274 const errorMsg = `Empty charging station configuration from file ${configurationFile}`
275 logger.error(`${logPrefix} ${errorMsg}`)
276 throw new BaseError(errorMsg)
73edcc94 277 }
66a7748d 278}
73edcc94 279
fba11dc6
JB
280export const checkConnectorsConfiguration = (
281 stationTemplate: ChargingStationTemplate,
282 logPrefix: string,
66a7748d 283 templateFile: string
fba11dc6 284): {
66a7748d
JB
285 configuredMaxConnectors: number
286 templateMaxConnectors: number
287 templateMaxAvailableConnectors: number
fba11dc6 288} => {
66a7748d
JB
289 const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate)
290 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile)
291 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
292 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!)
293 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile)
294 const templateMaxAvailableConnectors =
295 stationTemplate.Connectors?.[0] != null ? templateMaxConnectors - 1 : templateMaxConnectors
fba11dc6
JB
296 if (
297 configuredMaxConnectors > templateMaxAvailableConnectors &&
66a7748d 298 stationTemplate?.randomConnectors === false
8a133cc8 299 ) {
fba11dc6 300 logger.warn(
66a7748d
JB
301 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
302 )
303 stationTemplate.randomConnectors = true
fba11dc6 304 }
66a7748d
JB
305 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors }
306}
fba11dc6
JB
307
308export const checkStationInfoConnectorStatus = (
309 connectorId: number,
310 connectorStatus: ConnectorStatus,
311 logPrefix: string,
66a7748d 312 templateFile: string
fba11dc6 313): void => {
a807045b 314 if (connectorStatus?.status != null) {
fba11dc6 315 logger.warn(
66a7748d
JB
316 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
317 )
318 delete connectorStatus.status
fba11dc6 319 }
66a7748d 320}
fba11dc6
JB
321
322export const buildConnectorsMap = (
323 connectors: Record<string, ConnectorStatus>,
324 logPrefix: string,
66a7748d 325 templateFile: string
fba11dc6 326): Map<number, ConnectorStatus> => {
66a7748d 327 const connectorsMap = new Map<number, ConnectorStatus>()
fba11dc6
JB
328 if (getMaxNumberOfConnectors(connectors) > 0) {
329 for (const connector in connectors) {
66a7748d
JB
330 const connectorStatus = connectors[connector]
331 const connectorId = convertToInt(connector)
332 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile)
333 connectorsMap.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus))
fa7bccf4 334 }
fba11dc6
JB
335 } else {
336 logger.warn(
66a7748d
JB
337 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
338 )
fa7bccf4 339 }
66a7748d
JB
340 return connectorsMap
341}
fa7bccf4 342
fba11dc6
JB
343export const initializeConnectorsMapStatus = (
344 connectors: Map<number, ConnectorStatus>,
66a7748d 345 logPrefix: string
fba11dc6
JB
346): void => {
347 for (const connectorId of connectors.keys()) {
348 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
04b1261c 349 logger.warn(
5edd8ba0 350 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
66a7748d
JB
351 connectorId
352 )?.transactionId}`
353 )
04b1261c 354 }
fba11dc6 355 if (connectorId === 0) {
66a7748d
JB
356 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
357 connectors.get(connectorId)!.availability = AvailabilityType.Operative
300418e9 358 if (connectors.get(connectorId)?.chargingProfiles == null) {
66a7748d
JB
359 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
360 connectors.get(connectorId)!.chargingProfiles = []
04b1261c 361 }
be9f397b 362 } else if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted == null) {
66a7748d
JB
363 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
364 initializeConnectorStatus(connectors.get(connectorId)!)
04b1261c
JB
365 }
366 }
66a7748d 367}
fba11dc6
JB
368
369export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
86f51b96 370 connectorStatus.chargingProfiles =
66a7748d 371 connectorStatus.transactionId != null && isNotEmptyArray(connectorStatus.chargingProfiles)
86f51b96 372 ? connectorStatus.chargingProfiles?.filter(
66a7748d
JB
373 (chargingProfile) => chargingProfile.transactionId !== connectorStatus.transactionId
374 )
375 : []
376 connectorStatus.idTagLocalAuthorized = false
377 connectorStatus.idTagAuthorized = false
378 connectorStatus.transactionRemoteStarted = false
379 connectorStatus.transactionStarted = false
380 delete connectorStatus?.transactionStart
381 delete connectorStatus?.transactionId
382 delete connectorStatus?.localAuthorizeIdTag
383 delete connectorStatus?.authorizeIdTag
384 delete connectorStatus?.transactionIdTag
385 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
386 delete connectorStatus?.transactionBeginMeterValue
387}
fba11dc6
JB
388
389export const createBootNotificationRequest = (
390 stationInfo: ChargingStationInfo,
66a7748d 391 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
fba11dc6 392): BootNotificationRequest => {
66a7748d
JB
393 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
394 const ocppVersion = stationInfo.ocppVersion!
fba11dc6
JB
395 switch (ocppVersion) {
396 case OCPPVersion.VERSION_16:
397 return {
398 chargePointModel: stationInfo.chargePointModel,
399 chargePointVendor: stationInfo.chargePointVendor,
300418e9 400 ...(stationInfo.chargeBoxSerialNumber !== undefined && {
66a7748d 401 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
fba11dc6 402 }),
300418e9 403 ...(stationInfo.chargePointSerialNumber !== undefined && {
66a7748d 404 chargePointSerialNumber: stationInfo.chargePointSerialNumber
fba11dc6 405 }),
300418e9 406 ...(stationInfo.firmwareVersion !== undefined && {
66a7748d 407 firmwareVersion: stationInfo.firmwareVersion
fba11dc6 408 }),
300418e9
JB
409 ...(stationInfo.iccid !== undefined && { iccid: stationInfo.iccid }),
410 ...(stationInfo.imsi !== undefined && { imsi: stationInfo.imsi }),
411 ...(stationInfo.meterSerialNumber !== undefined && {
66a7748d 412 meterSerialNumber: stationInfo.meterSerialNumber
fba11dc6 413 }),
300418e9 414 ...(stationInfo.meterType !== undefined && {
66a7748d
JB
415 meterType: stationInfo.meterType
416 })
417 } satisfies OCPP16BootNotificationRequest
fba11dc6
JB
418 case OCPPVersion.VERSION_20:
419 case OCPPVersion.VERSION_201:
420 return {
421 reason: bootReason,
422 chargingStation: {
423 model: stationInfo.chargePointModel,
424 vendorName: stationInfo.chargePointVendor,
300418e9 425 ...(stationInfo.firmwareVersion !== undefined && {
66a7748d 426 firmwareVersion: stationInfo.firmwareVersion
d270cc87 427 }),
300418e9 428 ...(stationInfo.chargeBoxSerialNumber !== undefined && {
66a7748d 429 serialNumber: stationInfo.chargeBoxSerialNumber
d270cc87 430 }),
300418e9 431 ...((stationInfo.iccid !== undefined || stationInfo.imsi !== undefined) && {
fba11dc6 432 modem: {
300418e9
JB
433 ...(stationInfo.iccid !== undefined && { iccid: stationInfo.iccid }),
434 ...(stationInfo.imsi !== undefined && { imsi: stationInfo.imsi })
66a7748d
JB
435 }
436 })
437 }
438 } satisfies OCPP20BootNotificationRequest
fba11dc6 439 }
66a7748d 440}
fba11dc6
JB
441
442export const warnTemplateKeysDeprecation = (
443 stationTemplate: ChargingStationTemplate,
444 logPrefix: string,
66a7748d
JB
445 templateFile: string
446): void => {
447 const templateKeys: Array<{ deprecatedKey: string, key?: string }> = [
e4c6cf05
JB
448 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
449 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
450 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
66a7748d
JB
451 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' }
452 ]
fba11dc6
JB
453 for (const templateKey of templateKeys) {
454 warnDeprecatedTemplateKey(
455 stationTemplate,
456 templateKey.deprecatedKey,
457 logPrefix,
458 templateFile,
300418e9 459 templateKey.key !== undefined ? `Use '${templateKey.key}' instead` : undefined
66a7748d
JB
460 )
461 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
fba11dc6 462 }
66a7748d 463}
fba11dc6
JB
464
465export const stationTemplateToStationInfo = (
66a7748d 466 stationTemplate: ChargingStationTemplate
fba11dc6 467): ChargingStationInfo => {
66a7748d
JB
468 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate)
469 delete stationTemplate.power
470 delete stationTemplate.powerUnit
471 delete stationTemplate.Connectors
472 delete stationTemplate.Evses
473 delete stationTemplate.Configuration
474 delete stationTemplate.AutomaticTransactionGenerator
475 delete stationTemplate.chargeBoxSerialNumberPrefix
476 delete stationTemplate.chargePointSerialNumberPrefix
477 delete stationTemplate.meterSerialNumberPrefix
478 return stationTemplate as ChargingStationInfo
479}
fba11dc6
JB
480
481export const createSerialNumber = (
482 stationTemplate: ChargingStationTemplate,
483 stationInfo: ChargingStationInfo,
7f3decca 484 params?: {
66a7748d
JB
485 randomSerialNumberUpperCase?: boolean
486 randomSerialNumber?: boolean
487 }
fba11dc6 488): void => {
66a7748d
JB
489 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params }
490 const serialNumberSuffix =
491 params?.randomSerialNumber === true
492 ? getRandomSerialNumberSuffix({
493 upperCase: params.randomSerialNumberUpperCase
fba11dc6 494 })
66a7748d 495 : ''
fba11dc6 496 isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
66a7748d 497 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
fba11dc6 498 isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
66a7748d 499 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
fba11dc6 500 isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
66a7748d
JB
501 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
502}
fba11dc6
JB
503
504export const propagateSerialNumber = (
505 stationTemplate: ChargingStationTemplate,
506 stationInfoSrc: ChargingStationInfo,
66a7748d
JB
507 stationInfoDst: ChargingStationInfo
508): void => {
509 if (stationInfoSrc == null || stationTemplate == null) {
fba11dc6 510 throw new BaseError(
66a7748d
JB
511 'Missing charging station template or existing configuration to propagate serial number'
512 )
17ac262c 513 }
66a7748d
JB
514 stationTemplate?.chargePointSerialNumberPrefix != null &&
515 stationInfoSrc?.chargePointSerialNumber != null
fba11dc6 516 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
66a7748d
JB
517 : stationInfoDst?.chargePointSerialNumber != null &&
518 delete stationInfoDst.chargePointSerialNumber
519 stationTemplate?.chargeBoxSerialNumberPrefix != null &&
520 stationInfoSrc?.chargeBoxSerialNumber != null
fba11dc6 521 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
66a7748d
JB
522 : stationInfoDst?.chargeBoxSerialNumber != null && delete stationInfoDst.chargeBoxSerialNumber
523 stationTemplate?.meterSerialNumberPrefix != null && stationInfoSrc?.meterSerialNumber != null
fba11dc6 524 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
66a7748d
JB
525 : stationInfoDst?.meterSerialNumber != null && delete stationInfoDst.meterSerialNumber
526}
fba11dc6 527
d8093be1
JB
528export const hasFeatureProfile = (
529 chargingStation: ChargingStation,
66a7748d 530 featureProfile: SupportedFeatureProfiles
d8093be1
JB
531): boolean | undefined => {
532 return getConfigurationKey(
533 chargingStation,
66a7748d
JB
534 StandardParametersKey.SupportedFeatureProfiles
535 )?.value?.includes(featureProfile)
536}
d8093be1 537
fba11dc6 538export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
66a7748d 539 let unitDivider = 1
fba11dc6
JB
540 switch (stationInfo.amperageLimitationUnit) {
541 case AmpereUnits.DECI_AMPERE:
66a7748d
JB
542 unitDivider = 10
543 break
fba11dc6 544 case AmpereUnits.CENTI_AMPERE:
66a7748d
JB
545 unitDivider = 100
546 break
fba11dc6 547 case AmpereUnits.MILLI_AMPERE:
66a7748d
JB
548 unitDivider = 1000
549 break
fba11dc6 550 }
66a7748d
JB
551 return unitDivider
552}
fba11dc6 553
6fc0c6f3
JB
554/**
555 * Gets the connector cloned charging profiles applying a power limitation
21ee4dc2 556 * and sorted by connector id descending then stack level descending
6fc0c6f3
JB
557 *
558 * @param chargingStation -
559 * @param connectorId -
560 * @returns connector charging profiles array
561 */
562export const getConnectorChargingProfiles = (
563 chargingStation: ChargingStation,
66a7748d
JB
564 connectorId: number
565): ChargingProfile[] => {
6fc0c6f3 566 return cloneObject<ChargingProfile[]>(
21ee4dc2 567 (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
6fc0c6f3
JB
568 .sort((a, b) => b.stackLevel - a.stackLevel)
569 .concat(
21ee4dc2 570 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
66a7748d
JB
571 (a, b) => b.stackLevel - a.stackLevel
572 )
573 )
574 )
575}
6fc0c6f3 576
fba11dc6
JB
577export const getChargingStationConnectorChargingProfilesPowerLimit = (
578 chargingStation: ChargingStation,
66a7748d 579 connectorId: number
fba11dc6 580): number | undefined => {
66a7748d 581 let limit: number | undefined, chargingProfile: ChargingProfile | undefined
6fc0c6f3 582 // Get charging profiles sorted by connector id then stack level
66a7748d 583 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
fba11dc6 584 if (isNotEmptyArray(chargingProfiles)) {
a71d4e70
JB
585 const result = getLimitFromChargingProfiles(
586 chargingStation,
587 connectorId,
588 chargingProfiles,
66a7748d
JB
589 chargingStation.logPrefix()
590 )
be9f397b 591 if (result != null) {
66a7748d
JB
592 limit = result?.limit
593 chargingProfile = result?.chargingProfile
5398cecf 594 switch (chargingStation.stationInfo?.currentOutType) {
fba11dc6
JB
595 case CurrentType.AC:
596 limit =
bbb55ee4 597 chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6
JB
598 ? limit
599 : ACElectricUtils.powerTotal(
66a7748d
JB
600 chargingStation.getNumberOfPhases(),
601 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
602 chargingStation.stationInfo.voltageOut!,
be9f397b 603 limit
66a7748d
JB
604 )
605 break
fba11dc6
JB
606 case CurrentType.DC:
607 limit =
bbb55ee4 608 chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6 609 ? limit
66a7748d 610 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
be9f397b 611 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
fba11dc6
JB
612 }
613 const connectorMaximumPower =
66a7748d
JB
614 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
615 chargingStation.stationInfo.maximumPower! / chargingStation.powerDivider
be9f397b 616 if (limit > connectorMaximumPower) {
fba11dc6 617 logger.error(
bbb55ee4 618 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
66a7748d
JB
619 result
620 )
621 limit = connectorMaximumPower
15068be9
JB
622 }
623 }
15068be9 624 }
66a7748d
JB
625 return limit
626}
fba11dc6
JB
627
628export const getDefaultVoltageOut = (
629 currentType: CurrentType,
630 logPrefix: string,
66a7748d 631 templateFile: string
fba11dc6 632): Voltage => {
66a7748d
JB
633 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
634 let defaultVoltageOut: number
fba11dc6
JB
635 switch (currentType) {
636 case CurrentType.AC:
66a7748d
JB
637 defaultVoltageOut = Voltage.VOLTAGE_230
638 break
fba11dc6 639 case CurrentType.DC:
66a7748d
JB
640 defaultVoltageOut = Voltage.VOLTAGE_400
641 break
fba11dc6 642 default:
66a7748d
JB
643 logger.error(`${logPrefix} ${errorMsg}`)
644 throw new BaseError(errorMsg)
15068be9 645 }
66a7748d
JB
646 return defaultVoltageOut
647}
fba11dc6
JB
648
649export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
66a7748d
JB
650 return stationInfo.idTagsFile != null
651 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
652 : undefined
653}
fba11dc6 654
b2b60626 655export const waitChargingStationEvents = async (
fba11dc6
JB
656 emitter: EventEmitter,
657 event: ChargingStationWorkerMessageEvents,
66a7748d 658 eventsToWait: number
fba11dc6 659): Promise<number> => {
66a7748d
JB
660 return await new Promise<number>((resolve) => {
661 let events = 0
fba11dc6 662 if (eventsToWait === 0) {
66a7748d
JB
663 resolve(events)
664 return
fba11dc6
JB
665 }
666 emitter.on(event, () => {
66a7748d 667 ++events
fba11dc6 668 if (events === eventsToWait) {
66a7748d 669 resolve(events)
b1f1b0f6 670 }
66a7748d
JB
671 })
672 })
673}
fba11dc6 674
cfc9875a 675const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
66a7748d
JB
676 let configuredMaxNumberOfConnectors = 0
677 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
678 const numberOfConnectors = stationTemplate.numberOfConnectors as number[]
679 configuredMaxNumberOfConnectors =
680 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
300418e9 681 } else if (stationTemplate.numberOfConnectors != null) {
66a7748d
JB
682 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number
683 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
cfc9875a 684 configuredMaxNumberOfConnectors =
66a7748d
JB
685 stationTemplate.Connectors?.[0] != null
686 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
687 : getMaxNumberOfConnectors(stationTemplate.Connectors)
688 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
fba11dc6
JB
689 for (const evse in stationTemplate.Evses) {
690 if (evse === '0') {
66a7748d 691 continue
cda5d0fb 692 }
cfc9875a 693 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
66a7748d
JB
694 stationTemplate.Evses[evse].Connectors
695 )
cda5d0fb 696 }
cda5d0fb 697 }
66a7748d
JB
698 return configuredMaxNumberOfConnectors
699}
fba11dc6
JB
700
701const checkConfiguredMaxConnectors = (
702 configuredMaxConnectors: number,
703 logPrefix: string,
66a7748d 704 templateFile: string
fba11dc6
JB
705): void => {
706 if (configuredMaxConnectors <= 0) {
707 logger.warn(
66a7748d
JB
708 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
709 )
cda5d0fb 710 }
66a7748d 711}
cda5d0fb 712
fba11dc6
JB
713const checkTemplateMaxConnectors = (
714 templateMaxConnectors: number,
715 logPrefix: string,
66a7748d 716 templateFile: string
fba11dc6
JB
717): void => {
718 if (templateMaxConnectors === 0) {
719 logger.warn(
66a7748d
JB
720 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
721 )
fba11dc6
JB
722 } else if (templateMaxConnectors < 0) {
723 logger.error(
66a7748d
JB
724 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
725 )
fba11dc6 726 }
66a7748d 727}
fba11dc6
JB
728
729const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
66a7748d
JB
730 connectorStatus.availability = AvailabilityType.Operative
731 connectorStatus.idTagLocalAuthorized = false
732 connectorStatus.idTagAuthorized = false
733 connectorStatus.transactionRemoteStarted = false
734 connectorStatus.transactionStarted = false
735 connectorStatus.energyActiveImportRegisterValue = 0
736 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
300418e9 737 if (connectorStatus.chargingProfiles == null) {
66a7748d 738 connectorStatus.chargingProfiles = []
fba11dc6 739 }
66a7748d 740}
fba11dc6
JB
741
742const warnDeprecatedTemplateKey = (
743 template: ChargingStationTemplate,
744 key: string,
745 logPrefix: string,
746 templateFile: string,
66a7748d 747 logMsgToAppend = ''
fba11dc6 748): void => {
300418e9 749 if (template?.[key as keyof ChargingStationTemplate] !== undefined) {
fba11dc6
JB
750 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
751 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
66a7748d
JB
752 }`
753 logger.warn(`${logPrefix} ${logMsg}`)
754 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
fba11dc6 755 }
66a7748d 756}
fba11dc6
JB
757
758const convertDeprecatedTemplateKey = (
759 template: ChargingStationTemplate,
760 deprecatedKey: string,
66a7748d 761 key?: string
fba11dc6 762): void => {
300418e9
JB
763 if (template?.[deprecatedKey as keyof ChargingStationTemplate] !== undefined) {
764 if (key !== undefined) {
765 (template as unknown as Record<string, unknown>)[key] =
66a7748d 766 template[deprecatedKey as keyof ChargingStationTemplate]
e1d9a0f4 767 }
66a7748d
JB
768 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
769 delete template[deprecatedKey as keyof ChargingStationTemplate]
fba11dc6 770 }
66a7748d 771}
fba11dc6 772
947f048a 773interface ChargingProfilesLimit {
66a7748d
JB
774 limit: number
775 chargingProfile: ChargingProfile
947f048a
JB
776}
777
fba11dc6 778/**
21ee4dc2 779 * Charging profiles shall already be sorted by connector id descending then stack level descending
fba11dc6 780 *
d467756c
JB
781 * @param chargingStation -
782 * @param connectorId -
fba11dc6
JB
783 * @param chargingProfiles -
784 * @param logPrefix -
947f048a 785 * @returns ChargingProfilesLimit
fba11dc6
JB
786 */
787const getLimitFromChargingProfiles = (
a71d4e70
JB
788 chargingStation: ChargingStation,
789 connectorId: number,
fba11dc6 790 chargingProfiles: ChargingProfile[],
66a7748d 791 logPrefix: string
947f048a 792): ChargingProfilesLimit | undefined => {
66a7748d
JB
793 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
794 const currentDate = new Date()
795 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
796 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
fba11dc6 797 for (const chargingProfile of chargingProfiles) {
66a7748d 798 const chargingSchedule = chargingProfile.chargingSchedule
be9f397b 799 if (chargingSchedule?.startSchedule == null && connectorStatus?.transactionStarted === true) {
109c677a 800 logger.debug(
66a7748d
JB
801 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
802 )
a71d4e70 803 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
66a7748d 804 chargingSchedule.startSchedule = connectorStatus?.transactionStart
52952bf8 805 }
be9f397b 806 if (chargingSchedule?.startSchedule != null && !isDate(chargingSchedule?.startSchedule)) {
ef9e3b33 807 logger.warn(
66a7748d
JB
808 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
809 )
810 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
811 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!
ef9e3b33 812 }
be9f397b 813 if (chargingSchedule?.startSchedule != null && chargingSchedule?.duration == null) {
da332e70 814 logger.debug(
66a7748d
JB
815 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
816 )
da332e70 817 // OCPP specifies that if duration is not defined, it should be infinite
be9f397b 818 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
da332e70 819 }
0eb666db 820 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
66a7748d 821 continue
ec4a242a 822 }
0bd926c1 823 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
66a7748d 824 continue
142a66c9 825 }
fba11dc6
JB
826 // Check if the charging profile is active
827 if (
975e18ec 828 isWithinInterval(currentDate, {
66a7748d 829 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
975e18ec 830 start: chargingSchedule.startSchedule!,
66a7748d
JB
831 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
832 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!)
975e18ec 833 })
fba11dc6 834 ) {
252a7d22 835 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
836 const chargingSchedulePeriodCompareFn = (
837 a: ChargingSchedulePeriod,
66a7748d
JB
838 b: ChargingSchedulePeriod
839 ): number => a.startPeriod - b.startPeriod
80c58041 840 if (
6fc0c6f3 841 !isArraySorted<ChargingSchedulePeriod>(
80c58041 842 chargingSchedule.chargingSchedulePeriod,
66a7748d 843 chargingSchedulePeriodCompareFn
6fc0c6f3 844 )
80c58041
JB
845 ) {
846 logger.warn(
66a7748d
JB
847 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
848 )
849 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
80c58041 850 }
da332e70 851 // Check if the first schedule period startPeriod property is equal to 0
55f2ab60
JB
852 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
853 logger.error(
66a7748d
JB
854 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
855 )
856 continue
55f2ab60 857 }
991fb26b 858 // Handle only one schedule period
975e18ec 859 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
860 const result: ChargingProfilesLimit = {
861 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
66a7748d
JB
862 chargingProfile
863 }
864 logger.debug(debugLogMsg, result)
865 return result
41189456 866 }
66a7748d 867 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
252a7d22 868 // Search for the right schedule period
e3037969
JB
869 for (const [
870 index,
66a7748d 871 chargingSchedulePeriod
e3037969 872 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
873 // Find the right schedule period
874 if (
875 isAfter(
66a7748d 876 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e3037969 877 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
66a7748d 878 currentDate
252a7d22
JB
879 )
880 ) {
e3037969 881 // Found the schedule period: previous is the correct one
252a7d22 882 const result: ChargingProfilesLimit = {
66a7748d 883 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e3037969 884 limit: previousChargingSchedulePeriod!.limit,
66a7748d
JB
885 chargingProfile
886 }
887 logger.debug(debugLogMsg, result)
888 return result
252a7d22 889 }
e3037969 890 // Keep a reference to previous one
66a7748d 891 previousChargingSchedulePeriod = chargingSchedulePeriod
975e18ec 892 // Handle the last schedule period within the charging profile duration
252a7d22 893 if (
975e18ec
JB
894 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
895 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
ccfa30bc
JB
896 differenceInSeconds(
897 addSeconds(
66a7748d 898 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
d9dc6292 899 chargingSchedule.startSchedule!,
66a7748d 900 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
ccfa30bc 901 ),
66a7748d
JB
902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
903 chargingSchedule.startSchedule!
904 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ccfa30bc 905 ) > chargingSchedule.duration!)
252a7d22
JB
906 ) {
907 const result: ChargingProfilesLimit = {
e3037969 908 limit: previousChargingSchedulePeriod.limit,
66a7748d
JB
909 chargingProfile
910 }
911 logger.debug(debugLogMsg, result)
912 return result
252a7d22 913 }
17ac262c
JB
914 }
915 }
916 }
17ac262c 917 }
66a7748d 918}
17ac262c 919
0eb666db
JB
920export const prepareChargingProfileKind = (
921 connectorStatus: ConnectorStatus,
922 chargingProfile: ChargingProfile,
923 currentDate: Date,
66a7748d 924 logPrefix: string
0eb666db
JB
925): boolean => {
926 switch (chargingProfile.chargingProfileKind) {
927 case ChargingProfileKindType.RECURRING:
928 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
66a7748d 929 return false
0eb666db 930 }
66a7748d
JB
931 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
932 break
0eb666db 933 case ChargingProfileKindType.RELATIVE:
be9f397b 934 if (chargingProfile.chargingSchedule.startSchedule != null) {
ccfa30bc 935 logger.warn(
66a7748d
JB
936 `${logPrefix} ${moduleName}.prepareChargingProfileKind: Relative charging profile id ${chargingProfile.chargingProfileId} has a startSchedule property defined. It will be ignored or used if the connector has a transaction started`
937 )
938 delete chargingProfile.chargingSchedule.startSchedule
ccfa30bc 939 }
66a7748d
JB
940 if (connectorStatus?.transactionStarted === true) {
941 chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart
ef9e3b33
JB
942 }
943 // FIXME: Handle relative charging profile duration
66a7748d 944 break
0eb666db 945 }
66a7748d
JB
946 return true
947}
0eb666db 948
ad490d5f 949export const canProceedChargingProfile = (
0bd926c1
JB
950 chargingProfile: ChargingProfile,
951 currentDate: Date,
66a7748d 952 logPrefix: string
0bd926c1
JB
953): boolean => {
954 if (
66a7748d 955 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
0bd926c1 956 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
66a7748d 957 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
0bd926c1
JB
958 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
959 ) {
960 logger.debug(
961 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
962 chargingProfile.chargingProfileId
66a7748d
JB
963 } is not valid for the current date ${currentDate.toISOString()}`
964 )
965 return false
0bd926c1 966 }
ef9e3b33 967 if (
be9f397b
JB
968 chargingProfile.chargingSchedule.startSchedule == null ||
969 chargingProfile.chargingSchedule.duration == null
ef9e3b33 970 ) {
0bd926c1 971 logger.error(
66a7748d
JB
972 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
973 )
974 return false
ef9e3b33
JB
975 }
976 if (
be9f397b 977 chargingProfile.chargingSchedule.startSchedule != null &&
ef9e3b33
JB
978 !isValidTime(chargingProfile.chargingSchedule.startSchedule)
979 ) {
980 logger.error(
66a7748d
JB
981 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
982 )
983 return false
ef9e3b33
JB
984 }
985 if (
be9f397b 986 chargingProfile.chargingSchedule.duration != null &&
ef9e3b33
JB
987 !Number.isSafeInteger(chargingProfile.chargingSchedule.duration)
988 ) {
989 logger.error(
66a7748d
JB
990 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
991 )
992 return false
0bd926c1 993 }
66a7748d
JB
994 return true
995}
0bd926c1 996
0eb666db 997const canProceedRecurringChargingProfile = (
0bd926c1 998 chargingProfile: ChargingProfile,
66a7748d 999 logPrefix: string
0bd926c1
JB
1000): boolean => {
1001 if (
1002 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1003 chargingProfile.recurrencyKind == null
0bd926c1
JB
1004 ) {
1005 logger.error(
66a7748d
JB
1006 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1007 )
1008 return false
0bd926c1 1009 }
d929adcc
JB
1010 if (
1011 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1012 chargingProfile.chargingSchedule.startSchedule == null
d929adcc 1013 ) {
ef9e3b33 1014 logger.error(
66a7748d
JB
1015 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1016 )
1017 return false
ef9e3b33 1018 }
66a7748d
JB
1019 return true
1020}
0bd926c1 1021
522e4b05 1022/**
ec4a242a 1023 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
1024 *
1025 * @param chargingProfile -
1026 * @param currentDate -
1027 * @param logPrefix -
1028 */
0eb666db 1029const prepareRecurringChargingProfile = (
76dab5a9
JB
1030 chargingProfile: ChargingProfile,
1031 currentDate: Date,
66a7748d 1032 logPrefix: string
ec4a242a 1033): boolean => {
66a7748d
JB
1034 const chargingSchedule = chargingProfile.chargingSchedule
1035 let recurringIntervalTranslated = false
1036 let recurringInterval: Interval
76dab5a9
JB
1037 switch (chargingProfile.recurrencyKind) {
1038 case RecurrencyKindType.DAILY:
522e4b05 1039 recurringInterval = {
66a7748d 1040 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1041 start: chargingSchedule.startSchedule!,
66a7748d
JB
1042 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1043 end: addDays(chargingSchedule.startSchedule!, 1)
1044 }
1045 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1046 if (
1047 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1048 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1049 ) {
1050 chargingSchedule.startSchedule = addDays(
991fb26b 1051 recurringInterval.start,
66a7748d
JB
1052 differenceInDays(currentDate, recurringInterval.start)
1053 )
522e4b05
JB
1054 recurringInterval = {
1055 start: chargingSchedule.startSchedule,
66a7748d
JB
1056 end: addDays(chargingSchedule.startSchedule, 1)
1057 }
1058 recurringIntervalTranslated = true
76dab5a9 1059 }
66a7748d 1060 break
76dab5a9 1061 case RecurrencyKindType.WEEKLY:
522e4b05 1062 recurringInterval = {
66a7748d 1063 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1064 start: chargingSchedule.startSchedule!,
66a7748d
JB
1065 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1066 end: addWeeks(chargingSchedule.startSchedule!, 1)
1067 }
1068 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1069 if (
1070 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1071 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1072 ) {
1073 chargingSchedule.startSchedule = addWeeks(
991fb26b 1074 recurringInterval.start,
66a7748d
JB
1075 differenceInWeeks(currentDate, recurringInterval.start)
1076 )
522e4b05
JB
1077 recurringInterval = {
1078 start: chargingSchedule.startSchedule,
66a7748d
JB
1079 end: addWeeks(chargingSchedule.startSchedule, 1)
1080 }
1081 recurringIntervalTranslated = true
76dab5a9 1082 }
66a7748d 1083 break
ec4a242a
JB
1084 default:
1085 logger.error(
66a7748d
JB
1086 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1087 )
76dab5a9 1088 }
66a7748d 1089 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ec4a242a 1090 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 1091 logger.error(
aa5c5ad4 1092 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 1093 chargingProfile.recurrencyKind
991fb26b 1094 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
66a7748d
JB
1095 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1096 recurringInterval!.start
991fb26b 1097 ).toISOString()}, ${toDate(
66a7748d
JB
1098 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1099 recurringInterval!.end
1100 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `
1101 )
522e4b05 1102 }
66a7748d
JB
1103 return recurringIntervalTranslated
1104}
76dab5a9 1105
d476bc1b
JB
1106const checkRecurringChargingProfileDuration = (
1107 chargingProfile: ChargingProfile,
1108 interval: Interval,
66a7748d 1109 logPrefix: string
ec4a242a 1110): void => {
be9f397b 1111 if (chargingProfile.chargingSchedule.duration == null) {
142a66c9
JB
1112 logger.warn(
1113 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1114 chargingProfile.chargingProfileKind
1115 } charging profile id ${
1116 chargingProfile.chargingProfileId
1117 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1118 interval.end,
66a7748d
JB
1119 interval.start
1120 )}`
1121 )
1122 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
142a66c9 1123 } else if (
be9f397b 1124 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
d476bc1b
JB
1125 ) {
1126 logger.warn(
aa5c5ad4 1127 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1128 chargingProfile.chargingProfileKind
1129 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1130 chargingProfile.chargingSchedule.duration
710d50eb 1131 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b 1132 interval.end,
66a7748d
JB
1133 interval.start
1134 )}`
1135 )
1136 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
d476bc1b 1137 }
66a7748d 1138}
d476bc1b 1139
fba11dc6 1140const getRandomSerialNumberSuffix = (params?: {
66a7748d
JB
1141 randomBytesLength?: number
1142 upperCase?: boolean
fba11dc6 1143}): string => {
66a7748d
JB
1144 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1145 if (params?.upperCase === true) {
1146 return randomSerialNumberSuffix.toUpperCase()
17ac262c 1147 }
66a7748d
JB
1148 return randomSerialNumberSuffix
1149}