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