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