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