Merge branch 'main' into issue39-ocpp2
[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
6fc0c6f3 631/**
7abb61bb 632 * Gets the connector charging profiles relevant for power limitation shallow cloned and sorted by priorities
6fc0c6f3 633 *
7abb61bb
JB
634 * @param chargingStation - Charging station
635 * @param connectorId - Connector id
6fc0c6f3
JB
636 * @returns connector charging profiles array
637 */
638export const getConnectorChargingProfiles = (
639 chargingStation: ChargingStation,
66a7748d
JB
640 connectorId: number
641): ChargingProfile[] => {
7abb61bb 642 // FIXME: handle charging profile purpose CHARGE_POINT_MAX_PROFILE
a629e6fc
JB
643 return (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
644 .slice()
7abb61bb
JB
645 .sort((a, b) => {
646 if (
647 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE &&
648 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
649 ) {
650 return -1
651 } else if (
652 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE &&
653 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE
654 ) {
655 return 1
656 }
657 return b.stackLevel - a.stackLevel
658 })
a629e6fc
JB
659 .concat(
660 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [])
7abb61bb
JB
661 .filter(
662 chargingProfile =>
663 chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
664 )
a629e6fc
JB
665 .sort((a, b) => b.stackLevel - a.stackLevel)
666 )
66a7748d 667}
6fc0c6f3 668
fba11dc6
JB
669export const getChargingStationConnectorChargingProfilesPowerLimit = (
670 chargingStation: ChargingStation,
66a7748d 671 connectorId: number
fba11dc6 672): number | undefined => {
66a7748d 673 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
fba11dc6 674 if (isNotEmptyArray(chargingProfiles)) {
c76d9c83 675 const chargingProfilesLimit = getLimitFromChargingProfiles(
a71d4e70
JB
676 chargingStation,
677 connectorId,
678 chargingProfiles,
66a7748d
JB
679 chargingStation.logPrefix()
680 )
c76d9c83
JB
681 if (chargingProfilesLimit != null) {
682 let { limit, chargingProfile } = chargingProfilesLimit
5398cecf 683 switch (chargingStation.stationInfo?.currentOutType) {
fba11dc6
JB
684 case CurrentType.AC:
685 limit =
5199f9fd 686 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6
JB
687 ? limit
688 : ACElectricUtils.powerTotal(
66a7748d
JB
689 chargingStation.getNumberOfPhases(),
690 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
691 chargingStation.stationInfo.voltageOut!,
be9f397b 692 limit
66a7748d
JB
693 )
694 break
fba11dc6
JB
695 case CurrentType.DC:
696 limit =
5199f9fd 697 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6 698 ? limit
66a7748d 699 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
be9f397b 700 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
fba11dc6
JB
701 }
702 const connectorMaximumPower =
66a7748d 703 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 704 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
be9f397b 705 if (limit > connectorMaximumPower) {
fba11dc6 706 logger.error(
5199f9fd
JB
707 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
708 chargingProfile.chargingProfileId
709 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
c76d9c83 710 chargingProfilesLimit
66a7748d
JB
711 )
712 limit = connectorMaximumPower
15068be9 713 }
c76d9c83 714 return limit
15068be9 715 }
15068be9 716 }
66a7748d 717}
fba11dc6
JB
718
719export const getDefaultVoltageOut = (
720 currentType: CurrentType,
721 logPrefix: string,
66a7748d 722 templateFile: string
fba11dc6 723): Voltage => {
66a7748d
JB
724 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
725 let defaultVoltageOut: number
fba11dc6
JB
726 switch (currentType) {
727 case CurrentType.AC:
66a7748d
JB
728 defaultVoltageOut = Voltage.VOLTAGE_230
729 break
fba11dc6 730 case CurrentType.DC:
66a7748d
JB
731 defaultVoltageOut = Voltage.VOLTAGE_400
732 break
fba11dc6 733 default:
66a7748d
JB
734 logger.error(`${logPrefix} ${errorMsg}`)
735 throw new BaseError(errorMsg)
15068be9 736 }
66a7748d
JB
737 return defaultVoltageOut
738}
fba11dc6
JB
739
740export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
66a7748d
JB
741 return stationInfo.idTagsFile != null
742 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
743 : undefined
744}
fba11dc6 745
b2b60626 746export const waitChargingStationEvents = async (
fba11dc6
JB
747 emitter: EventEmitter,
748 event: ChargingStationWorkerMessageEvents,
66a7748d 749 eventsToWait: number
fba11dc6 750): Promise<number> => {
a974c8e4 751 return await new Promise<number>(resolve => {
66a7748d 752 let events = 0
fba11dc6 753 if (eventsToWait === 0) {
66a7748d
JB
754 resolve(events)
755 return
fba11dc6
JB
756 }
757 emitter.on(event, () => {
66a7748d 758 ++events
fba11dc6 759 if (events === eventsToWait) {
66a7748d 760 resolve(events)
b1f1b0f6 761 }
66a7748d
JB
762 })
763 })
764}
fba11dc6 765
cfc9875a 766const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
66a7748d
JB
767 let configuredMaxNumberOfConnectors = 0
768 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
5dc7c990 769 const numberOfConnectors = stationTemplate.numberOfConnectors
66a7748d
JB
770 configuredMaxNumberOfConnectors =
771 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
300418e9 772 } else if (stationTemplate.numberOfConnectors != null) {
5dc7c990 773 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
66a7748d 774 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
cfc9875a 775 configuredMaxNumberOfConnectors =
5199f9fd
JB
776 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
777 stationTemplate.Connectors[0] != null
66a7748d
JB
778 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
779 : getMaxNumberOfConnectors(stationTemplate.Connectors)
780 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
fba11dc6
JB
781 for (const evse in stationTemplate.Evses) {
782 if (evse === '0') {
66a7748d 783 continue
cda5d0fb 784 }
cfc9875a 785 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
66a7748d
JB
786 stationTemplate.Evses[evse].Connectors
787 )
cda5d0fb 788 }
cda5d0fb 789 }
66a7748d
JB
790 return configuredMaxNumberOfConnectors
791}
fba11dc6
JB
792
793const checkConfiguredMaxConnectors = (
794 configuredMaxConnectors: number,
795 logPrefix: string,
66a7748d 796 templateFile: string
fba11dc6
JB
797): void => {
798 if (configuredMaxConnectors <= 0) {
799 logger.warn(
66a7748d
JB
800 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
801 )
cda5d0fb 802 }
66a7748d 803}
cda5d0fb 804
fba11dc6
JB
805const checkTemplateMaxConnectors = (
806 templateMaxConnectors: number,
807 logPrefix: string,
66a7748d 808 templateFile: string
fba11dc6
JB
809): void => {
810 if (templateMaxConnectors === 0) {
811 logger.warn(
66a7748d
JB
812 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
813 )
fba11dc6
JB
814 } else if (templateMaxConnectors < 0) {
815 logger.error(
66a7748d
JB
816 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
817 )
fba11dc6 818 }
66a7748d 819}
fba11dc6
JB
820
821const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
66a7748d
JB
822 connectorStatus.availability = AvailabilityType.Operative
823 connectorStatus.idTagLocalAuthorized = false
824 connectorStatus.idTagAuthorized = false
825 connectorStatus.transactionRemoteStarted = false
826 connectorStatus.transactionStarted = false
827 connectorStatus.energyActiveImportRegisterValue = 0
828 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
300418e9 829 if (connectorStatus.chargingProfiles == null) {
66a7748d 830 connectorStatus.chargingProfiles = []
fba11dc6 831 }
66a7748d 832}
fba11dc6
JB
833
834const warnDeprecatedTemplateKey = (
835 template: ChargingStationTemplate,
836 key: string,
837 logPrefix: string,
838 templateFile: string,
66a7748d 839 logMsgToAppend = ''
fba11dc6 840): void => {
d760a0a6 841 if (template[key as keyof ChargingStationTemplate] != null) {
fba11dc6
JB
842 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
843 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
66a7748d
JB
844 }`
845 logger.warn(`${logPrefix} ${logMsg}`)
846 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
fba11dc6 847 }
66a7748d 848}
fba11dc6
JB
849
850const convertDeprecatedTemplateKey = (
851 template: ChargingStationTemplate,
852 deprecatedKey: string,
66a7748d 853 key?: string
fba11dc6 854): void => {
d760a0a6
JB
855 if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
856 if (key != null) {
300418e9 857 (template as unknown as Record<string, unknown>)[key] =
66a7748d 858 template[deprecatedKey as keyof ChargingStationTemplate]
e1d9a0f4 859 }
66a7748d
JB
860 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
861 delete template[deprecatedKey as keyof ChargingStationTemplate]
fba11dc6 862 }
66a7748d 863}
fba11dc6 864
947f048a 865interface ChargingProfilesLimit {
66a7748d
JB
866 limit: number
867 chargingProfile: ChargingProfile
947f048a
JB
868}
869
fba11dc6 870/**
21ee4dc2 871 * Charging profiles shall already be sorted by connector id descending then stack level descending
fba11dc6 872 *
d467756c
JB
873 * @param chargingStation -
874 * @param connectorId -
fba11dc6
JB
875 * @param chargingProfiles -
876 * @param logPrefix -
947f048a 877 * @returns ChargingProfilesLimit
fba11dc6
JB
878 */
879const getLimitFromChargingProfiles = (
a71d4e70
JB
880 chargingStation: ChargingStation,
881 connectorId: number,
fba11dc6 882 chargingProfiles: ChargingProfile[],
66a7748d 883 logPrefix: string
947f048a 884): ChargingProfilesLimit | undefined => {
c76d9c83 885 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profiles limit found: %j`
66a7748d 886 const currentDate = new Date()
f938317f 887 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
7abb61bb 888 let previousActiveChargingProfile: ChargingProfile | undefined
fba11dc6 889 for (const chargingProfile of chargingProfiles) {
66a7748d 890 const chargingSchedule = chargingProfile.chargingSchedule
2466918c 891 if (chargingSchedule.startSchedule == null) {
109c677a 892 logger.debug(
66a7748d
JB
893 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
894 )
a71d4e70 895 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
f938317f 896 chargingSchedule.startSchedule = connectorStatus?.transactionStart
52952bf8 897 }
2466918c 898 if (!isDate(chargingSchedule.startSchedule)) {
ef9e3b33 899 logger.warn(
66a7748d
JB
900 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
901 )
902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 903 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
ef9e3b33 904 }
2466918c 905 if (chargingSchedule.duration == null) {
da332e70 906 logger.debug(
66a7748d
JB
907 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
908 )
da332e70 909 // OCPP specifies that if duration is not defined, it should be infinite
be9f397b 910 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
da332e70 911 }
0eb666db 912 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
66a7748d 913 continue
ec4a242a 914 }
0bd926c1 915 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
66a7748d 916 continue
142a66c9 917 }
fba11dc6
JB
918 // Check if the charging profile is active
919 if (
975e18ec 920 isWithinInterval(currentDate, {
2466918c
JB
921 start: chargingSchedule.startSchedule,
922 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
975e18ec 923 })
fba11dc6 924 ) {
252a7d22 925 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
926 const chargingSchedulePeriodCompareFn = (
927 a: ChargingSchedulePeriod,
66a7748d
JB
928 b: ChargingSchedulePeriod
929 ): number => a.startPeriod - b.startPeriod
80c58041 930 if (
6fc0c6f3 931 !isArraySorted<ChargingSchedulePeriod>(
80c58041 932 chargingSchedule.chargingSchedulePeriod,
66a7748d 933 chargingSchedulePeriodCompareFn
6fc0c6f3 934 )
80c58041
JB
935 ) {
936 logger.warn(
66a7748d
JB
937 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
938 )
939 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
80c58041 940 }
da332e70 941 // Check if the first schedule period startPeriod property is equal to 0
55f2ab60
JB
942 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
943 logger.error(
66a7748d
JB
944 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
945 )
946 continue
55f2ab60 947 }
991fb26b 948 // Handle only one schedule period
975e18ec 949 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
c76d9c83 950 const chargingProfilesLimit: ChargingProfilesLimit = {
252a7d22 951 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
66a7748d
JB
952 chargingProfile
953 }
c76d9c83
JB
954 logger.debug(debugLogMsg, chargingProfilesLimit)
955 return chargingProfilesLimit
41189456 956 }
66a7748d 957 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
252a7d22 958 // Search for the right schedule period
e3037969
JB
959 for (const [
960 index,
66a7748d 961 chargingSchedulePeriod
e3037969 962 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
963 // Find the right schedule period
964 if (
965 isAfter(
2466918c 966 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
66a7748d 967 currentDate
252a7d22
JB
968 )
969 ) {
e3037969 970 // Found the schedule period: previous is the correct one
c76d9c83 971 const chargingProfilesLimit: ChargingProfilesLimit = {
7abb61bb
JB
972 limit: previousChargingSchedulePeriod?.limit ?? chargingSchedulePeriod.limit,
973 chargingProfile: previousActiveChargingProfile ?? chargingProfile
66a7748d 974 }
c76d9c83
JB
975 logger.debug(debugLogMsg, chargingProfilesLimit)
976 return chargingProfilesLimit
252a7d22 977 }
975e18ec 978 // Handle the last schedule period within the charging profile duration
252a7d22 979 if (
975e18ec
JB
980 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
981 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
ccfa30bc
JB
982 differenceInSeconds(
983 addSeconds(
2466918c 984 chargingSchedule.startSchedule,
66a7748d 985 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
ccfa30bc 986 ),
2466918c
JB
987 chargingSchedule.startSchedule
988 ) > chargingSchedule.duration)
252a7d22 989 ) {
c76d9c83 990 const chargingProfilesLimit: ChargingProfilesLimit = {
7abb61bb 991 limit: chargingSchedulePeriod.limit,
66a7748d
JB
992 chargingProfile
993 }
c76d9c83
JB
994 logger.debug(debugLogMsg, chargingProfilesLimit)
995 return chargingProfilesLimit
252a7d22 996 }
7abb61bb
JB
997 // Keep a reference to previous charging schedule period
998 previousChargingSchedulePeriod = chargingSchedulePeriod
17ac262c
JB
999 }
1000 }
7abb61bb
JB
1001 // Keep a reference to previous active charging profile
1002 previousActiveChargingProfile = chargingProfile
17ac262c 1003 }
17ac262c 1004 }
66a7748d 1005}
17ac262c 1006
0eb666db 1007export const prepareChargingProfileKind = (
f938317f 1008 connectorStatus: ConnectorStatus | undefined,
0eb666db 1009 chargingProfile: ChargingProfile,
6dde6c5f 1010 currentDate: string | number | Date,
66a7748d 1011 logPrefix: string
0eb666db
JB
1012): boolean => {
1013 switch (chargingProfile.chargingProfileKind) {
1014 case ChargingProfileKindType.RECURRING:
1015 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
66a7748d 1016 return false
0eb666db 1017 }
66a7748d
JB
1018 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
1019 break
0eb666db 1020 case ChargingProfileKindType.RELATIVE:
be9f397b 1021 if (chargingProfile.chargingSchedule.startSchedule != null) {
ccfa30bc 1022 logger.warn(
66a7748d
JB
1023 `${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`
1024 )
1025 delete chargingProfile.chargingSchedule.startSchedule
ccfa30bc 1026 }
f938317f 1027 if (connectorStatus?.transactionStarted === true) {
5199f9fd 1028 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
ef9e3b33 1029 }
24dc52e9 1030 // FIXME: handle relative charging profile duration
66a7748d 1031 break
0eb666db 1032 }
66a7748d
JB
1033 return true
1034}
0eb666db 1035
ad490d5f 1036export const canProceedChargingProfile = (
0bd926c1 1037 chargingProfile: ChargingProfile,
6dde6c5f 1038 currentDate: string | number | Date,
66a7748d 1039 logPrefix: string
0bd926c1
JB
1040): boolean => {
1041 if (
5dc7c990
JB
1042 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
1043 (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
0bd926c1
JB
1044 ) {
1045 logger.debug(
1046 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1047 chargingProfile.chargingProfileId
6dde6c5f 1048 } is not valid for the current date ${
5dc7c990 1049 isDate(currentDate) ? currentDate.toISOString() : currentDate
6dde6c5f 1050 }`
66a7748d
JB
1051 )
1052 return false
0bd926c1 1053 }
ef9e3b33 1054 if (
be9f397b
JB
1055 chargingProfile.chargingSchedule.startSchedule == null ||
1056 chargingProfile.chargingSchedule.duration == null
ef9e3b33 1057 ) {
0bd926c1 1058 logger.error(
66a7748d
JB
1059 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1060 )
1061 return false
ef9e3b33 1062 }
5dc7c990 1063 if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
ef9e3b33 1064 logger.error(
66a7748d
JB
1065 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1066 )
1067 return false
ef9e3b33 1068 }
5199f9fd 1069 if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
ef9e3b33 1070 logger.error(
66a7748d
JB
1071 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1072 )
1073 return false
0bd926c1 1074 }
66a7748d
JB
1075 return true
1076}
0bd926c1 1077
0eb666db 1078const canProceedRecurringChargingProfile = (
0bd926c1 1079 chargingProfile: ChargingProfile,
66a7748d 1080 logPrefix: string
0bd926c1
JB
1081): boolean => {
1082 if (
1083 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1084 chargingProfile.recurrencyKind == null
0bd926c1
JB
1085 ) {
1086 logger.error(
66a7748d
JB
1087 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1088 )
1089 return false
0bd926c1 1090 }
d929adcc
JB
1091 if (
1092 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1093 chargingProfile.chargingSchedule.startSchedule == null
d929adcc 1094 ) {
ef9e3b33 1095 logger.error(
66a7748d
JB
1096 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1097 )
1098 return false
ef9e3b33 1099 }
66a7748d
JB
1100 return true
1101}
0bd926c1 1102
522e4b05 1103/**
ec4a242a 1104 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
1105 *
1106 * @param chargingProfile -
1107 * @param currentDate -
1108 * @param logPrefix -
1109 */
0eb666db 1110const prepareRecurringChargingProfile = (
76dab5a9 1111 chargingProfile: ChargingProfile,
6dde6c5f 1112 currentDate: string | number | Date,
66a7748d 1113 logPrefix: string
ec4a242a 1114): boolean => {
66a7748d
JB
1115 const chargingSchedule = chargingProfile.chargingSchedule
1116 let recurringIntervalTranslated = false
5dc7c990 1117 let recurringInterval: Interval | undefined
76dab5a9
JB
1118 switch (chargingProfile.recurrencyKind) {
1119 case RecurrencyKindType.DAILY:
522e4b05 1120 recurringInterval = {
66a7748d 1121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1122 start: chargingSchedule.startSchedule!,
66a7748d
JB
1123 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1124 end: addDays(chargingSchedule.startSchedule!, 1)
1125 }
1126 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1127 if (
1128 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1129 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1130 ) {
1131 chargingSchedule.startSchedule = addDays(
991fb26b 1132 recurringInterval.start,
66a7748d
JB
1133 differenceInDays(currentDate, recurringInterval.start)
1134 )
522e4b05
JB
1135 recurringInterval = {
1136 start: chargingSchedule.startSchedule,
66a7748d
JB
1137 end: addDays(chargingSchedule.startSchedule, 1)
1138 }
1139 recurringIntervalTranslated = true
76dab5a9 1140 }
66a7748d 1141 break
76dab5a9 1142 case RecurrencyKindType.WEEKLY:
522e4b05 1143 recurringInterval = {
66a7748d 1144 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1145 start: chargingSchedule.startSchedule!,
66a7748d
JB
1146 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1147 end: addWeeks(chargingSchedule.startSchedule!, 1)
1148 }
1149 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1150 if (
1151 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1152 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1153 ) {
1154 chargingSchedule.startSchedule = addWeeks(
991fb26b 1155 recurringInterval.start,
66a7748d
JB
1156 differenceInWeeks(currentDate, recurringInterval.start)
1157 )
522e4b05
JB
1158 recurringInterval = {
1159 start: chargingSchedule.startSchedule,
66a7748d
JB
1160 end: addWeeks(chargingSchedule.startSchedule, 1)
1161 }
1162 recurringIntervalTranslated = true
76dab5a9 1163 }
66a7748d 1164 break
ec4a242a
JB
1165 default:
1166 logger.error(
66a7748d
JB
1167 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1168 )
76dab5a9 1169 }
66a7748d 1170 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ec4a242a 1171 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 1172 logger.error(
aa5c5ad4 1173 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 1174 chargingProfile.recurrencyKind
991fb26b 1175 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
5dc7c990 1176 recurringInterval?.start as Date
991fb26b 1177 ).toISOString()}, ${toDate(
5dc7c990 1178 recurringInterval?.end as Date
6dde6c5f 1179 ).toISOString()}] has not been properly translated to current date ${
5dc7c990 1180 isDate(currentDate) ? currentDate.toISOString() : currentDate
6dde6c5f 1181 } `
66a7748d 1182 )
522e4b05 1183 }
66a7748d
JB
1184 return recurringIntervalTranslated
1185}
76dab5a9 1186
d476bc1b
JB
1187const checkRecurringChargingProfileDuration = (
1188 chargingProfile: ChargingProfile,
1189 interval: Interval,
66a7748d 1190 logPrefix: string
ec4a242a 1191): void => {
be9f397b 1192 if (chargingProfile.chargingSchedule.duration == null) {
142a66c9
JB
1193 logger.warn(
1194 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1195 chargingProfile.chargingProfileKind
1196 } charging profile id ${
1197 chargingProfile.chargingProfileId
1198 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1199 interval.end,
66a7748d
JB
1200 interval.start
1201 )}`
1202 )
1203 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
142a66c9 1204 } else if (
be9f397b 1205 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
d476bc1b
JB
1206 ) {
1207 logger.warn(
aa5c5ad4 1208 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1209 chargingProfile.chargingProfileKind
1210 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1211 chargingProfile.chargingSchedule.duration
710d50eb 1212 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b 1213 interval.end,
66a7748d
JB
1214 interval.start
1215 )}`
1216 )
1217 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
d476bc1b 1218 }
66a7748d 1219}
d476bc1b 1220
fba11dc6 1221const getRandomSerialNumberSuffix = (params?: {
66a7748d
JB
1222 randomBytesLength?: number
1223 upperCase?: boolean
fba11dc6 1224}): string => {
66a7748d
JB
1225 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1226 if (params?.upperCase === true) {
1227 return randomSerialNumberSuffix.toUpperCase()
17ac262c 1228 }
66a7748d
JB
1229 return randomSerialNumberSuffix
1230}