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