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