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