Commit | Line | Data |
---|---|---|
43ee4373 | 1 | import crypto from 'node:crypto'; |
b1f1b0f6 | 2 | import type EventEmitter from 'node:events'; |
130783a7 JB |
3 | import path from 'node:path'; |
4 | import { fileURLToPath } from 'node:url'; | |
8114d10e | 5 | |
e302df1d | 6 | import chalk from 'chalk'; |
8114d10e JB |
7 | import moment from 'moment'; |
8 | ||
4c3c0d59 | 9 | import type { ChargingStation } from './ChargingStation'; |
268a74bb | 10 | import { BaseError } from '../exception'; |
981ebfbe | 11 | import { |
492cf6ab | 12 | AmpereUnits, |
04b1261c | 13 | AvailabilityType, |
268a74bb JB |
14 | type BootNotificationRequest, |
15 | BootReasonEnumType, | |
15068be9 | 16 | type ChargingProfile, |
268a74bb | 17 | ChargingProfileKindType, |
15068be9 JB |
18 | ChargingRateUnitType, |
19 | type ChargingSchedulePeriod, | |
268a74bb JB |
20 | type ChargingStationInfo, |
21 | type ChargingStationTemplate, | |
b1f1b0f6 | 22 | ChargingStationWorkerMessageEvents, |
dd08d43d | 23 | ConnectorPhaseRotation, |
a78ef5ed | 24 | type ConnectorStatus, |
c3b83130 | 25 | ConnectorStatusEnum, |
268a74bb | 26 | CurrentType, |
ae25f265 | 27 | type EvseTemplate, |
268a74bb JB |
28 | type OCPP16BootNotificationRequest, |
29 | type OCPP20BootNotificationRequest, | |
30 | OCPPVersion, | |
31 | RecurrencyKindType, | |
32 | Voltage, | |
33 | } from '../types'; | |
aa7d6d95 | 34 | import { ACElectricUtils, Constants, DCElectricUtils, Utils, logger } from '../utils'; |
17ac262c | 35 | |
91a4f151 JB |
36 | const moduleName = 'ChargingStationUtils'; |
37 | ||
17ac262c | 38 | export class ChargingStationUtils { |
d5bd1c00 JB |
39 | private constructor() { |
40 | // This is intentional | |
41 | } | |
42 | ||
17ac262c JB |
43 | public static getChargingStationId( |
44 | index: number, | |
45 | stationTemplate: ChargingStationTemplate | |
46 | ): string { | |
47 | // In case of multiple instances: add instance index to charging station id | |
48 | const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0; | |
1b271a54 | 49 | const idSuffix = stationTemplate?.nameSuffix ?? ''; |
44eb6026 | 50 | const idStr = `000000000${index.toString()}`; |
ccb1d6e9 | 51 | return stationTemplate?.fixedName |
17ac262c | 52 | ? stationTemplate.baseName |
44eb6026 JB |
53 | : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring( |
54 | idStr.length - 4 | |
55 | )}${idSuffix}`; | |
17ac262c JB |
56 | } |
57 | ||
fa7bccf4 | 58 | public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string { |
99e92237 | 59 | const chargingStationInfo = { |
fa7bccf4 JB |
60 | chargePointModel: stationTemplate.chargePointModel, |
61 | chargePointVendor: stationTemplate.chargePointVendor, | |
62 | ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && { | |
63 | chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix, | |
17ac262c | 64 | }), |
fa7bccf4 JB |
65 | ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && { |
66 | chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix, | |
17ac262c | 67 | }), |
fa7bccf4 JB |
68 | ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && { |
69 | meterSerialNumber: stationTemplate.meterSerialNumberPrefix, | |
17ac262c | 70 | }), |
fa7bccf4 JB |
71 | ...(!Utils.isUndefined(stationTemplate.meterType) && { |
72 | meterType: stationTemplate.meterType, | |
17ac262c JB |
73 | }), |
74 | }; | |
75 | return crypto | |
76 | .createHash(Constants.DEFAULT_HASH_ALGORITHM) | |
fa7bccf4 | 77 | .update( |
14ecae6a JB |
78 | `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId( |
79 | index, | |
80 | stationTemplate | |
81 | )}` | |
fa7bccf4 | 82 | ) |
17ac262c JB |
83 | .digest('hex'); |
84 | } | |
85 | ||
1bf29f5b JB |
86 | public static checkChargingStation(chargingStation: ChargingStation, logPrefix: string): boolean { |
87 | if (chargingStation.started === false && chargingStation.starting === false) { | |
88 | logger.warn(`${logPrefix} charging station is stopped, cannot proceed`); | |
89 | return false; | |
90 | } | |
91 | return true; | |
92 | } | |
93 | ||
be34dba5 JB |
94 | public static getPhaseRotationValue( |
95 | connectorId: number, | |
96 | numberOfPhases: number | |
97 | ): string | undefined { | |
dd08d43d JB |
98 | // AC/DC |
99 | if (connectorId === 0 && numberOfPhases === 0) { | |
100 | return `${connectorId}.${ConnectorPhaseRotation.RST}`; | |
101 | } else if (connectorId > 0 && numberOfPhases === 0) { | |
102 | return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`; | |
103 | // AC | |
104 | } else if (connectorId > 0 && numberOfPhases === 1) { | |
105 | return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`; | |
106 | } else if (connectorId > 0 && numberOfPhases === 3) { | |
107 | return `${connectorId}.${ConnectorPhaseRotation.RST}`; | |
108 | } | |
109 | } | |
110 | ||
ae25f265 JB |
111 | public static getMaxNumberOfEvses(evses: Record<string, EvseTemplate>): number { |
112 | if (!evses) { | |
113 | return -1; | |
114 | } | |
115 | return Object.keys(evses).length; | |
116 | } | |
117 | ||
a78ef5ed JB |
118 | public static getMaxNumberOfConnectors(connectors: Record<string, ConnectorStatus>): number { |
119 | if (!connectors) { | |
fa7bccf4 JB |
120 | return -1; |
121 | } | |
a78ef5ed | 122 | return Object.keys(connectors).length; |
fa7bccf4 JB |
123 | } |
124 | ||
c3b83130 JB |
125 | public static getBootConnectorStatus( |
126 | chargingStation: ChargingStation, | |
127 | connectorId: number, | |
128 | connectorStatus: ConnectorStatus | |
129 | ): ConnectorStatusEnum { | |
130 | let connectorBootStatus: ConnectorStatusEnum; | |
131 | if ( | |
132 | !connectorStatus?.status && | |
133 | (chargingStation.isChargingStationAvailable() === false || | |
134 | chargingStation.isConnectorAvailable(connectorId) === false) | |
135 | ) { | |
136 | connectorBootStatus = ConnectorStatusEnum.Unavailable; | |
137 | } else if (!connectorStatus?.status && connectorStatus?.bootStatus) { | |
138 | // Set boot status in template at startup | |
139 | connectorBootStatus = connectorStatus?.bootStatus; | |
140 | } else if (connectorStatus?.status) { | |
141 | // Set previous status at startup | |
142 | connectorBootStatus = connectorStatus?.status; | |
143 | } else { | |
144 | // Set default status | |
145 | connectorBootStatus = ConnectorStatusEnum.Available; | |
146 | } | |
147 | return connectorBootStatus; | |
148 | } | |
149 | ||
e1893686 | 150 | public static checkTemplate( |
8a133cc8 JB |
151 | stationTemplate: ChargingStationTemplate, |
152 | logPrefix: string, | |
153 | templateFile: string | |
154 | ) { | |
155 | if (Utils.isNullOrUndefined(stationTemplate)) { | |
156 | const errorMsg = `Failed to read charging station template file ${templateFile}`; | |
157 | logger.error(`${logPrefix} ${errorMsg}`); | |
158 | throw new BaseError(errorMsg); | |
159 | } | |
160 | if (Utils.isEmptyObject(stationTemplate)) { | |
161 | const errorMsg = `Empty charging station information from template file ${templateFile}`; | |
162 | logger.error(`${logPrefix} ${errorMsg}`); | |
163 | throw new BaseError(errorMsg); | |
164 | } | |
ac7f79af | 165 | if (Utils.isEmptyObject(stationTemplate.AutomaticTransactionGenerator)) { |
1fe0632a | 166 | stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION; |
ac7f79af | 167 | logger.warn( |
1fe0632a JB |
168 | `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`, |
169 | Constants.DEFAULT_ATG_CONFIGURATION | |
ac7f79af JB |
170 | ); |
171 | } | |
111aaf89 JB |
172 | if ( |
173 | Utils.isNullOrUndefined(stationTemplate.idTagsFile) || | |
174 | Utils.isEmptyString(stationTemplate.idTagsFile) | |
175 | ) { | |
176 | logger.warn( | |
177 | `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator` | |
178 | ); | |
179 | } | |
8a133cc8 JB |
180 | } |
181 | ||
cda5d0fb | 182 | public static checkConnectorsConfiguration( |
8a133cc8 JB |
183 | stationTemplate: ChargingStationTemplate, |
184 | logPrefix: string, | |
185 | templateFile: string | |
cda5d0fb JB |
186 | ): { |
187 | configuredMaxConnectors: number; | |
188 | templateMaxConnectors: number; | |
189 | templateMaxAvailableConnectors: number; | |
190 | } { | |
191 | const configuredMaxConnectors = | |
192 | ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate); | |
193 | ChargingStationUtils.checkConfiguredMaxConnectors( | |
194 | configuredMaxConnectors, | |
8a133cc8 JB |
195 | logPrefix, |
196 | templateFile | |
cda5d0fb JB |
197 | ); |
198 | const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors( | |
199 | stationTemplate.Connectors | |
200 | ); | |
8a133cc8 | 201 | ChargingStationUtils.checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile); |
cda5d0fb JB |
202 | const templateMaxAvailableConnectors = stationTemplate?.Connectors[0] |
203 | ? templateMaxConnectors - 1 | |
204 | : templateMaxConnectors; | |
205 | if ( | |
206 | configuredMaxConnectors > templateMaxAvailableConnectors && | |
207 | !stationTemplate?.randomConnectors | |
208 | ) { | |
fa7bccf4 | 209 | logger.warn( |
cda5d0fb | 210 | `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation` |
fa7bccf4 | 211 | ); |
cda5d0fb | 212 | stationTemplate.randomConnectors = true; |
fa7bccf4 | 213 | } |
cda5d0fb | 214 | return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors }; |
fa7bccf4 JB |
215 | } |
216 | ||
04b1261c JB |
217 | public static checkStationInfoConnectorStatus( |
218 | connectorId: number, | |
219 | connectorStatus: ConnectorStatus, | |
220 | logPrefix: string, | |
221 | templateFile: string | |
222 | ): void { | |
223 | if (!Utils.isNullOrUndefined(connectorStatus?.status)) { | |
224 | logger.warn( | |
54ebb82c | 225 | `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it` |
04b1261c JB |
226 | ); |
227 | delete connectorStatus.status; | |
228 | } | |
229 | } | |
230 | ||
231 | public static buildConnectorsMap( | |
232 | connectors: Record<string, ConnectorStatus>, | |
233 | logPrefix: string, | |
234 | templateFile: string | |
235 | ): Map<number, ConnectorStatus> { | |
236 | const connectorsMap = new Map<number, ConnectorStatus>(); | |
ae25f265 JB |
237 | if (ChargingStationUtils.getMaxNumberOfConnectors(connectors) > 0) { |
238 | for (const connector in connectors) { | |
239 | const connectorStatus = connectors[connector]; | |
240 | const connectorId = Utils.convertToInt(connector); | |
241 | ChargingStationUtils.checkStationInfoConnectorStatus( | |
242 | connectorId, | |
243 | connectorStatus, | |
244 | logPrefix, | |
245 | templateFile | |
246 | ); | |
247 | connectorsMap.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus)); | |
04b1261c | 248 | } |
ae25f265 JB |
249 | } else { |
250 | logger.warn( | |
251 | `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map` | |
252 | ); | |
04b1261c JB |
253 | } |
254 | return connectorsMap; | |
255 | } | |
256 | ||
257 | public static initializeConnectorsMapStatus( | |
258 | connectors: Map<number, ConnectorStatus>, | |
259 | logPrefix: string | |
260 | ): void { | |
261 | for (const connectorId of connectors.keys()) { | |
262 | if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) { | |
263 | logger.warn( | |
54ebb82c | 264 | `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${ |
04b1261c JB |
265 | connectors.get(connectorId)?.transactionId |
266 | }` | |
267 | ); | |
268 | } | |
8e2430ee | 269 | if (connectorId === 0) { |
52952bf8 JB |
270 | connectors.get(connectorId).availability = AvailabilityType.Operative; |
271 | if (Utils.isUndefined(connectors.get(connectorId)?.chargingProfiles)) { | |
272 | connectors.get(connectorId).chargingProfiles = []; | |
273 | } | |
274 | } else if ( | |
04b1261c JB |
275 | connectorId > 0 && |
276 | Utils.isNullOrUndefined(connectors.get(connectorId)?.transactionStarted) | |
277 | ) { | |
278 | ChargingStationUtils.initializeConnectorStatus(connectors.get(connectorId)); | |
279 | } | |
280 | } | |
281 | } | |
282 | ||
04b1261c JB |
283 | public static resetConnectorStatus(connectorStatus: ConnectorStatus): void { |
284 | connectorStatus.idTagLocalAuthorized = false; | |
285 | connectorStatus.idTagAuthorized = false; | |
286 | connectorStatus.transactionRemoteStarted = false; | |
287 | connectorStatus.transactionStarted = false; | |
288 | delete connectorStatus?.localAuthorizeIdTag; | |
289 | delete connectorStatus?.authorizeIdTag; | |
290 | delete connectorStatus?.transactionId; | |
291 | delete connectorStatus?.transactionIdTag; | |
292 | connectorStatus.transactionEnergyActiveImportRegisterValue = 0; | |
293 | delete connectorStatus?.transactionBeginMeterValue; | |
294 | } | |
295 | ||
17ac262c | 296 | public static createBootNotificationRequest( |
15068be9 JB |
297 | stationInfo: ChargingStationInfo, |
298 | bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp | |
17ac262c | 299 | ): BootNotificationRequest { |
d270cc87 JB |
300 | const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16; |
301 | switch (ocppVersion) { | |
302 | case OCPPVersion.VERSION_16: | |
303 | return { | |
304 | chargePointModel: stationInfo.chargePointModel, | |
305 | chargePointVendor: stationInfo.chargePointVendor, | |
306 | ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && { | |
307 | chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber, | |
308 | }), | |
309 | ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && { | |
310 | chargePointSerialNumber: stationInfo.chargePointSerialNumber, | |
311 | }), | |
312 | ...(!Utils.isUndefined(stationInfo.firmwareVersion) && { | |
313 | firmwareVersion: stationInfo.firmwareVersion, | |
314 | }), | |
315 | ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }), | |
316 | ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }), | |
317 | ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && { | |
318 | meterSerialNumber: stationInfo.meterSerialNumber, | |
319 | }), | |
320 | ...(!Utils.isUndefined(stationInfo.meterType) && { | |
321 | meterType: stationInfo.meterType, | |
322 | }), | |
323 | } as OCPP16BootNotificationRequest; | |
324 | case OCPPVersion.VERSION_20: | |
325 | case OCPPVersion.VERSION_201: | |
326 | return { | |
15068be9 | 327 | reason: bootReason, |
d270cc87 JB |
328 | chargingStation: { |
329 | model: stationInfo.chargePointModel, | |
330 | vendorName: stationInfo.chargePointVendor, | |
331 | ...(!Utils.isUndefined(stationInfo.firmwareVersion) && { | |
332 | firmwareVersion: stationInfo.firmwareVersion, | |
333 | }), | |
334 | ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && { | |
335 | serialNumber: stationInfo.chargeBoxSerialNumber, | |
336 | }), | |
98fc1389 JB |
337 | ...((!Utils.isUndefined(stationInfo.iccid) || !Utils.isUndefined(stationInfo.imsi)) && { |
338 | modem: { | |
339 | ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }), | |
340 | ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }), | |
341 | }, | |
342 | }), | |
d270cc87 JB |
343 | }, |
344 | } as OCPP20BootNotificationRequest; | |
345 | } | |
17ac262c JB |
346 | } |
347 | ||
ae5020a3 | 348 | public static warnTemplateKeysDeprecation( |
ae5020a3 | 349 | stationTemplate: ChargingStationTemplate, |
8a133cc8 JB |
350 | logPrefix: string, |
351 | templateFile: string | |
ae5020a3 JB |
352 | ) { |
353 | const templateKeys: { key: string; deprecatedKey: string }[] = [ | |
354 | { key: 'supervisionUrls', deprecatedKey: 'supervisionUrl' }, | |
355 | { key: 'idTagsFile', deprecatedKey: 'authorizationFile' }, | |
356 | ]; | |
357 | for (const templateKey of templateKeys) { | |
358 | ChargingStationUtils.warnDeprecatedTemplateKey( | |
359 | stationTemplate, | |
360 | templateKey.deprecatedKey, | |
ae5020a3 | 361 | logPrefix, |
8a133cc8 | 362 | templateFile, |
ae5020a3 JB |
363 | `Use '${templateKey.key}' instead` |
364 | ); | |
365 | ChargingStationUtils.convertDeprecatedTemplateKey( | |
366 | stationTemplate, | |
367 | templateKey.deprecatedKey, | |
368 | templateKey.key | |
369 | ); | |
17ac262c JB |
370 | } |
371 | } | |
372 | ||
fa7bccf4 JB |
373 | public static stationTemplateToStationInfo( |
374 | stationTemplate: ChargingStationTemplate | |
375 | ): ChargingStationInfo { | |
8df5ae48 | 376 | stationTemplate = Utils.cloneObject<ChargingStationTemplate>(stationTemplate); |
fa7bccf4 JB |
377 | delete stationTemplate.power; |
378 | delete stationTemplate.powerUnit; | |
8a133cc8 JB |
379 | delete stationTemplate?.Connectors; |
380 | delete stationTemplate?.Evses; | |
fa7bccf4 JB |
381 | delete stationTemplate.Configuration; |
382 | delete stationTemplate.AutomaticTransactionGenerator; | |
383 | delete stationTemplate.chargeBoxSerialNumberPrefix; | |
384 | delete stationTemplate.chargePointSerialNumberPrefix; | |
fec4d204 | 385 | delete stationTemplate.meterSerialNumberPrefix; |
51c83d6f | 386 | return stationTemplate as unknown as ChargingStationInfo; |
fa7bccf4 JB |
387 | } |
388 | ||
17ac262c | 389 | public static createSerialNumber( |
fa7bccf4 | 390 | stationTemplate: ChargingStationTemplate, |
4d20f040 | 391 | stationInfo: ChargingStationInfo, |
fa7bccf4 JB |
392 | params: { |
393 | randomSerialNumberUpperCase?: boolean; | |
394 | randomSerialNumber?: boolean; | |
395 | } = { | |
17ac262c JB |
396 | randomSerialNumberUpperCase: true, |
397 | randomSerialNumber: true, | |
398 | } | |
399 | ): void { | |
f832e5df | 400 | params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params }; |
fa7bccf4 JB |
401 | const serialNumberSuffix = params?.randomSerialNumber |
402 | ? ChargingStationUtils.getRandomSerialNumberSuffix({ | |
403 | upperCase: params.randomSerialNumberUpperCase, | |
404 | }) | |
405 | : ''; | |
f832e5df JB |
406 | Utils.isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) && |
407 | (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`); | |
408 | Utils.isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) && | |
409 | (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`); | |
410 | Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) && | |
411 | (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`); | |
fec4d204 JB |
412 | } |
413 | ||
414 | public static propagateSerialNumber( | |
415 | stationTemplate: ChargingStationTemplate, | |
416 | stationInfoSrc: ChargingStationInfo, | |
4d20f040 | 417 | stationInfoDst: ChargingStationInfo |
fec4d204 JB |
418 | ) { |
419 | if (!stationInfoSrc || !stationTemplate) { | |
baf93dda JB |
420 | throw new BaseError( |
421 | 'Missing charging station template or existing configuration to propagate serial number' | |
422 | ); | |
fec4d204 JB |
423 | } |
424 | stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber | |
425 | ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber) | |
426 | : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber; | |
427 | stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber | |
428 | ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber) | |
429 | : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber; | |
430 | stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber | |
431 | ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber) | |
432 | : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber; | |
17ac262c JB |
433 | } |
434 | ||
435 | public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number { | |
436 | let unitDivider = 1; | |
437 | switch (stationInfo.amperageLimitationUnit) { | |
438 | case AmpereUnits.DECI_AMPERE: | |
439 | unitDivider = 10; | |
440 | break; | |
441 | case AmpereUnits.CENTI_AMPERE: | |
442 | unitDivider = 100; | |
443 | break; | |
444 | case AmpereUnits.MILLI_AMPERE: | |
445 | unitDivider = 1000; | |
446 | break; | |
447 | } | |
448 | return unitDivider; | |
449 | } | |
450 | ||
15068be9 JB |
451 | public static getChargingStationConnectorChargingProfilesPowerLimit( |
452 | chargingStation: ChargingStation, | |
453 | connectorId: number | |
454 | ): number | undefined { | |
455 | let limit: number, matchingChargingProfile: ChargingProfile; | |
15068be9 | 456 | // Get charging profiles for connector and sort by stack level |
66a62eac | 457 | const chargingProfiles = |
8df5ae48 JB |
458 | Utils.cloneObject<ChargingProfile[]>( |
459 | chargingStation.getConnectorStatus(connectorId)?.chargingProfiles | |
460 | )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? []; | |
15068be9 | 461 | // Get profiles on connector 0 |
1895299d | 462 | if (chargingStation.getConnectorStatus(0)?.chargingProfiles) { |
15068be9 | 463 | chargingProfiles.push( |
8df5ae48 JB |
464 | ...Utils.cloneObject<ChargingProfile[]>( |
465 | chargingStation.getConnectorStatus(0).chargingProfiles | |
466 | ).sort((a, b) => b.stackLevel - a.stackLevel) | |
15068be9 JB |
467 | ); |
468 | } | |
53ac516c | 469 | if (Utils.isNotEmptyArray(chargingProfiles)) { |
15068be9 JB |
470 | const result = ChargingStationUtils.getLimitFromChargingProfiles( |
471 | chargingProfiles, | |
472 | chargingStation.logPrefix() | |
473 | ); | |
474 | if (!Utils.isNullOrUndefined(result)) { | |
1895299d JB |
475 | limit = result?.limit; |
476 | matchingChargingProfile = result?.matchingChargingProfile; | |
15068be9 JB |
477 | switch (chargingStation.getCurrentOutType()) { |
478 | case CurrentType.AC: | |
479 | limit = | |
480 | matchingChargingProfile.chargingSchedule.chargingRateUnit === | |
481 | ChargingRateUnitType.WATT | |
482 | ? limit | |
483 | : ACElectricUtils.powerTotal( | |
484 | chargingStation.getNumberOfPhases(), | |
485 | chargingStation.getVoltageOut(), | |
486 | limit | |
487 | ); | |
488 | break; | |
489 | case CurrentType.DC: | |
490 | limit = | |
491 | matchingChargingProfile.chargingSchedule.chargingRateUnit === | |
492 | ChargingRateUnitType.WATT | |
493 | ? limit | |
494 | : DCElectricUtils.power(chargingStation.getVoltageOut(), limit); | |
495 | } | |
496 | const connectorMaximumPower = | |
497 | chargingStation.getMaximumPower() / chargingStation.powerDivider; | |
498 | if (limit > connectorMaximumPower) { | |
499 | logger.error( | |
500 | `${chargingStation.logPrefix()} Charging profile id ${ | |
501 | matchingChargingProfile.chargingProfileId | |
502 | } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`, | |
503 | result | |
504 | ); | |
505 | limit = connectorMaximumPower; | |
506 | } | |
507 | } | |
508 | } | |
509 | return limit; | |
510 | } | |
511 | ||
512 | public static getDefaultVoltageOut( | |
513 | currentType: CurrentType, | |
8a133cc8 JB |
514 | logPrefix: string, |
515 | templateFile: string | |
15068be9 | 516 | ): Voltage { |
ded57f02 | 517 | const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`; |
15068be9 JB |
518 | let defaultVoltageOut: number; |
519 | switch (currentType) { | |
520 | case CurrentType.AC: | |
521 | defaultVoltageOut = Voltage.VOLTAGE_230; | |
522 | break; | |
523 | case CurrentType.DC: | |
524 | defaultVoltageOut = Voltage.VOLTAGE_400; | |
525 | break; | |
526 | default: | |
ded57f02 JB |
527 | logger.error(`${logPrefix} ${errorMsg}`); |
528 | throw new BaseError(errorMsg); | |
15068be9 JB |
529 | } |
530 | return defaultVoltageOut; | |
531 | } | |
532 | ||
e302df1d | 533 | public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined { |
15068be9 | 534 | return ( |
e302df1d | 535 | stationInfo.idTagsFile && |
15068be9 | 536 | path.join( |
51022aa0 | 537 | path.dirname(fileURLToPath(import.meta.url)), |
15068be9 | 538 | 'assets', |
e302df1d | 539 | path.basename(stationInfo.idTagsFile) |
15068be9 JB |
540 | ) |
541 | ); | |
542 | } | |
543 | ||
b1f1b0f6 JB |
544 | public static waitForChargingStationEvents = async ( |
545 | emitter: EventEmitter, | |
546 | event: ChargingStationWorkerMessageEvents, | |
547 | eventsToWait: number | |
548 | ): Promise<number> => { | |
549 | return new Promise((resolve) => { | |
550 | let events = 0; | |
551 | if (eventsToWait === 0) { | |
552 | resolve(events); | |
553 | } | |
554 | emitter.on(event, () => { | |
555 | ++events; | |
556 | if (events === eventsToWait) { | |
557 | resolve(events); | |
558 | } | |
559 | }); | |
560 | }); | |
561 | }; | |
562 | ||
8a133cc8 | 563 | private static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number { |
cda5d0fb JB |
564 | let configuredMaxConnectors: number; |
565 | if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) { | |
566 | const numberOfConnectors = stationTemplate.numberOfConnectors as number[]; | |
567 | configuredMaxConnectors = | |
568 | numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)]; | |
569 | } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) { | |
570 | configuredMaxConnectors = stationTemplate.numberOfConnectors as number; | |
571 | } else if (stationTemplate.Connectors && !stationTemplate.Evses) { | |
572 | configuredMaxConnectors = stationTemplate?.Connectors[0] | |
573 | ? ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors) - 1 | |
574 | : ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors); | |
575 | } else if (stationTemplate.Evses && !stationTemplate.Connectors) { | |
576 | configuredMaxConnectors = 0; | |
577 | for (const evse in stationTemplate.Evses) { | |
578 | if (evse === '0') { | |
579 | continue; | |
580 | } | |
581 | configuredMaxConnectors += ChargingStationUtils.getMaxNumberOfConnectors( | |
582 | stationTemplate.Evses[evse].Connectors | |
583 | ); | |
584 | } | |
585 | } | |
586 | return configuredMaxConnectors; | |
587 | } | |
588 | ||
589 | private static checkConfiguredMaxConnectors( | |
590 | configuredMaxConnectors: number, | |
8a133cc8 JB |
591 | logPrefix: string, |
592 | templateFile: string | |
cda5d0fb JB |
593 | ): void { |
594 | if (configuredMaxConnectors <= 0) { | |
595 | logger.warn( | |
596 | `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors` | |
597 | ); | |
598 | } | |
599 | } | |
600 | ||
601 | private static checkTemplateMaxConnectors( | |
602 | templateMaxConnectors: number, | |
8a133cc8 JB |
603 | logPrefix: string, |
604 | templateFile: string | |
cda5d0fb JB |
605 | ): void { |
606 | if (templateMaxConnectors === 0) { | |
607 | logger.warn( | |
608 | `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration` | |
609 | ); | |
610 | } else if (templateMaxConnectors < 0) { | |
611 | logger.error( | |
612 | `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined` | |
613 | ); | |
614 | } | |
615 | } | |
616 | ||
52952bf8 JB |
617 | private static initializeConnectorStatus(connectorStatus: ConnectorStatus): void { |
618 | connectorStatus.availability = AvailabilityType.Operative; | |
619 | connectorStatus.idTagLocalAuthorized = false; | |
620 | connectorStatus.idTagAuthorized = false; | |
621 | connectorStatus.transactionRemoteStarted = false; | |
622 | connectorStatus.transactionStarted = false; | |
623 | connectorStatus.energyActiveImportRegisterValue = 0; | |
624 | connectorStatus.transactionEnergyActiveImportRegisterValue = 0; | |
625 | if (Utils.isUndefined(connectorStatus.chargingProfiles)) { | |
626 | connectorStatus.chargingProfiles = []; | |
627 | } | |
628 | } | |
629 | ||
ae5020a3 JB |
630 | private static warnDeprecatedTemplateKey( |
631 | template: ChargingStationTemplate, | |
632 | key: string, | |
ae5020a3 | 633 | logPrefix: string, |
8a133cc8 | 634 | templateFile: string, |
ae5020a3 JB |
635 | logMsgToAppend = '' |
636 | ): void { | |
637 | if (!Utils.isUndefined(template[key])) { | |
638 | const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${ | |
639 | Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : '' | |
640 | }`; | |
641 | logger.warn(`${logPrefix} ${logMsg}`); | |
642 | console.warn(chalk.yellow(`${logMsg}`)); | |
643 | } | |
644 | } | |
645 | ||
646 | private static convertDeprecatedTemplateKey( | |
647 | template: ChargingStationTemplate, | |
648 | deprecatedKey: string, | |
649 | key: string | |
650 | ): void { | |
651 | if (!Utils.isUndefined(template[deprecatedKey])) { | |
652 | template[key] = template[deprecatedKey] as unknown; | |
653 | delete template[deprecatedKey]; | |
654 | } | |
655 | } | |
656 | ||
17ac262c | 657 | /** |
54ebb82c | 658 | * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority) |
17ac262c | 659 | * |
0e4fa348 JB |
660 | * @param chargingProfiles - |
661 | * @param logPrefix - | |
662 | * @returns | |
17ac262c | 663 | */ |
15068be9 | 664 | private static getLimitFromChargingProfiles( |
17ac262c JB |
665 | chargingProfiles: ChargingProfile[], |
666 | logPrefix: string | |
667 | ): { | |
668 | limit: number; | |
669 | matchingChargingProfile: ChargingProfile; | |
670 | } | null { | |
91a4f151 | 671 | const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`; |
41189456 JB |
672 | const currentMoment = moment(); |
673 | const currentDate = new Date(); | |
17ac262c JB |
674 | for (const chargingProfile of chargingProfiles) { |
675 | // Set helpers | |
17ac262c | 676 | const chargingSchedule = chargingProfile.chargingSchedule; |
41189456 JB |
677 | if (!chargingSchedule?.startSchedule) { |
678 | logger.warn( | |
679 | `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}` | |
680 | ); | |
681 | } | |
17ac262c JB |
682 | // Check type (recurring) and if it is already active |
683 | // Adjust the daily recurring schedule to today | |
684 | if ( | |
685 | chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING && | |
686 | chargingProfile.recurrencyKind === RecurrencyKindType.DAILY && | |
687 | currentMoment.isAfter(chargingSchedule.startSchedule) | |
688 | ) { | |
41189456 JB |
689 | if (!(chargingSchedule?.startSchedule instanceof Date)) { |
690 | logger.warn( | |
691 | `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object` | |
692 | ); | |
693 | chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule); | |
694 | } | |
17ac262c JB |
695 | chargingSchedule.startSchedule.setFullYear( |
696 | currentDate.getFullYear(), | |
697 | currentDate.getMonth(), | |
698 | currentDate.getDate() | |
699 | ); | |
700 | // Check if the start of the schedule is yesterday | |
701 | if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) { | |
702 | chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1); | |
703 | } | |
704 | } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) { | |
705 | return null; | |
706 | } | |
707 | // Check if the charging profile is active | |
708 | if ( | |
709 | moment(chargingSchedule.startSchedule) | |
710 | .add(chargingSchedule.duration, 's') | |
711 | .isAfter(currentMoment) | |
712 | ) { | |
713 | let lastButOneSchedule: ChargingSchedulePeriod; | |
714 | // Search the right schedule period | |
715 | for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) { | |
716 | // Handling of only one period | |
717 | if ( | |
718 | chargingSchedule.chargingSchedulePeriod.length === 1 && | |
719 | schedulePeriod.startPeriod === 0 | |
720 | ) { | |
721 | const result = { | |
722 | limit: schedulePeriod.limit, | |
723 | matchingChargingProfile: chargingProfile, | |
724 | }; | |
91a4f151 | 725 | logger.debug(debugLogMsg, result); |
17ac262c JB |
726 | return result; |
727 | } | |
728 | // Find the right schedule period | |
729 | if ( | |
730 | moment(chargingSchedule.startSchedule) | |
731 | .add(schedulePeriod.startPeriod, 's') | |
732 | .isAfter(currentMoment) | |
733 | ) { | |
734 | // Found the schedule: last but one is the correct one | |
735 | const result = { | |
736 | limit: lastButOneSchedule.limit, | |
737 | matchingChargingProfile: chargingProfile, | |
738 | }; | |
91a4f151 | 739 | logger.debug(debugLogMsg, result); |
17ac262c JB |
740 | return result; |
741 | } | |
742 | // Keep it | |
743 | lastButOneSchedule = schedulePeriod; | |
744 | // Handle the last schedule period | |
745 | if ( | |
746 | schedulePeriod.startPeriod === | |
747 | chargingSchedule.chargingSchedulePeriod[ | |
748 | chargingSchedule.chargingSchedulePeriod.length - 1 | |
749 | ].startPeriod | |
750 | ) { | |
751 | const result = { | |
752 | limit: lastButOneSchedule.limit, | |
753 | matchingChargingProfile: chargingProfile, | |
754 | }; | |
91a4f151 | 755 | logger.debug(debugLogMsg, result); |
17ac262c JB |
756 | return result; |
757 | } | |
758 | } | |
759 | } | |
760 | } | |
761 | return null; | |
762 | } | |
763 | ||
764 | private static getRandomSerialNumberSuffix(params?: { | |
765 | randomBytesLength?: number; | |
766 | upperCase?: boolean; | |
767 | }): string { | |
768 | const randomSerialNumberSuffix = crypto | |
769 | .randomBytes(params?.randomBytesLength ?? 16) | |
770 | .toString('hex'); | |
771 | if (params?.upperCase) { | |
772 | return randomSerialNumberSuffix.toUpperCase(); | |
773 | } | |
774 | return randomSerialNumberSuffix; | |
775 | } | |
776 | } |