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