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