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