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