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