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