fix: ensure no charging profile purpose TxProfile is loaded at startup
[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 =>
65099c1e 423 chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE ||
626d3ce5
JB
424 (chargingProfile.transactionId != null &&
425 connectorStatus.transactionId != null &&
65099c1e 426 chargingProfile.transactionId !== connectorStatus.transactionId)
626d3ce5
JB
427 )
428 }
239194e9 429 resetAuthorizeConnectorStatus(connectorStatus)
66a7748d
JB
430 connectorStatus.transactionRemoteStarted = false
431 connectorStatus.transactionStarted = false
5199f9fd
JB
432 delete connectorStatus.transactionStart
433 delete connectorStatus.transactionId
5199f9fd 434 delete connectorStatus.transactionIdTag
66a7748d 435 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
5199f9fd 436 delete connectorStatus.transactionBeginMeterValue
66a7748d 437}
fba11dc6 438
3edfdf53 439export const prepareConnectorStatus = (connectorStatus: ConnectorStatus): ConnectorStatus => {
1fa9df8c
JB
440 if (connectorStatus.reservation != null) {
441 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
442 connectorStatus.reservation.expiryDate = convertToDate(connectorStatus.reservation.expiryDate)!
443 }
444 if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
3edfdf53
JB
445 connectorStatus.chargingProfiles = connectorStatus.chargingProfiles
446 .filter(
447 chargingProfile =>
448 chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE
1fa9df8c 449 )
3edfdf53
JB
450 .map(chargingProfile => {
451 chargingProfile.chargingSchedule.startSchedule = convertToDate(
452 chargingProfile.chargingSchedule.startSchedule
453 )
454 chargingProfile.validFrom = convertToDate(chargingProfile.validFrom)
455 chargingProfile.validTo = convertToDate(chargingProfile.validTo)
456 return chargingProfile
457 })
1fa9df8c
JB
458 }
459 return connectorStatus
460}
461
fba11dc6
JB
462export const createBootNotificationRequest = (
463 stationInfo: ChargingStationInfo,
66a7748d 464 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
2466918c
JB
465): BootNotificationRequest | undefined => {
466 const ocppVersion = stationInfo.ocppVersion
fba11dc6
JB
467 switch (ocppVersion) {
468 case OCPPVersion.VERSION_16:
469 return {
470 chargePointModel: stationInfo.chargePointModel,
471 chargePointVendor: stationInfo.chargePointVendor,
d760a0a6 472 ...(stationInfo.chargeBoxSerialNumber != null && {
66a7748d 473 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
fba11dc6 474 }),
d760a0a6 475 ...(stationInfo.chargePointSerialNumber != null && {
66a7748d 476 chargePointSerialNumber: stationInfo.chargePointSerialNumber
fba11dc6 477 }),
d760a0a6 478 ...(stationInfo.firmwareVersion != null && {
66a7748d 479 firmwareVersion: stationInfo.firmwareVersion
fba11dc6 480 }),
d760a0a6
JB
481 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
482 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
483 ...(stationInfo.meterSerialNumber != null && {
66a7748d 484 meterSerialNumber: stationInfo.meterSerialNumber
fba11dc6 485 }),
d760a0a6 486 ...(stationInfo.meterType != null && {
66a7748d
JB
487 meterType: stationInfo.meterType
488 })
489 } satisfies OCPP16BootNotificationRequest
fba11dc6
JB
490 case OCPPVersion.VERSION_20:
491 case OCPPVersion.VERSION_201:
492 return {
493 reason: bootReason,
494 chargingStation: {
495 model: stationInfo.chargePointModel,
496 vendorName: stationInfo.chargePointVendor,
d760a0a6 497 ...(stationInfo.firmwareVersion != null && {
66a7748d 498 firmwareVersion: stationInfo.firmwareVersion
d270cc87 499 }),
d760a0a6 500 ...(stationInfo.chargeBoxSerialNumber != null && {
66a7748d 501 serialNumber: stationInfo.chargeBoxSerialNumber
d270cc87 502 }),
d760a0a6 503 ...((stationInfo.iccid != null || stationInfo.imsi != null) && {
fba11dc6 504 modem: {
d760a0a6
JB
505 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
506 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi })
66a7748d
JB
507 }
508 })
509 }
510 } satisfies OCPP20BootNotificationRequest
fba11dc6 511 }
66a7748d 512}
fba11dc6
JB
513
514export const warnTemplateKeysDeprecation = (
515 stationTemplate: ChargingStationTemplate,
516 logPrefix: string,
66a7748d
JB
517 templateFile: string
518): void => {
519 const templateKeys: Array<{ deprecatedKey: string, key?: string }> = [
e4c6cf05
JB
520 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
521 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
522 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
66a7748d
JB
523 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' }
524 ]
fba11dc6
JB
525 for (const templateKey of templateKeys) {
526 warnDeprecatedTemplateKey(
527 stationTemplate,
528 templateKey.deprecatedKey,
529 logPrefix,
530 templateFile,
d760a0a6 531 templateKey.key != null ? `Use '${templateKey.key}' instead` : undefined
66a7748d
JB
532 )
533 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
fba11dc6 534 }
66a7748d 535}
fba11dc6
JB
536
537export const stationTemplateToStationInfo = (
66a7748d 538 stationTemplate: ChargingStationTemplate
fba11dc6 539): ChargingStationInfo => {
40615072 540 stationTemplate = clone<ChargingStationTemplate>(stationTemplate)
66a7748d
JB
541 delete stationTemplate.power
542 delete stationTemplate.powerUnit
543 delete stationTemplate.Connectors
544 delete stationTemplate.Evses
545 delete stationTemplate.Configuration
546 delete stationTemplate.AutomaticTransactionGenerator
e566a667 547 delete stationTemplate.numberOfConnectors
66a7748d
JB
548 delete stationTemplate.chargeBoxSerialNumberPrefix
549 delete stationTemplate.chargePointSerialNumberPrefix
550 delete stationTemplate.meterSerialNumberPrefix
551 return stationTemplate as ChargingStationInfo
552}
fba11dc6
JB
553
554export const createSerialNumber = (
555 stationTemplate: ChargingStationTemplate,
556 stationInfo: ChargingStationInfo,
7f3decca 557 params?: {
66a7748d
JB
558 randomSerialNumberUpperCase?: boolean
559 randomSerialNumber?: boolean
560 }
fba11dc6 561): void => {
48847bc0
JB
562 params = {
563 ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true },
564 ...params
565 }
66a7748d 566 const serialNumberSuffix =
5199f9fd 567 params.randomSerialNumber === true
66a7748d
JB
568 ? getRandomSerialNumberSuffix({
569 upperCase: params.randomSerialNumberUpperCase
fba11dc6 570 })
66a7748d 571 : ''
5199f9fd 572 isNotEmptyString(stationTemplate.chargePointSerialNumberPrefix) &&
66a7748d 573 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
5199f9fd 574 isNotEmptyString(stationTemplate.chargeBoxSerialNumberPrefix) &&
66a7748d 575 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
5199f9fd 576 isNotEmptyString(stationTemplate.meterSerialNumberPrefix) &&
66a7748d
JB
577 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
578}
fba11dc6
JB
579
580export const propagateSerialNumber = (
5199f9fd
JB
581 stationTemplate: ChargingStationTemplate | undefined,
582 stationInfoSrc: ChargingStationInfo | undefined,
66a7748d
JB
583 stationInfoDst: ChargingStationInfo
584): void => {
585 if (stationInfoSrc == null || stationTemplate == null) {
fba11dc6 586 throw new BaseError(
66a7748d
JB
587 'Missing charging station template or existing configuration to propagate serial number'
588 )
17ac262c 589 }
5199f9fd
JB
590 stationTemplate.chargePointSerialNumberPrefix != null &&
591 stationInfoSrc.chargePointSerialNumber != null
fba11dc6 592 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
5199f9fd 593 : stationInfoDst.chargePointSerialNumber != null &&
66a7748d 594 delete stationInfoDst.chargePointSerialNumber
5199f9fd
JB
595 stationTemplate.chargeBoxSerialNumberPrefix != null &&
596 stationInfoSrc.chargeBoxSerialNumber != null
fba11dc6 597 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
5199f9fd
JB
598 : stationInfoDst.chargeBoxSerialNumber != null && delete stationInfoDst.chargeBoxSerialNumber
599 stationTemplate.meterSerialNumberPrefix != null && stationInfoSrc.meterSerialNumber != null
fba11dc6 600 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
5199f9fd 601 : stationInfoDst.meterSerialNumber != null && delete stationInfoDst.meterSerialNumber
66a7748d 602}
fba11dc6 603
d8093be1
JB
604export const hasFeatureProfile = (
605 chargingStation: ChargingStation,
66a7748d 606 featureProfile: SupportedFeatureProfiles
d8093be1
JB
607): boolean | undefined => {
608 return getConfigurationKey(
609 chargingStation,
66a7748d
JB
610 StandardParametersKey.SupportedFeatureProfiles
611 )?.value?.includes(featureProfile)
612}
d8093be1 613
fba11dc6 614export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
66a7748d 615 let unitDivider = 1
fba11dc6
JB
616 switch (stationInfo.amperageLimitationUnit) {
617 case AmpereUnits.DECI_AMPERE:
66a7748d
JB
618 unitDivider = 10
619 break
fba11dc6 620 case AmpereUnits.CENTI_AMPERE:
66a7748d
JB
621 unitDivider = 100
622 break
fba11dc6 623 case AmpereUnits.MILLI_AMPERE:
66a7748d
JB
624 unitDivider = 1000
625 break
fba11dc6 626 }
66a7748d
JB
627 return unitDivider
628}
fba11dc6 629
6fc0c6f3 630/**
7abb61bb 631 * Gets the connector charging profiles relevant for power limitation shallow cloned and sorted by priorities
6fc0c6f3 632 *
7abb61bb
JB
633 * @param chargingStation - Charging station
634 * @param connectorId - Connector id
6fc0c6f3
JB
635 * @returns connector charging profiles array
636 */
637export const getConnectorChargingProfiles = (
638 chargingStation: ChargingStation,
66a7748d
JB
639 connectorId: number
640): ChargingProfile[] => {
7abb61bb 641 // FIXME: handle charging profile purpose CHARGE_POINT_MAX_PROFILE
a629e6fc
JB
642 return (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
643 .slice()
7abb61bb
JB
644 .sort((a, b) => {
645 if (
646 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE &&
647 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
648 ) {
649 return -1
650 } else if (
651 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE &&
652 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE
653 ) {
654 return 1
655 }
656 return b.stackLevel - a.stackLevel
657 })
a629e6fc
JB
658 .concat(
659 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [])
7abb61bb
JB
660 .filter(
661 chargingProfile =>
662 chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
663 )
a629e6fc
JB
664 .sort((a, b) => b.stackLevel - a.stackLevel)
665 )
66a7748d 666}
6fc0c6f3 667
fba11dc6
JB
668export const getChargingStationConnectorChargingProfilesPowerLimit = (
669 chargingStation: ChargingStation,
66a7748d 670 connectorId: number
fba11dc6 671): number | undefined => {
66a7748d 672 let limit: number | undefined, chargingProfile: ChargingProfile | undefined
66a7748d 673 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
fba11dc6 674 if (isNotEmptyArray(chargingProfiles)) {
a71d4e70
JB
675 const result = getLimitFromChargingProfiles(
676 chargingStation,
677 connectorId,
678 chargingProfiles,
66a7748d
JB
679 chargingStation.logPrefix()
680 )
be9f397b 681 if (result != null) {
5199f9fd
JB
682 limit = result.limit
683 chargingProfile = result.chargingProfile
5398cecf 684 switch (chargingStation.stationInfo?.currentOutType) {
fba11dc6
JB
685 case CurrentType.AC:
686 limit =
5199f9fd 687 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6
JB
688 ? limit
689 : ACElectricUtils.powerTotal(
66a7748d
JB
690 chargingStation.getNumberOfPhases(),
691 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
692 chargingStation.stationInfo.voltageOut!,
be9f397b 693 limit
66a7748d
JB
694 )
695 break
fba11dc6
JB
696 case CurrentType.DC:
697 limit =
5199f9fd 698 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6 699 ? limit
66a7748d 700 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
be9f397b 701 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
fba11dc6
JB
702 }
703 const connectorMaximumPower =
66a7748d 704 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 705 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
be9f397b 706 if (limit > connectorMaximumPower) {
fba11dc6 707 logger.error(
5199f9fd
JB
708 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
709 chargingProfile.chargingProfileId
710 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
66a7748d
JB
711 result
712 )
713 limit = connectorMaximumPower
15068be9
JB
714 }
715 }
15068be9 716 }
66a7748d
JB
717 return limit
718}
fba11dc6
JB
719
720export const getDefaultVoltageOut = (
721 currentType: CurrentType,
722 logPrefix: string,
66a7748d 723 templateFile: string
fba11dc6 724): Voltage => {
66a7748d
JB
725 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
726 let defaultVoltageOut: number
fba11dc6
JB
727 switch (currentType) {
728 case CurrentType.AC:
66a7748d
JB
729 defaultVoltageOut = Voltage.VOLTAGE_230
730 break
fba11dc6 731 case CurrentType.DC:
66a7748d
JB
732 defaultVoltageOut = Voltage.VOLTAGE_400
733 break
fba11dc6 734 default:
66a7748d
JB
735 logger.error(`${logPrefix} ${errorMsg}`)
736 throw new BaseError(errorMsg)
15068be9 737 }
66a7748d
JB
738 return defaultVoltageOut
739}
fba11dc6
JB
740
741export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
66a7748d
JB
742 return stationInfo.idTagsFile != null
743 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
744 : undefined
745}
fba11dc6 746
b2b60626 747export const waitChargingStationEvents = async (
fba11dc6
JB
748 emitter: EventEmitter,
749 event: ChargingStationWorkerMessageEvents,
66a7748d 750 eventsToWait: number
fba11dc6 751): Promise<number> => {
a974c8e4 752 return await new Promise<number>(resolve => {
66a7748d 753 let events = 0
fba11dc6 754 if (eventsToWait === 0) {
66a7748d
JB
755 resolve(events)
756 return
fba11dc6
JB
757 }
758 emitter.on(event, () => {
66a7748d 759 ++events
fba11dc6 760 if (events === eventsToWait) {
66a7748d 761 resolve(events)
b1f1b0f6 762 }
66a7748d
JB
763 })
764 })
765}
fba11dc6 766
cfc9875a 767const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
66a7748d
JB
768 let configuredMaxNumberOfConnectors = 0
769 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
5dc7c990 770 const numberOfConnectors = stationTemplate.numberOfConnectors
66a7748d
JB
771 configuredMaxNumberOfConnectors =
772 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
300418e9 773 } else if (stationTemplate.numberOfConnectors != null) {
5dc7c990 774 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
66a7748d 775 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
cfc9875a 776 configuredMaxNumberOfConnectors =
5199f9fd
JB
777 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
778 stationTemplate.Connectors[0] != null
66a7748d
JB
779 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
780 : getMaxNumberOfConnectors(stationTemplate.Connectors)
781 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
fba11dc6
JB
782 for (const evse in stationTemplate.Evses) {
783 if (evse === '0') {
66a7748d 784 continue
cda5d0fb 785 }
cfc9875a 786 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
66a7748d
JB
787 stationTemplate.Evses[evse].Connectors
788 )
cda5d0fb 789 }
cda5d0fb 790 }
66a7748d
JB
791 return configuredMaxNumberOfConnectors
792}
fba11dc6
JB
793
794const checkConfiguredMaxConnectors = (
795 configuredMaxConnectors: number,
796 logPrefix: string,
66a7748d 797 templateFile: string
fba11dc6
JB
798): void => {
799 if (configuredMaxConnectors <= 0) {
800 logger.warn(
66a7748d
JB
801 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
802 )
cda5d0fb 803 }
66a7748d 804}
cda5d0fb 805
fba11dc6
JB
806const checkTemplateMaxConnectors = (
807 templateMaxConnectors: number,
808 logPrefix: string,
66a7748d 809 templateFile: string
fba11dc6
JB
810): void => {
811 if (templateMaxConnectors === 0) {
812 logger.warn(
66a7748d
JB
813 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
814 )
fba11dc6
JB
815 } else if (templateMaxConnectors < 0) {
816 logger.error(
66a7748d
JB
817 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
818 )
fba11dc6 819 }
66a7748d 820}
fba11dc6
JB
821
822const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
66a7748d
JB
823 connectorStatus.availability = AvailabilityType.Operative
824 connectorStatus.idTagLocalAuthorized = false
825 connectorStatus.idTagAuthorized = false
826 connectorStatus.transactionRemoteStarted = false
827 connectorStatus.transactionStarted = false
828 connectorStatus.energyActiveImportRegisterValue = 0
829 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
300418e9 830 if (connectorStatus.chargingProfiles == null) {
66a7748d 831 connectorStatus.chargingProfiles = []
fba11dc6 832 }
66a7748d 833}
fba11dc6
JB
834
835const warnDeprecatedTemplateKey = (
836 template: ChargingStationTemplate,
837 key: string,
838 logPrefix: string,
839 templateFile: string,
66a7748d 840 logMsgToAppend = ''
fba11dc6 841): void => {
d760a0a6 842 if (template[key as keyof ChargingStationTemplate] != null) {
fba11dc6
JB
843 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
844 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
66a7748d
JB
845 }`
846 logger.warn(`${logPrefix} ${logMsg}`)
847 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
fba11dc6 848 }
66a7748d 849}
fba11dc6
JB
850
851const convertDeprecatedTemplateKey = (
852 template: ChargingStationTemplate,
853 deprecatedKey: string,
66a7748d 854 key?: string
fba11dc6 855): void => {
d760a0a6
JB
856 if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
857 if (key != null) {
300418e9 858 (template as unknown as Record<string, unknown>)[key] =
66a7748d 859 template[deprecatedKey as keyof ChargingStationTemplate]
e1d9a0f4 860 }
66a7748d
JB
861 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
862 delete template[deprecatedKey as keyof ChargingStationTemplate]
fba11dc6 863 }
66a7748d 864}
fba11dc6 865
947f048a 866interface ChargingProfilesLimit {
66a7748d
JB
867 limit: number
868 chargingProfile: ChargingProfile
947f048a
JB
869}
870
fba11dc6 871/**
21ee4dc2 872 * Charging profiles shall already be sorted by connector id descending then stack level descending
fba11dc6 873 *
d467756c
JB
874 * @param chargingStation -
875 * @param connectorId -
fba11dc6
JB
876 * @param chargingProfiles -
877 * @param logPrefix -
947f048a 878 * @returns ChargingProfilesLimit
fba11dc6
JB
879 */
880const getLimitFromChargingProfiles = (
a71d4e70
JB
881 chargingStation: ChargingStation,
882 connectorId: number,
fba11dc6 883 chargingProfiles: ChargingProfile[],
66a7748d 884 logPrefix: string
947f048a 885): ChargingProfilesLimit | undefined => {
66a7748d
JB
886 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
887 const currentDate = new Date()
f938317f 888 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
7abb61bb 889 let previousActiveChargingProfile: ChargingProfile | undefined
fba11dc6 890 for (const chargingProfile of chargingProfiles) {
66a7748d 891 const chargingSchedule = chargingProfile.chargingSchedule
2466918c 892 if (chargingSchedule.startSchedule == null) {
109c677a 893 logger.debug(
66a7748d
JB
894 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
895 )
a71d4e70 896 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
f938317f 897 chargingSchedule.startSchedule = connectorStatus?.transactionStart
52952bf8 898 }
2466918c 899 if (!isDate(chargingSchedule.startSchedule)) {
ef9e3b33 900 logger.warn(
66a7748d
JB
901 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
902 )
903 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 904 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
ef9e3b33 905 }
2466918c 906 if (chargingSchedule.duration == null) {
da332e70 907 logger.debug(
66a7748d
JB
908 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
909 )
da332e70 910 // OCPP specifies that if duration is not defined, it should be infinite
be9f397b 911 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
da332e70 912 }
0eb666db 913 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
66a7748d 914 continue
ec4a242a 915 }
0bd926c1 916 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
66a7748d 917 continue
142a66c9 918 }
fba11dc6
JB
919 // Check if the charging profile is active
920 if (
975e18ec 921 isWithinInterval(currentDate, {
2466918c
JB
922 start: chargingSchedule.startSchedule,
923 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
975e18ec 924 })
fba11dc6 925 ) {
252a7d22 926 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
927 const chargingSchedulePeriodCompareFn = (
928 a: ChargingSchedulePeriod,
66a7748d
JB
929 b: ChargingSchedulePeriod
930 ): number => a.startPeriod - b.startPeriod
80c58041 931 if (
6fc0c6f3 932 !isArraySorted<ChargingSchedulePeriod>(
80c58041 933 chargingSchedule.chargingSchedulePeriod,
66a7748d 934 chargingSchedulePeriodCompareFn
6fc0c6f3 935 )
80c58041
JB
936 ) {
937 logger.warn(
66a7748d
JB
938 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
939 )
940 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
80c58041 941 }
da332e70 942 // Check if the first schedule period startPeriod property is equal to 0
55f2ab60
JB
943 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
944 logger.error(
66a7748d
JB
945 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
946 )
947 continue
55f2ab60 948 }
991fb26b 949 // Handle only one schedule period
975e18ec 950 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
951 const result: ChargingProfilesLimit = {
952 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
66a7748d
JB
953 chargingProfile
954 }
955 logger.debug(debugLogMsg, result)
956 return result
41189456 957 }
66a7748d 958 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
252a7d22 959 // Search for the right schedule period
e3037969
JB
960 for (const [
961 index,
66a7748d 962 chargingSchedulePeriod
e3037969 963 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
964 // Find the right schedule period
965 if (
966 isAfter(
2466918c 967 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
66a7748d 968 currentDate
252a7d22
JB
969 )
970 ) {
e3037969 971 // Found the schedule period: previous is the correct one
252a7d22 972 const result: ChargingProfilesLimit = {
7abb61bb
JB
973 limit: previousChargingSchedulePeriod?.limit ?? chargingSchedulePeriod.limit,
974 chargingProfile: previousActiveChargingProfile ?? chargingProfile
66a7748d
JB
975 }
976 logger.debug(debugLogMsg, result)
977 return result
252a7d22 978 }
975e18ec 979 // Handle the last schedule period within the charging profile duration
252a7d22 980 if (
975e18ec
JB
981 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
982 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
ccfa30bc
JB
983 differenceInSeconds(
984 addSeconds(
2466918c 985 chargingSchedule.startSchedule,
66a7748d 986 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
ccfa30bc 987 ),
2466918c
JB
988 chargingSchedule.startSchedule
989 ) > chargingSchedule.duration)
252a7d22
JB
990 ) {
991 const result: ChargingProfilesLimit = {
7abb61bb 992 limit: chargingSchedulePeriod.limit,
66a7748d
JB
993 chargingProfile
994 }
995 logger.debug(debugLogMsg, result)
996 return result
252a7d22 997 }
7abb61bb
JB
998 // Keep a reference to previous charging schedule period
999 previousChargingSchedulePeriod = chargingSchedulePeriod
17ac262c
JB
1000 }
1001 }
7abb61bb
JB
1002 // Keep a reference to previous active charging profile
1003 previousActiveChargingProfile = chargingProfile
17ac262c 1004 }
17ac262c 1005 }
66a7748d 1006}
17ac262c 1007
0eb666db 1008export const prepareChargingProfileKind = (
f938317f 1009 connectorStatus: ConnectorStatus | undefined,
0eb666db 1010 chargingProfile: ChargingProfile,
6dde6c5f 1011 currentDate: string | number | Date,
66a7748d 1012 logPrefix: string
0eb666db
JB
1013): boolean => {
1014 switch (chargingProfile.chargingProfileKind) {
1015 case ChargingProfileKindType.RECURRING:
1016 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
66a7748d 1017 return false
0eb666db 1018 }
66a7748d
JB
1019 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
1020 break
0eb666db 1021 case ChargingProfileKindType.RELATIVE:
be9f397b 1022 if (chargingProfile.chargingSchedule.startSchedule != null) {
ccfa30bc 1023 logger.warn(
66a7748d
JB
1024 `${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`
1025 )
1026 delete chargingProfile.chargingSchedule.startSchedule
ccfa30bc 1027 }
f938317f 1028 if (connectorStatus?.transactionStarted === true) {
5199f9fd 1029 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
ef9e3b33 1030 }
24dc52e9 1031 // FIXME: handle relative charging profile duration
66a7748d 1032 break
0eb666db 1033 }
66a7748d
JB
1034 return true
1035}
0eb666db 1036
ad490d5f 1037export const canProceedChargingProfile = (
0bd926c1 1038 chargingProfile: ChargingProfile,
6dde6c5f 1039 currentDate: string | number | Date,
66a7748d 1040 logPrefix: string
0bd926c1
JB
1041): boolean => {
1042 if (
5dc7c990
JB
1043 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
1044 (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
0bd926c1
JB
1045 ) {
1046 logger.debug(
1047 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1048 chargingProfile.chargingProfileId
6dde6c5f 1049 } is not valid for the current date ${
5dc7c990 1050 isDate(currentDate) ? currentDate.toISOString() : currentDate
6dde6c5f 1051 }`
66a7748d
JB
1052 )
1053 return false
0bd926c1 1054 }
ef9e3b33 1055 if (
be9f397b
JB
1056 chargingProfile.chargingSchedule.startSchedule == null ||
1057 chargingProfile.chargingSchedule.duration == null
ef9e3b33 1058 ) {
0bd926c1 1059 logger.error(
66a7748d
JB
1060 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1061 )
1062 return false
ef9e3b33 1063 }
5dc7c990 1064 if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
ef9e3b33 1065 logger.error(
66a7748d
JB
1066 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1067 )
1068 return false
ef9e3b33 1069 }
5199f9fd 1070 if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
ef9e3b33 1071 logger.error(
66a7748d
JB
1072 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1073 )
1074 return false
0bd926c1 1075 }
66a7748d
JB
1076 return true
1077}
0bd926c1 1078
0eb666db 1079const canProceedRecurringChargingProfile = (
0bd926c1 1080 chargingProfile: ChargingProfile,
66a7748d 1081 logPrefix: string
0bd926c1
JB
1082): boolean => {
1083 if (
1084 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1085 chargingProfile.recurrencyKind == null
0bd926c1
JB
1086 ) {
1087 logger.error(
66a7748d
JB
1088 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1089 )
1090 return false
0bd926c1 1091 }
d929adcc
JB
1092 if (
1093 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1094 chargingProfile.chargingSchedule.startSchedule == null
d929adcc 1095 ) {
ef9e3b33 1096 logger.error(
66a7748d
JB
1097 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1098 )
1099 return false
ef9e3b33 1100 }
66a7748d
JB
1101 return true
1102}
0bd926c1 1103
522e4b05 1104/**
ec4a242a 1105 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
1106 *
1107 * @param chargingProfile -
1108 * @param currentDate -
1109 * @param logPrefix -
1110 */
0eb666db 1111const prepareRecurringChargingProfile = (
76dab5a9 1112 chargingProfile: ChargingProfile,
6dde6c5f 1113 currentDate: string | number | Date,
66a7748d 1114 logPrefix: string
ec4a242a 1115): boolean => {
66a7748d
JB
1116 const chargingSchedule = chargingProfile.chargingSchedule
1117 let recurringIntervalTranslated = false
5dc7c990 1118 let recurringInterval: Interval | undefined
76dab5a9
JB
1119 switch (chargingProfile.recurrencyKind) {
1120 case RecurrencyKindType.DAILY:
522e4b05 1121 recurringInterval = {
66a7748d 1122 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1123 start: chargingSchedule.startSchedule!,
66a7748d
JB
1124 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1125 end: addDays(chargingSchedule.startSchedule!, 1)
1126 }
1127 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1128 if (
1129 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1130 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1131 ) {
1132 chargingSchedule.startSchedule = addDays(
991fb26b 1133 recurringInterval.start,
66a7748d
JB
1134 differenceInDays(currentDate, recurringInterval.start)
1135 )
522e4b05
JB
1136 recurringInterval = {
1137 start: chargingSchedule.startSchedule,
66a7748d
JB
1138 end: addDays(chargingSchedule.startSchedule, 1)
1139 }
1140 recurringIntervalTranslated = true
76dab5a9 1141 }
66a7748d 1142 break
76dab5a9 1143 case RecurrencyKindType.WEEKLY:
522e4b05 1144 recurringInterval = {
66a7748d 1145 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1146 start: chargingSchedule.startSchedule!,
66a7748d
JB
1147 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1148 end: addWeeks(chargingSchedule.startSchedule!, 1)
1149 }
1150 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1151 if (
1152 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1153 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1154 ) {
1155 chargingSchedule.startSchedule = addWeeks(
991fb26b 1156 recurringInterval.start,
66a7748d
JB
1157 differenceInWeeks(currentDate, recurringInterval.start)
1158 )
522e4b05
JB
1159 recurringInterval = {
1160 start: chargingSchedule.startSchedule,
66a7748d
JB
1161 end: addWeeks(chargingSchedule.startSchedule, 1)
1162 }
1163 recurringIntervalTranslated = true
76dab5a9 1164 }
66a7748d 1165 break
ec4a242a
JB
1166 default:
1167 logger.error(
66a7748d
JB
1168 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1169 )
76dab5a9 1170 }
66a7748d 1171 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ec4a242a 1172 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 1173 logger.error(
aa5c5ad4 1174 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 1175 chargingProfile.recurrencyKind
991fb26b 1176 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
5dc7c990 1177 recurringInterval?.start as Date
991fb26b 1178 ).toISOString()}, ${toDate(
5dc7c990 1179 recurringInterval?.end as Date
6dde6c5f 1180 ).toISOString()}] has not been properly translated to current date ${
5dc7c990 1181 isDate(currentDate) ? currentDate.toISOString() : currentDate
6dde6c5f 1182 } `
66a7748d 1183 )
522e4b05 1184 }
66a7748d
JB
1185 return recurringIntervalTranslated
1186}
76dab5a9 1187
d476bc1b
JB
1188const checkRecurringChargingProfileDuration = (
1189 chargingProfile: ChargingProfile,
1190 interval: Interval,
66a7748d 1191 logPrefix: string
ec4a242a 1192): void => {
be9f397b 1193 if (chargingProfile.chargingSchedule.duration == null) {
142a66c9
JB
1194 logger.warn(
1195 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1196 chargingProfile.chargingProfileKind
1197 } charging profile id ${
1198 chargingProfile.chargingProfileId
1199 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1200 interval.end,
66a7748d
JB
1201 interval.start
1202 )}`
1203 )
1204 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
142a66c9 1205 } else if (
be9f397b 1206 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
d476bc1b
JB
1207 ) {
1208 logger.warn(
aa5c5ad4 1209 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1210 chargingProfile.chargingProfileKind
1211 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1212 chargingProfile.chargingSchedule.duration
710d50eb 1213 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b 1214 interval.end,
66a7748d
JB
1215 interval.start
1216 )}`
1217 )
1218 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
d476bc1b 1219 }
66a7748d 1220}
d476bc1b 1221
fba11dc6 1222const getRandomSerialNumberSuffix = (params?: {
66a7748d
JB
1223 randomBytesLength?: number
1224 upperCase?: boolean
fba11dc6 1225}): string => {
66a7748d
JB
1226 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1227 if (params?.upperCase === true) {
1228 return randomSerialNumberSuffix.toUpperCase()
17ac262c 1229 }
66a7748d
JB
1230 return randomSerialNumberSuffix
1231}