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