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