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