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