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