Commit | Line | Data |
---|---|---|
d972af76 JB |
1 | import { readFileSync } from 'node:fs'; |
2 | import { dirname, join } from 'node:path'; | |
51022aa0 | 3 | import { fileURLToPath } from 'node:url'; |
7164966d JB |
4 | |
5 | import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv'; | |
3dcf7b67 | 6 | import { isDate } from 'date-fns'; |
06ad945f | 7 | |
4c3c0d59 JB |
8 | import { OCPP16Constants } from './1.6/OCPP16Constants'; |
9 | import { OCPP20Constants } from './2.0/OCPP20Constants'; | |
c3da35d4 | 10 | import { OCPPConstants } from './OCPPConstants'; |
ae725be3 | 11 | import { type ChargingStation, getConfigurationKey, getIdTagsFile } from '../../charging-station'; |
41f3983a | 12 | import { BaseError, OCPPError } from '../../exception'; |
6e939d9e | 13 | import { |
ae725be3 JB |
14 | AuthorizationStatus, |
15 | type AuthorizeRequest, | |
16 | type AuthorizeResponse, | |
268a74bb | 17 | ChargePointErrorCode, |
3e888c65 | 18 | ChargingStationEvents, |
ae725be3 | 19 | type ConnectorStatus, |
268a74bb | 20 | type ConnectorStatusEnum, |
41f3983a | 21 | CurrentType, |
268a74bb JB |
22 | ErrorType, |
23 | FileType, | |
6e939d9e | 24 | IncomingRequestCommand, |
268a74bb | 25 | type JsonType, |
41f3983a JB |
26 | type MeasurandPerPhaseSampledValueTemplates, |
27 | type MeasurandValues, | |
6e939d9e | 28 | MessageTrigger, |
268a74bb | 29 | MessageType, |
41f3983a JB |
30 | type MeterValue, |
31 | MeterValueContext, | |
32 | MeterValueLocation, | |
268a74bb | 33 | MeterValueMeasurand, |
41f3983a JB |
34 | MeterValuePhase, |
35 | MeterValueUnit, | |
268a74bb JB |
36 | type OCPP16StatusNotificationRequest, |
37 | type OCPP20StatusNotificationRequest, | |
38 | OCPPVersion, | |
6e939d9e | 39 | RequestCommand, |
41f3983a | 40 | type SampledValue, |
268a74bb JB |
41 | type SampledValueTemplate, |
42 | StandardParametersKey, | |
6e939d9e | 43 | type StatusNotificationRequest, |
48b75072 | 44 | type StatusNotificationResponse, |
268a74bb | 45 | } from '../../types'; |
9bf0ef23 | 46 | import { |
41f3983a JB |
47 | ACElectricUtils, |
48 | Constants, | |
49 | DCElectricUtils, | |
50 | convertToFloat, | |
51 | convertToInt, | |
52 | getRandomFloatFluctuatedRounded, | |
53 | getRandomFloatRounded, | |
54 | getRandomInteger, | |
9bf0ef23 JB |
55 | handleFileException, |
56 | isNotEmptyArray, | |
57 | isNotEmptyString, | |
41f3983a JB |
58 | isNullOrUndefined, |
59 | isUndefined, | |
9bf0ef23 JB |
60 | logPrefix, |
61 | logger, | |
d71ce3fa | 62 | max, |
5adf6ca4 | 63 | min, |
41f3983a | 64 | roundTo, |
9bf0ef23 | 65 | } from '../../utils'; |
06ad945f | 66 | |
041365be JB |
67 | export const getMessageTypeString = (messageType: MessageType): string => { |
68 | switch (messageType) { | |
69 | case MessageType.CALL_MESSAGE: | |
70 | return 'request'; | |
71 | case MessageType.CALL_RESULT_MESSAGE: | |
72 | return 'response'; | |
73 | case MessageType.CALL_ERROR_MESSAGE: | |
74 | return 'error'; | |
75 | default: | |
76 | return 'unknown'; | |
77 | } | |
78 | }; | |
79 | ||
80 | export const buildStatusNotificationRequest = ( | |
81 | chargingStation: ChargingStation, | |
82 | connectorId: number, | |
83 | status: ConnectorStatusEnum, | |
84 | evseId?: number, | |
85 | ): StatusNotificationRequest => { | |
86 | switch (chargingStation.stationInfo?.ocppVersion) { | |
87 | case OCPPVersion.VERSION_16: | |
88 | return { | |
89 | connectorId, | |
90 | status, | |
91 | errorCode: ChargePointErrorCode.NO_ERROR, | |
92 | } as OCPP16StatusNotificationRequest; | |
93 | case OCPPVersion.VERSION_20: | |
94 | case OCPPVersion.VERSION_201: | |
95 | return { | |
96 | timestamp: new Date(), | |
97 | connectorStatus: status, | |
98 | connectorId, | |
99 | evseId, | |
100 | } as OCPP20StatusNotificationRequest; | |
101 | default: | |
102 | throw new BaseError('Cannot build status notification payload: OCPP version not supported'); | |
103 | } | |
104 | }; | |
105 | ||
106 | export const isIdTagAuthorized = async ( | |
107 | chargingStation: ChargingStation, | |
108 | connectorId: number, | |
109 | idTag: string, | |
110 | ): Promise<boolean> => { | |
111 | if ( | |
112 | !chargingStation.getLocalAuthListEnabled() && | |
113 | !chargingStation.stationInfo?.remoteAuthorization | |
114 | ) { | |
115 | logger.warn( | |
116 | `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`, | |
117 | ); | |
118 | } | |
119 | if ( | |
120 | chargingStation.getLocalAuthListEnabled() === true && | |
121 | isIdTagLocalAuthorized(chargingStation, idTag) | |
122 | ) { | |
123 | const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!; | |
124 | connectorStatus.localAuthorizeIdTag = idTag; | |
125 | connectorStatus.idTagLocalAuthorized = true; | |
126 | return true; | |
127 | } else if (chargingStation.stationInfo?.remoteAuthorization) { | |
128 | return await isIdTagRemoteAuthorized(chargingStation, connectorId, idTag); | |
129 | } | |
130 | return false; | |
131 | }; | |
132 | ||
133 | const isIdTagLocalAuthorized = (chargingStation: ChargingStation, idTag: string): boolean => { | |
134 | return ( | |
135 | chargingStation.hasIdTags() === true && | |
136 | isNotEmptyString( | |
137 | chargingStation.idTagsCache | |
138 | .getIdTags(getIdTagsFile(chargingStation.stationInfo)!) | |
139 | ?.find((tag) => tag === idTag), | |
140 | ) | |
141 | ); | |
142 | }; | |
143 | ||
144 | const isIdTagRemoteAuthorized = async ( | |
145 | chargingStation: ChargingStation, | |
146 | connectorId: number, | |
147 | idTag: string, | |
148 | ): Promise<boolean> => { | |
149 | chargingStation.getConnectorStatus(connectorId)!.authorizeIdTag = idTag; | |
150 | return ( | |
151 | ( | |
152 | await chargingStation.ocppRequestService.requestHandler<AuthorizeRequest, AuthorizeResponse>( | |
153 | chargingStation, | |
154 | RequestCommand.AUTHORIZE, | |
155 | { | |
156 | idTag, | |
157 | }, | |
158 | ) | |
159 | )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED | |
160 | ); | |
161 | }; | |
162 | ||
163 | export const sendAndSetConnectorStatus = async ( | |
164 | chargingStation: ChargingStation, | |
165 | connectorId: number, | |
166 | status: ConnectorStatusEnum, | |
167 | evseId?: number, | |
168 | options?: { send: boolean }, | |
169 | ): Promise<void> => { | |
170 | options = { send: true, ...options }; | |
171 | if (options.send) { | |
172 | checkConnectorStatusTransition(chargingStation, connectorId, status); | |
173 | await chargingStation.ocppRequestService.requestHandler< | |
174 | StatusNotificationRequest, | |
175 | StatusNotificationResponse | |
176 | >( | |
177 | chargingStation, | |
178 | RequestCommand.STATUS_NOTIFICATION, | |
179 | buildStatusNotificationRequest(chargingStation, connectorId, status, evseId), | |
180 | ); | |
181 | } | |
182 | chargingStation.getConnectorStatus(connectorId)!.status = status; | |
183 | chargingStation.emit(ChargingStationEvents.connectorStatusChanged, { | |
184 | connectorId, | |
185 | ...chargingStation.getConnectorStatus(connectorId), | |
186 | }); | |
187 | }; | |
188 | ||
189 | const checkConnectorStatusTransition = ( | |
190 | chargingStation: ChargingStation, | |
191 | connectorId: number, | |
192 | status: ConnectorStatusEnum, | |
193 | ): boolean => { | |
194 | const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status; | |
195 | let transitionAllowed = false; | |
196 | switch (chargingStation.stationInfo?.ocppVersion) { | |
197 | case OCPPVersion.VERSION_16: | |
198 | if ( | |
199 | (connectorId === 0 && | |
200 | OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex( | |
201 | (transition) => transition.from === fromStatus && transition.to === status, | |
202 | ) !== -1) || | |
203 | (connectorId > 0 && | |
204 | OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex( | |
205 | (transition) => transition.from === fromStatus && transition.to === status, | |
206 | ) !== -1) | |
207 | ) { | |
208 | transitionAllowed = true; | |
209 | } | |
210 | break; | |
211 | case OCPPVersion.VERSION_20: | |
212 | case OCPPVersion.VERSION_201: | |
213 | if ( | |
214 | (connectorId === 0 && | |
215 | OCPP20Constants.ChargingStationStatusTransitions.findIndex( | |
216 | (transition) => transition.from === fromStatus && transition.to === status, | |
217 | ) !== -1) || | |
218 | (connectorId > 0 && | |
219 | OCPP20Constants.ConnectorStatusTransitions.findIndex( | |
220 | (transition) => transition.from === fromStatus && transition.to === status, | |
221 | ) !== -1) | |
222 | ) { | |
223 | transitionAllowed = true; | |
224 | } | |
225 | break; | |
226 | default: | |
227 | throw new BaseError( | |
228 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions | |
229 | `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`, | |
230 | ); | |
231 | } | |
232 | if (transitionAllowed === false) { | |
233 | logger.warn( | |
234 | `${chargingStation.logPrefix()} OCPP ${chargingStation.stationInfo | |
235 | ?.ocppVersion} connector id ${connectorId} status transition from '${ | |
236 | chargingStation.getConnectorStatus(connectorId)!.status | |
237 | }' to '${status}' is not allowed`, | |
238 | ); | |
239 | } | |
240 | return transitionAllowed; | |
241 | }; | |
242 | ||
41f3983a JB |
243 | export const buildMeterValue = ( |
244 | chargingStation: ChargingStation, | |
245 | connectorId: number, | |
246 | transactionId: number, | |
247 | interval: number, | |
248 | debug = false, | |
249 | ): MeterValue => { | |
250 | const connector = chargingStation.getConnectorStatus(connectorId); | |
251 | let meterValue: MeterValue; | |
252 | let socSampledValueTemplate: SampledValueTemplate | undefined; | |
253 | let voltageSampledValueTemplate: SampledValueTemplate | undefined; | |
254 | let powerSampledValueTemplate: SampledValueTemplate | undefined; | |
255 | let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {}; | |
256 | let currentSampledValueTemplate: SampledValueTemplate | undefined; | |
257 | let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {}; | |
258 | let energySampledValueTemplate: SampledValueTemplate | undefined; | |
259 | switch (chargingStation.stationInfo?.ocppVersion) { | |
260 | case OCPPVersion.VERSION_16: | |
261 | meterValue = { | |
262 | timestamp: new Date(), | |
263 | sampledValue: [], | |
264 | }; | |
265 | // SoC measurand | |
266 | socSampledValueTemplate = getSampledValueTemplate( | |
267 | chargingStation, | |
268 | connectorId, | |
269 | MeterValueMeasurand.STATE_OF_CHARGE, | |
270 | ); | |
271 | if (socSampledValueTemplate) { | |
272 | const socMaximumValue = 100; | |
273 | const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0; | |
274 | const socSampledValueTemplateValue = isNotEmptyString(socSampledValueTemplate.value) | |
275 | ? getRandomFloatFluctuatedRounded( | |
276 | parseInt(socSampledValueTemplate.value), | |
277 | socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT, | |
278 | ) | |
279 | : getRandomInteger(socMaximumValue, socMinimumValue); | |
280 | meterValue.sampledValue.push( | |
281 | buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue), | |
282 | ); | |
283 | const sampledValuesIndex = meterValue.sampledValue.length - 1; | |
284 | if ( | |
285 | convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue || | |
286 | convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue || | |
287 | debug | |
288 | ) { | |
289 | logger.error( | |
290 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
291 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
292 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
293 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${socMinimumValue}/${ | |
294 | meterValue.sampledValue[sampledValuesIndex].value | |
295 | }/${socMaximumValue}`, | |
296 | ); | |
297 | } | |
298 | } | |
299 | // Voltage measurand | |
300 | voltageSampledValueTemplate = getSampledValueTemplate( | |
301 | chargingStation, | |
302 | connectorId, | |
303 | MeterValueMeasurand.VOLTAGE, | |
304 | ); | |
305 | if (voltageSampledValueTemplate) { | |
306 | const voltageSampledValueTemplateValue = isNotEmptyString(voltageSampledValueTemplate.value) | |
307 | ? parseInt(voltageSampledValueTemplate.value) | |
308 | : chargingStation.stationInfo.voltageOut!; | |
309 | const fluctuationPercent = | |
310 | voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT; | |
311 | const voltageMeasurandValue = getRandomFloatFluctuatedRounded( | |
312 | voltageSampledValueTemplateValue, | |
313 | fluctuationPercent, | |
314 | ); | |
315 | if ( | |
316 | chargingStation.getNumberOfPhases() !== 3 || | |
317 | (chargingStation.getNumberOfPhases() === 3 && | |
318 | chargingStation.stationInfo?.mainVoltageMeterValues) | |
319 | ) { | |
320 | meterValue.sampledValue.push( | |
321 | buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue), | |
322 | ); | |
323 | } | |
324 | for ( | |
325 | let phase = 1; | |
326 | chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); | |
327 | phase++ | |
328 | ) { | |
329 | const phaseLineToNeutralValue = `L${phase}-N`; | |
330 | const voltagePhaseLineToNeutralSampledValueTemplate = getSampledValueTemplate( | |
331 | chargingStation, | |
332 | connectorId, | |
333 | MeterValueMeasurand.VOLTAGE, | |
334 | phaseLineToNeutralValue as MeterValuePhase, | |
335 | ); | |
336 | let voltagePhaseLineToNeutralMeasurandValue: number | undefined; | |
337 | if (voltagePhaseLineToNeutralSampledValueTemplate) { | |
338 | const voltagePhaseLineToNeutralSampledValueTemplateValue = isNotEmptyString( | |
339 | voltagePhaseLineToNeutralSampledValueTemplate.value, | |
340 | ) | |
341 | ? parseInt(voltagePhaseLineToNeutralSampledValueTemplate.value) | |
342 | : chargingStation.stationInfo.voltageOut!; | |
343 | const fluctuationPhaseToNeutralPercent = | |
344 | voltagePhaseLineToNeutralSampledValueTemplate.fluctuationPercent ?? | |
345 | Constants.DEFAULT_FLUCTUATION_PERCENT; | |
346 | voltagePhaseLineToNeutralMeasurandValue = getRandomFloatFluctuatedRounded( | |
347 | voltagePhaseLineToNeutralSampledValueTemplateValue, | |
348 | fluctuationPhaseToNeutralPercent, | |
349 | ); | |
350 | } | |
351 | meterValue.sampledValue.push( | |
352 | buildSampledValue( | |
353 | voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate, | |
354 | voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue, | |
355 | undefined, | |
356 | phaseLineToNeutralValue as MeterValuePhase, | |
357 | ), | |
358 | ); | |
359 | if (chargingStation.stationInfo?.phaseLineToLineVoltageMeterValues) { | |
360 | const phaseLineToLineValue = `L${phase}-L${ | |
361 | (phase + 1) % chargingStation.getNumberOfPhases() !== 0 | |
362 | ? (phase + 1) % chargingStation.getNumberOfPhases() | |
363 | : chargingStation.getNumberOfPhases() | |
364 | }`; | |
365 | const voltagePhaseLineToLineValueRounded = roundTo( | |
366 | Math.sqrt(chargingStation.getNumberOfPhases()) * | |
367 | chargingStation.stationInfo.voltageOut!, | |
368 | 2, | |
369 | ); | |
370 | const voltagePhaseLineToLineSampledValueTemplate = getSampledValueTemplate( | |
371 | chargingStation, | |
372 | connectorId, | |
373 | MeterValueMeasurand.VOLTAGE, | |
374 | phaseLineToLineValue as MeterValuePhase, | |
375 | ); | |
376 | let voltagePhaseLineToLineMeasurandValue: number | undefined; | |
377 | if (voltagePhaseLineToLineSampledValueTemplate) { | |
378 | const voltagePhaseLineToLineSampledValueTemplateValue = isNotEmptyString( | |
379 | voltagePhaseLineToLineSampledValueTemplate.value, | |
380 | ) | |
381 | ? parseInt(voltagePhaseLineToLineSampledValueTemplate.value) | |
382 | : voltagePhaseLineToLineValueRounded; | |
383 | const fluctuationPhaseLineToLinePercent = | |
384 | voltagePhaseLineToLineSampledValueTemplate.fluctuationPercent ?? | |
385 | Constants.DEFAULT_FLUCTUATION_PERCENT; | |
386 | voltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded( | |
387 | voltagePhaseLineToLineSampledValueTemplateValue, | |
388 | fluctuationPhaseLineToLinePercent, | |
389 | ); | |
390 | } | |
391 | const defaultVoltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded( | |
392 | voltagePhaseLineToLineValueRounded, | |
393 | fluctuationPercent, | |
394 | ); | |
395 | meterValue.sampledValue.push( | |
396 | buildSampledValue( | |
397 | voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate, | |
398 | voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue, | |
399 | undefined, | |
400 | phaseLineToLineValue as MeterValuePhase, | |
401 | ), | |
402 | ); | |
403 | } | |
404 | } | |
405 | } | |
406 | // Power.Active.Import measurand | |
407 | powerSampledValueTemplate = getSampledValueTemplate( | |
408 | chargingStation, | |
409 | connectorId, | |
410 | MeterValueMeasurand.POWER_ACTIVE_IMPORT, | |
411 | ); | |
412 | if (chargingStation.getNumberOfPhases() === 3) { | |
413 | powerPerPhaseSampledValueTemplates = { | |
414 | L1: getSampledValueTemplate( | |
415 | chargingStation, | |
416 | connectorId, | |
417 | MeterValueMeasurand.POWER_ACTIVE_IMPORT, | |
418 | MeterValuePhase.L1_N, | |
419 | ), | |
420 | L2: getSampledValueTemplate( | |
421 | chargingStation, | |
422 | connectorId, | |
423 | MeterValueMeasurand.POWER_ACTIVE_IMPORT, | |
424 | MeterValuePhase.L2_N, | |
425 | ), | |
426 | L3: getSampledValueTemplate( | |
427 | chargingStation, | |
428 | connectorId, | |
429 | MeterValueMeasurand.POWER_ACTIVE_IMPORT, | |
430 | MeterValuePhase.L3_N, | |
431 | ), | |
432 | }; | |
433 | } | |
434 | if (powerSampledValueTemplate) { | |
435 | checkMeasurandPowerDivider(chargingStation, powerSampledValueTemplate.measurand!); | |
436 | const errMsg = `MeterValues measurand ${ | |
437 | powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
438 | }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${ | |
439 | chargingStation.templateFile | |
440 | }, cannot calculate ${ | |
441 | powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
442 | } measurand value`; | |
443 | const powerMeasurandValues: MeasurandValues = {} as MeasurandValues; | |
444 | const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1; | |
445 | const connectorMaximumAvailablePower = | |
446 | chargingStation.getConnectorMaximumAvailablePower(connectorId); | |
447 | const connectorMaximumPower = Math.round(connectorMaximumAvailablePower); | |
448 | const connectorMaximumPowerPerPhase = Math.round( | |
449 | connectorMaximumAvailablePower / chargingStation.getNumberOfPhases(), | |
450 | ); | |
451 | const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue ?? 0); | |
452 | const connectorMinimumPowerPerPhase = Math.round( | |
453 | connectorMinimumPower / chargingStation.getNumberOfPhases(), | |
454 | ); | |
455 | switch (chargingStation.stationInfo?.currentOutType) { | |
456 | case CurrentType.AC: | |
457 | if (chargingStation.getNumberOfPhases() === 3) { | |
458 | const defaultFluctuatedPowerPerPhase = isNotEmptyString( | |
459 | powerSampledValueTemplate.value, | |
460 | ) | |
461 | ? getRandomFloatFluctuatedRounded( | |
462 | getLimitFromSampledValueTemplateCustomValue( | |
463 | powerSampledValueTemplate.value, | |
464 | connectorMaximumPower / unitDivider, | |
465 | connectorMinimumPower / unitDivider, | |
466 | { | |
467 | limitationEnabled: | |
468 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
469 | fallbackValue: connectorMinimumPower / unitDivider, | |
470 | }, | |
471 | ) / chargingStation.getNumberOfPhases(), | |
472 | powerSampledValueTemplate.fluctuationPercent ?? | |
473 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
474 | ) | |
475 | : undefined; | |
476 | const phase1FluctuatedValue = isNotEmptyString( | |
477 | powerPerPhaseSampledValueTemplates.L1?.value, | |
478 | ) | |
479 | ? getRandomFloatFluctuatedRounded( | |
480 | getLimitFromSampledValueTemplateCustomValue( | |
481 | powerPerPhaseSampledValueTemplates.L1?.value, | |
482 | connectorMaximumPowerPerPhase / unitDivider, | |
483 | connectorMinimumPowerPerPhase / unitDivider, | |
484 | { | |
485 | limitationEnabled: | |
486 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
487 | fallbackValue: connectorMinimumPowerPerPhase / unitDivider, | |
488 | }, | |
489 | ), | |
490 | powerPerPhaseSampledValueTemplates.L1?.fluctuationPercent ?? | |
491 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
492 | ) | |
493 | : undefined; | |
494 | const phase2FluctuatedValue = isNotEmptyString( | |
495 | powerPerPhaseSampledValueTemplates.L2?.value, | |
496 | ) | |
497 | ? getRandomFloatFluctuatedRounded( | |
498 | getLimitFromSampledValueTemplateCustomValue( | |
499 | powerPerPhaseSampledValueTemplates.L2?.value, | |
500 | connectorMaximumPowerPerPhase / unitDivider, | |
501 | connectorMinimumPowerPerPhase / unitDivider, | |
502 | { | |
503 | limitationEnabled: | |
504 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
505 | fallbackValue: connectorMinimumPowerPerPhase / unitDivider, | |
506 | }, | |
507 | ), | |
508 | powerPerPhaseSampledValueTemplates.L2?.fluctuationPercent ?? | |
509 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
510 | ) | |
511 | : undefined; | |
512 | const phase3FluctuatedValue = isNotEmptyString( | |
513 | powerPerPhaseSampledValueTemplates.L3?.value, | |
514 | ) | |
515 | ? getRandomFloatFluctuatedRounded( | |
516 | getLimitFromSampledValueTemplateCustomValue( | |
517 | powerPerPhaseSampledValueTemplates.L3?.value, | |
518 | connectorMaximumPowerPerPhase / unitDivider, | |
519 | connectorMinimumPowerPerPhase / unitDivider, | |
520 | { | |
521 | limitationEnabled: | |
522 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
523 | fallbackValue: connectorMinimumPowerPerPhase / unitDivider, | |
524 | }, | |
525 | ), | |
526 | powerPerPhaseSampledValueTemplates.L3?.fluctuationPercent ?? | |
527 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
528 | ) | |
529 | : undefined; | |
530 | powerMeasurandValues.L1 = | |
531 | phase1FluctuatedValue ?? | |
532 | defaultFluctuatedPowerPerPhase ?? | |
533 | getRandomFloatRounded( | |
534 | connectorMaximumPowerPerPhase / unitDivider, | |
535 | connectorMinimumPowerPerPhase / unitDivider, | |
536 | ); | |
537 | powerMeasurandValues.L2 = | |
538 | phase2FluctuatedValue ?? | |
539 | defaultFluctuatedPowerPerPhase ?? | |
540 | getRandomFloatRounded( | |
541 | connectorMaximumPowerPerPhase / unitDivider, | |
542 | connectorMinimumPowerPerPhase / unitDivider, | |
543 | ); | |
544 | powerMeasurandValues.L3 = | |
545 | phase3FluctuatedValue ?? | |
546 | defaultFluctuatedPowerPerPhase ?? | |
547 | getRandomFloatRounded( | |
548 | connectorMaximumPowerPerPhase / unitDivider, | |
549 | connectorMinimumPowerPerPhase / unitDivider, | |
550 | ); | |
551 | } else { | |
552 | powerMeasurandValues.L1 = isNotEmptyString(powerSampledValueTemplate.value) | |
553 | ? getRandomFloatFluctuatedRounded( | |
554 | getLimitFromSampledValueTemplateCustomValue( | |
555 | powerSampledValueTemplate.value, | |
556 | connectorMaximumPower / unitDivider, | |
557 | connectorMinimumPower / unitDivider, | |
558 | { | |
559 | limitationEnabled: | |
560 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
561 | fallbackValue: connectorMinimumPower / unitDivider, | |
562 | }, | |
563 | ), | |
564 | powerSampledValueTemplate.fluctuationPercent ?? | |
565 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
566 | ) | |
567 | : getRandomFloatRounded( | |
568 | connectorMaximumPower / unitDivider, | |
569 | connectorMinimumPower / unitDivider, | |
570 | ); | |
571 | powerMeasurandValues.L2 = 0; | |
572 | powerMeasurandValues.L3 = 0; | |
573 | } | |
574 | powerMeasurandValues.allPhases = roundTo( | |
575 | powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3, | |
576 | 2, | |
577 | ); | |
578 | break; | |
579 | case CurrentType.DC: | |
580 | powerMeasurandValues.allPhases = isNotEmptyString(powerSampledValueTemplate.value) | |
581 | ? getRandomFloatFluctuatedRounded( | |
582 | getLimitFromSampledValueTemplateCustomValue( | |
583 | powerSampledValueTemplate.value, | |
584 | connectorMaximumPower / unitDivider, | |
585 | connectorMinimumPower / unitDivider, | |
586 | { | |
587 | limitationEnabled: | |
588 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
589 | fallbackValue: connectorMinimumPower / unitDivider, | |
590 | }, | |
591 | ), | |
592 | powerSampledValueTemplate.fluctuationPercent ?? | |
593 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
594 | ) | |
595 | : getRandomFloatRounded( | |
596 | connectorMaximumPower / unitDivider, | |
597 | connectorMinimumPower / unitDivider, | |
598 | ); | |
599 | break; | |
600 | default: | |
601 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`); | |
602 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES); | |
603 | } | |
604 | meterValue.sampledValue.push( | |
605 | buildSampledValue(powerSampledValueTemplate, powerMeasurandValues.allPhases), | |
606 | ); | |
607 | const sampledValuesIndex = meterValue.sampledValue.length - 1; | |
608 | const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2); | |
609 | const connectorMinimumPowerRounded = roundTo(connectorMinimumPower / unitDivider, 2); | |
610 | if ( | |
611 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > | |
612 | connectorMaximumPowerRounded || | |
613 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) < | |
614 | connectorMinimumPowerRounded || | |
615 | debug | |
616 | ) { | |
617 | logger.error( | |
618 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
619 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
620 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
621 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerRounded}/${ | |
622 | meterValue.sampledValue[sampledValuesIndex].value | |
623 | }/${connectorMaximumPowerRounded}`, | |
624 | ); | |
625 | } | |
626 | for ( | |
627 | let phase = 1; | |
628 | chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); | |
629 | phase++ | |
630 | ) { | |
631 | const phaseValue = `L${phase}-N`; | |
632 | meterValue.sampledValue.push( | |
633 | buildSampledValue( | |
634 | powerPerPhaseSampledValueTemplates[ | |
635 | `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates | |
636 | ] ?? powerSampledValueTemplate, | |
637 | powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates], | |
638 | undefined, | |
639 | phaseValue as MeterValuePhase, | |
640 | ), | |
641 | ); | |
642 | const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1; | |
643 | const connectorMaximumPowerPerPhaseRounded = roundTo( | |
644 | connectorMaximumPowerPerPhase / unitDivider, | |
645 | 2, | |
646 | ); | |
647 | const connectorMinimumPowerPerPhaseRounded = roundTo( | |
648 | connectorMinimumPowerPerPhase / unitDivider, | |
649 | 2, | |
650 | ); | |
651 | if ( | |
652 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > | |
653 | connectorMaximumPowerPerPhaseRounded || | |
654 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) < | |
655 | connectorMinimumPowerPerPhaseRounded || | |
656 | debug | |
657 | ) { | |
658 | logger.error( | |
659 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
660 | meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ?? | |
661 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
662 | }: phase ${ | |
663 | meterValue.sampledValue[sampledValuesPerPhaseIndex].phase | |
664 | }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${ | |
665 | meterValue.sampledValue[sampledValuesPerPhaseIndex].value | |
666 | }/${connectorMaximumPowerPerPhaseRounded}`, | |
667 | ); | |
668 | } | |
669 | } | |
670 | } | |
671 | // Current.Import measurand | |
672 | currentSampledValueTemplate = getSampledValueTemplate( | |
673 | chargingStation, | |
674 | connectorId, | |
675 | MeterValueMeasurand.CURRENT_IMPORT, | |
676 | ); | |
677 | if (chargingStation.getNumberOfPhases() === 3) { | |
678 | currentPerPhaseSampledValueTemplates = { | |
679 | L1: getSampledValueTemplate( | |
680 | chargingStation, | |
681 | connectorId, | |
682 | MeterValueMeasurand.CURRENT_IMPORT, | |
683 | MeterValuePhase.L1, | |
684 | ), | |
685 | L2: getSampledValueTemplate( | |
686 | chargingStation, | |
687 | connectorId, | |
688 | MeterValueMeasurand.CURRENT_IMPORT, | |
689 | MeterValuePhase.L2, | |
690 | ), | |
691 | L3: getSampledValueTemplate( | |
692 | chargingStation, | |
693 | connectorId, | |
694 | MeterValueMeasurand.CURRENT_IMPORT, | |
695 | MeterValuePhase.L3, | |
696 | ), | |
697 | }; | |
698 | } | |
699 | if (currentSampledValueTemplate) { | |
700 | checkMeasurandPowerDivider(chargingStation, currentSampledValueTemplate.measurand!); | |
701 | const errMsg = `MeterValues measurand ${ | |
702 | currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
703 | }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${ | |
704 | chargingStation.templateFile | |
705 | }, cannot calculate ${ | |
706 | currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
707 | } measurand value`; | |
708 | const currentMeasurandValues: MeasurandValues = {} as MeasurandValues; | |
709 | const connectorMaximumAvailablePower = | |
710 | chargingStation.getConnectorMaximumAvailablePower(connectorId); | |
711 | const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0; | |
712 | let connectorMaximumAmperage: number; | |
713 | switch (chargingStation.stationInfo?.currentOutType) { | |
714 | case CurrentType.AC: | |
715 | connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower( | |
716 | chargingStation.getNumberOfPhases(), | |
717 | connectorMaximumAvailablePower, | |
718 | chargingStation.stationInfo.voltageOut!, | |
719 | ); | |
720 | if (chargingStation.getNumberOfPhases() === 3) { | |
721 | const defaultFluctuatedAmperagePerPhase = isNotEmptyString( | |
722 | currentSampledValueTemplate.value, | |
723 | ) | |
724 | ? getRandomFloatFluctuatedRounded( | |
725 | getLimitFromSampledValueTemplateCustomValue( | |
726 | currentSampledValueTemplate.value, | |
727 | connectorMaximumAmperage, | |
728 | connectorMinimumAmperage, | |
729 | { | |
730 | limitationEnabled: | |
731 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
732 | fallbackValue: connectorMinimumAmperage, | |
733 | }, | |
734 | ), | |
735 | currentSampledValueTemplate.fluctuationPercent ?? | |
736 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
737 | ) | |
738 | : undefined; | |
739 | const phase1FluctuatedValue = isNotEmptyString( | |
740 | currentPerPhaseSampledValueTemplates.L1?.value, | |
741 | ) | |
742 | ? getRandomFloatFluctuatedRounded( | |
743 | getLimitFromSampledValueTemplateCustomValue( | |
744 | currentPerPhaseSampledValueTemplates.L1?.value, | |
745 | connectorMaximumAmperage, | |
746 | connectorMinimumAmperage, | |
747 | { | |
748 | limitationEnabled: | |
749 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
750 | fallbackValue: connectorMinimumAmperage, | |
751 | }, | |
752 | ), | |
753 | currentPerPhaseSampledValueTemplates.L1?.fluctuationPercent ?? | |
754 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
755 | ) | |
756 | : undefined; | |
757 | const phase2FluctuatedValue = isNotEmptyString( | |
758 | currentPerPhaseSampledValueTemplates.L2?.value, | |
759 | ) | |
760 | ? getRandomFloatFluctuatedRounded( | |
761 | getLimitFromSampledValueTemplateCustomValue( | |
762 | currentPerPhaseSampledValueTemplates.L2?.value, | |
763 | connectorMaximumAmperage, | |
764 | connectorMinimumAmperage, | |
765 | { | |
766 | limitationEnabled: | |
767 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
768 | fallbackValue: connectorMinimumAmperage, | |
769 | }, | |
770 | ), | |
771 | currentPerPhaseSampledValueTemplates.L2?.fluctuationPercent ?? | |
772 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
773 | ) | |
774 | : undefined; | |
775 | const phase3FluctuatedValue = isNotEmptyString( | |
776 | currentPerPhaseSampledValueTemplates.L3?.value, | |
777 | ) | |
778 | ? getRandomFloatFluctuatedRounded( | |
779 | getLimitFromSampledValueTemplateCustomValue( | |
780 | currentPerPhaseSampledValueTemplates.L3?.value, | |
781 | connectorMaximumAmperage, | |
782 | connectorMinimumAmperage, | |
783 | { | |
784 | limitationEnabled: | |
785 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
786 | fallbackValue: connectorMinimumAmperage, | |
787 | }, | |
788 | ), | |
789 | currentPerPhaseSampledValueTemplates.L3?.fluctuationPercent ?? | |
790 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
791 | ) | |
792 | : undefined; | |
793 | currentMeasurandValues.L1 = | |
794 | phase1FluctuatedValue ?? | |
795 | defaultFluctuatedAmperagePerPhase ?? | |
796 | getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); | |
797 | currentMeasurandValues.L2 = | |
798 | phase2FluctuatedValue ?? | |
799 | defaultFluctuatedAmperagePerPhase ?? | |
800 | getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); | |
801 | currentMeasurandValues.L3 = | |
802 | phase3FluctuatedValue ?? | |
803 | defaultFluctuatedAmperagePerPhase ?? | |
804 | getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); | |
805 | } else { | |
806 | currentMeasurandValues.L1 = isNotEmptyString(currentSampledValueTemplate.value) | |
807 | ? getRandomFloatFluctuatedRounded( | |
808 | getLimitFromSampledValueTemplateCustomValue( | |
809 | currentSampledValueTemplate.value, | |
810 | connectorMaximumAmperage, | |
811 | connectorMinimumAmperage, | |
812 | { | |
813 | limitationEnabled: | |
814 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
815 | fallbackValue: connectorMinimumAmperage, | |
816 | }, | |
817 | ), | |
818 | currentSampledValueTemplate.fluctuationPercent ?? | |
819 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
820 | ) | |
821 | : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); | |
822 | currentMeasurandValues.L2 = 0; | |
823 | currentMeasurandValues.L3 = 0; | |
824 | } | |
825 | currentMeasurandValues.allPhases = roundTo( | |
826 | (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) / | |
827 | chargingStation.getNumberOfPhases(), | |
828 | 2, | |
829 | ); | |
830 | break; | |
831 | case CurrentType.DC: | |
832 | connectorMaximumAmperage = DCElectricUtils.amperage( | |
833 | connectorMaximumAvailablePower, | |
834 | chargingStation.stationInfo.voltageOut!, | |
835 | ); | |
836 | currentMeasurandValues.allPhases = isNotEmptyString(currentSampledValueTemplate.value) | |
837 | ? getRandomFloatFluctuatedRounded( | |
838 | getLimitFromSampledValueTemplateCustomValue( | |
839 | currentSampledValueTemplate.value, | |
840 | connectorMaximumAmperage, | |
841 | connectorMinimumAmperage, | |
842 | { | |
843 | limitationEnabled: | |
844 | chargingStation.stationInfo?.customValueLimitationMeterValues, | |
845 | fallbackValue: connectorMinimumAmperage, | |
846 | }, | |
847 | ), | |
848 | currentSampledValueTemplate.fluctuationPercent ?? | |
849 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
850 | ) | |
851 | : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); | |
852 | break; | |
853 | default: | |
854 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`); | |
855 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES); | |
856 | } | |
857 | meterValue.sampledValue.push( | |
858 | buildSampledValue(currentSampledValueTemplate, currentMeasurandValues.allPhases), | |
859 | ); | |
860 | const sampledValuesIndex = meterValue.sampledValue.length - 1; | |
861 | if ( | |
862 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > | |
863 | connectorMaximumAmperage || | |
864 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) < | |
865 | connectorMinimumAmperage || | |
866 | debug | |
867 | ) { | |
868 | logger.error( | |
869 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
870 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
871 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
872 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${ | |
873 | meterValue.sampledValue[sampledValuesIndex].value | |
874 | }/${connectorMaximumAmperage}`, | |
875 | ); | |
876 | } | |
877 | for ( | |
878 | let phase = 1; | |
879 | chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); | |
880 | phase++ | |
881 | ) { | |
882 | const phaseValue = `L${phase}`; | |
883 | meterValue.sampledValue.push( | |
884 | buildSampledValue( | |
885 | currentPerPhaseSampledValueTemplates[ | |
886 | phaseValue as keyof MeasurandPerPhaseSampledValueTemplates | |
887 | ] ?? currentSampledValueTemplate, | |
888 | currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates], | |
889 | undefined, | |
890 | phaseValue as MeterValuePhase, | |
891 | ), | |
892 | ); | |
893 | const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1; | |
894 | if ( | |
895 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > | |
896 | connectorMaximumAmperage || | |
897 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) < | |
898 | connectorMinimumAmperage || | |
899 | debug | |
900 | ) { | |
901 | logger.error( | |
902 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
903 | meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ?? | |
904 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
905 | }: phase ${ | |
906 | meterValue.sampledValue[sampledValuesPerPhaseIndex].phase | |
907 | }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${ | |
908 | meterValue.sampledValue[sampledValuesPerPhaseIndex].value | |
909 | }/${connectorMaximumAmperage}`, | |
910 | ); | |
911 | } | |
912 | } | |
913 | } | |
914 | // Energy.Active.Import.Register measurand (default) | |
915 | energySampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId); | |
916 | if (energySampledValueTemplate) { | |
917 | checkMeasurandPowerDivider(chargingStation, energySampledValueTemplate.measurand!); | |
918 | const unitDivider = | |
919 | energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; | |
920 | const connectorMaximumAvailablePower = | |
921 | chargingStation.getConnectorMaximumAvailablePower(connectorId); | |
922 | const connectorMaximumEnergyRounded = roundTo( | |
923 | (connectorMaximumAvailablePower * interval) / (3600 * 1000), | |
924 | 2, | |
925 | ); | |
926 | const connectorMinimumEnergyRounded = roundTo( | |
927 | energySampledValueTemplate.minimumValue ?? 0, | |
928 | 2, | |
929 | ); | |
930 | const energyValueRounded = isNotEmptyString(energySampledValueTemplate.value) | |
931 | ? getRandomFloatFluctuatedRounded( | |
932 | getLimitFromSampledValueTemplateCustomValue( | |
933 | energySampledValueTemplate.value, | |
934 | connectorMaximumEnergyRounded, | |
935 | connectorMinimumEnergyRounded, | |
936 | { | |
937 | limitationEnabled: chargingStation.stationInfo?.customValueLimitationMeterValues, | |
938 | fallbackValue: connectorMinimumEnergyRounded, | |
939 | unitMultiplier: unitDivider, | |
940 | }, | |
941 | ), | |
942 | energySampledValueTemplate.fluctuationPercent ?? | |
943 | Constants.DEFAULT_FLUCTUATION_PERCENT, | |
944 | ) | |
945 | : getRandomFloatRounded(connectorMaximumEnergyRounded, connectorMinimumEnergyRounded); | |
946 | // Persist previous value on connector | |
947 | if (connector) { | |
948 | if ( | |
949 | isNullOrUndefined(connector.energyActiveImportRegisterValue) === false && | |
950 | connector.energyActiveImportRegisterValue! >= 0 && | |
951 | isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false && | |
952 | connector.transactionEnergyActiveImportRegisterValue! >= 0 | |
953 | ) { | |
954 | connector.energyActiveImportRegisterValue! += energyValueRounded; | |
955 | connector.transactionEnergyActiveImportRegisterValue! += energyValueRounded; | |
956 | } else { | |
957 | connector.energyActiveImportRegisterValue = 0; | |
958 | connector.transactionEnergyActiveImportRegisterValue = 0; | |
959 | } | |
960 | } | |
961 | meterValue.sampledValue.push( | |
962 | buildSampledValue( | |
963 | energySampledValueTemplate, | |
964 | roundTo( | |
965 | chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) / | |
966 | unitDivider, | |
967 | 2, | |
968 | ), | |
969 | ), | |
970 | ); | |
971 | const sampledValuesIndex = meterValue.sampledValue.length - 1; | |
972 | if ( | |
973 | energyValueRounded > connectorMaximumEnergyRounded || | |
974 | energyValueRounded < connectorMinimumEnergyRounded || | |
975 | debug | |
976 | ) { | |
977 | logger.error( | |
978 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
979 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
980 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
981 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumEnergyRounded}/${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`, | |
982 | ); | |
983 | } | |
984 | } | |
985 | return meterValue; | |
986 | case OCPPVersion.VERSION_20: | |
987 | case OCPPVersion.VERSION_201: | |
988 | default: | |
989 | throw new BaseError( | |
990 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions | |
991 | `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`, | |
992 | ); | |
993 | } | |
994 | }; | |
995 | ||
996 | export const buildTransactionEndMeterValue = ( | |
997 | chargingStation: ChargingStation, | |
998 | connectorId: number, | |
999 | meterStop: number, | |
1000 | ): MeterValue => { | |
1001 | let meterValue: MeterValue; | |
1002 | let sampledValueTemplate: SampledValueTemplate | undefined; | |
1003 | let unitDivider: number; | |
1004 | switch (chargingStation.stationInfo?.ocppVersion) { | |
1005 | case OCPPVersion.VERSION_16: | |
1006 | meterValue = { | |
1007 | timestamp: new Date(), | |
1008 | sampledValue: [], | |
1009 | }; | |
1010 | // Energy.Active.Import.Register measurand (default) | |
1011 | sampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId); | |
1012 | unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; | |
1013 | meterValue.sampledValue.push( | |
1014 | buildSampledValue( | |
1015 | sampledValueTemplate!, | |
1016 | roundTo((meterStop ?? 0) / unitDivider, 4), | |
1017 | MeterValueContext.TRANSACTION_END, | |
1018 | ), | |
1019 | ); | |
1020 | return meterValue; | |
1021 | case OCPPVersion.VERSION_20: | |
1022 | case OCPPVersion.VERSION_201: | |
1023 | default: | |
1024 | throw new BaseError( | |
1025 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions | |
1026 | `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`, | |
1027 | ); | |
1028 | } | |
1029 | }; | |
1030 | ||
1031 | const checkMeasurandPowerDivider = ( | |
1032 | chargingStation: ChargingStation, | |
1033 | measurandType: MeterValueMeasurand, | |
1034 | ): void => { | |
1035 | if (isUndefined(chargingStation.powerDivider)) { | |
1036 | const errMsg = `MeterValues measurand ${ | |
1037 | measurandType ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
1038 | }: powerDivider is undefined`; | |
1039 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`); | |
1040 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES); | |
1041 | } else if (chargingStation?.powerDivider <= 0) { | |
1042 | const errMsg = `MeterValues measurand ${ | |
1043 | measurandType ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
1044 | }: powerDivider have zero or below value ${chargingStation.powerDivider}`; | |
1045 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`); | |
1046 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES); | |
1047 | } | |
1048 | }; | |
1049 | ||
1050 | const getLimitFromSampledValueTemplateCustomValue = ( | |
1051 | value: string | undefined, | |
1052 | maxLimit: number, | |
1053 | minLimit: number, | |
1054 | options?: { limitationEnabled?: boolean; fallbackValue?: number; unitMultiplier?: number }, | |
1055 | ): number => { | |
1056 | options = { | |
1057 | ...{ | |
1058 | limitationEnabled: false, | |
1059 | unitMultiplier: 1, | |
1060 | fallbackValue: 0, | |
1061 | }, | |
1062 | ...options, | |
1063 | }; | |
1064 | const parsedValue = parseInt(value ?? ''); | |
1065 | if (options?.limitationEnabled) { | |
1066 | return max( | |
1067 | min((!isNaN(parsedValue) ? parsedValue : Infinity) * options.unitMultiplier!, maxLimit), | |
1068 | minLimit, | |
1069 | ); | |
1070 | } | |
1071 | return (!isNaN(parsedValue) ? parsedValue : options.fallbackValue!) * options.unitMultiplier!; | |
1072 | }; | |
1073 | ||
1074 | const getSampledValueTemplate = ( | |
1075 | chargingStation: ChargingStation, | |
1076 | connectorId: number, | |
1077 | measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER, | |
1078 | phase?: MeterValuePhase, | |
1079 | ): SampledValueTemplate | undefined => { | |
1080 | const onPhaseStr = phase ? `on phase ${phase} ` : ''; | |
1081 | if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) { | |
1082 | logger.warn( | |
1083 | `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`, | |
1084 | ); | |
1085 | return; | |
1086 | } | |
1087 | if ( | |
1088 | measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER && | |
1089 | getConfigurationKey( | |
1090 | chargingStation, | |
1091 | StandardParametersKey.MeterValuesSampledData, | |
1092 | )?.value?.includes(measurand) === false | |
1093 | ) { | |
1094 | logger.debug( | |
1095 | `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${ | |
1096 | StandardParametersKey.MeterValuesSampledData | |
1097 | }' OCPP parameter`, | |
1098 | ); | |
1099 | return; | |
1100 | } | |
1101 | const sampledValueTemplates: SampledValueTemplate[] = | |
1102 | chargingStation.getConnectorStatus(connectorId)!.MeterValues; | |
1103 | for ( | |
1104 | let index = 0; | |
1105 | isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length; | |
1106 | index++ | |
1107 | ) { | |
1108 | if ( | |
1109 | OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes( | |
1110 | sampledValueTemplates[index]?.measurand ?? | |
1111 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER, | |
1112 | ) === false | |
1113 | ) { | |
1114 | logger.warn( | |
1115 | `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`, | |
1116 | ); | |
1117 | } else if ( | |
1118 | phase && | |
1119 | sampledValueTemplates[index]?.phase === phase && | |
1120 | sampledValueTemplates[index]?.measurand === measurand && | |
1121 | getConfigurationKey( | |
1122 | chargingStation, | |
1123 | StandardParametersKey.MeterValuesSampledData, | |
1124 | )?.value?.includes(measurand) === true | |
1125 | ) { | |
1126 | return sampledValueTemplates[index]; | |
1127 | } else if ( | |
1128 | !phase && | |
1129 | !sampledValueTemplates[index]?.phase && | |
1130 | sampledValueTemplates[index]?.measurand === measurand && | |
1131 | getConfigurationKey( | |
1132 | chargingStation, | |
1133 | StandardParametersKey.MeterValuesSampledData, | |
1134 | )?.value?.includes(measurand) === true | |
1135 | ) { | |
1136 | return sampledValueTemplates[index]; | |
1137 | } else if ( | |
1138 | measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER && | |
1139 | (!sampledValueTemplates[index]?.measurand || | |
1140 | sampledValueTemplates[index]?.measurand === measurand) | |
1141 | ) { | |
1142 | return sampledValueTemplates[index]; | |
1143 | } | |
1144 | } | |
1145 | if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) { | |
1146 | const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`; | |
1147 | logger.error(`${chargingStation.logPrefix()} ${errorMsg}`); | |
1148 | throw new BaseError(errorMsg); | |
1149 | } | |
1150 | logger.debug( | |
1151 | `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`, | |
1152 | ); | |
1153 | }; | |
1154 | ||
1155 | const buildSampledValue = ( | |
1156 | sampledValueTemplate: SampledValueTemplate, | |
1157 | value: number, | |
1158 | context?: MeterValueContext, | |
1159 | phase?: MeterValuePhase, | |
1160 | ): SampledValue => { | |
1161 | const sampledValueContext = context ?? sampledValueTemplate?.context; | |
1162 | const sampledValueLocation = | |
1163 | sampledValueTemplate?.location ?? getMeasurandDefaultLocation(sampledValueTemplate.measurand!); | |
1164 | const sampledValuePhase = phase ?? sampledValueTemplate?.phase; | |
1165 | return { | |
1166 | ...(!isNullOrUndefined(sampledValueTemplate.unit) && { | |
1167 | unit: sampledValueTemplate.unit, | |
1168 | }), | |
1169 | ...(!isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }), | |
1170 | ...(!isNullOrUndefined(sampledValueTemplate.measurand) && { | |
1171 | measurand: sampledValueTemplate.measurand, | |
1172 | }), | |
1173 | ...(!isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }), | |
1174 | ...(!isNullOrUndefined(value) && { value: value.toString() }), | |
1175 | ...(!isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }), | |
1176 | } as SampledValue; | |
1177 | }; | |
1178 | ||
1179 | const getMeasurandDefaultLocation = ( | |
1180 | measurandType: MeterValueMeasurand, | |
1181 | ): MeterValueLocation | undefined => { | |
1182 | switch (measurandType) { | |
1183 | case MeterValueMeasurand.STATE_OF_CHARGE: | |
1184 | return MeterValueLocation.EV; | |
1185 | } | |
1186 | }; | |
1187 | ||
1188 | // const getMeasurandDefaultUnit = ( | |
1189 | // measurandType: MeterValueMeasurand, | |
1190 | // ): MeterValueUnit | undefined => { | |
1191 | // switch (measurandType) { | |
1192 | // case MeterValueMeasurand.CURRENT_EXPORT: | |
1193 | // case MeterValueMeasurand.CURRENT_IMPORT: | |
1194 | // case MeterValueMeasurand.CURRENT_OFFERED: | |
1195 | // return MeterValueUnit.AMP; | |
1196 | // case MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: | |
1197 | // case MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: | |
1198 | // return MeterValueUnit.WATT_HOUR; | |
1199 | // case MeterValueMeasurand.POWER_ACTIVE_EXPORT: | |
1200 | // case MeterValueMeasurand.POWER_ACTIVE_IMPORT: | |
1201 | // case MeterValueMeasurand.POWER_OFFERED: | |
1202 | // return MeterValueUnit.WATT; | |
1203 | // case MeterValueMeasurand.STATE_OF_CHARGE: | |
1204 | // return MeterValueUnit.PERCENT; | |
1205 | // case MeterValueMeasurand.VOLTAGE: | |
1206 | // return MeterValueUnit.VOLT; | |
1207 | // } | |
1208 | // }; | |
1209 | ||
90befdb8 | 1210 | export class OCPPServiceUtils { |
041365be JB |
1211 | public static getMessageTypeString = getMessageTypeString; |
1212 | public static sendAndSetConnectorStatus = sendAndSetConnectorStatus; | |
1213 | public static isIdTagAuthorized = isIdTagAuthorized; | |
41f3983a JB |
1214 | public static buildTransactionEndMeterValue = buildTransactionEndMeterValue; |
1215 | protected static getSampledValueTemplate = getSampledValueTemplate; | |
1216 | protected static buildSampledValue = buildSampledValue; | |
041365be | 1217 | |
d5bd1c00 JB |
1218 | protected constructor() { |
1219 | // This is intentional | |
1220 | } | |
1221 | ||
9ff486f4 JB |
1222 | public static ajvErrorsToErrorType(errors: ErrorObject[] | null | undefined): ErrorType { |
1223 | if (isNotEmptyArray(errors) === true) { | |
1224 | for (const error of errors as DefinedError[]) { | |
1225 | switch (error.keyword) { | |
1226 | case 'type': | |
1227 | return ErrorType.TYPE_CONSTRAINT_VIOLATION; | |
1228 | case 'dependencies': | |
1229 | case 'required': | |
1230 | return ErrorType.OCCURRENCE_CONSTRAINT_VIOLATION; | |
1231 | case 'pattern': | |
1232 | case 'format': | |
1233 | return ErrorType.PROPERTY_CONSTRAINT_VIOLATION; | |
1234 | } | |
06ad945f JB |
1235 | } |
1236 | } | |
1237 | return ErrorType.FORMAT_VIOLATION; | |
1238 | } | |
1239 | ||
ed3d2808 | 1240 | public static isRequestCommandSupported( |
fd3c56d1 | 1241 | chargingStation: ChargingStation, |
5edd8ba0 | 1242 | command: RequestCommand, |
ed3d2808 | 1243 | ): boolean { |
edd13439 | 1244 | const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command); |
ed3d2808 JB |
1245 | if ( |
1246 | isRequestCommand === true && | |
1247 | !chargingStation.stationInfo?.commandsSupport?.outgoingCommands | |
1248 | ) { | |
1249 | return true; | |
1250 | } else if ( | |
1251 | isRequestCommand === true && | |
1c9de2b9 | 1252 | chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] |
ed3d2808 | 1253 | ) { |
1c9de2b9 | 1254 | return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command]; |
ed3d2808 JB |
1255 | } |
1256 | logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`); | |
1257 | return false; | |
1258 | } | |
1259 | ||
1260 | public static isIncomingRequestCommandSupported( | |
fd3c56d1 | 1261 | chargingStation: ChargingStation, |
5edd8ba0 | 1262 | command: IncomingRequestCommand, |
ed3d2808 | 1263 | ): boolean { |
edd13439 JB |
1264 | const isIncomingRequestCommand = |
1265 | Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command); | |
ed3d2808 JB |
1266 | if ( |
1267 | isIncomingRequestCommand === true && | |
1268 | !chargingStation.stationInfo?.commandsSupport?.incomingCommands | |
1269 | ) { | |
1270 | return true; | |
1271 | } else if ( | |
1272 | isIncomingRequestCommand === true && | |
1c9de2b9 | 1273 | chargingStation.stationInfo?.commandsSupport?.incomingCommands?.[command] |
ed3d2808 | 1274 | ) { |
1c9de2b9 | 1275 | return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command]; |
ed3d2808 JB |
1276 | } |
1277 | logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`); | |
1278 | return false; | |
1279 | } | |
1280 | ||
c60ed4b8 JB |
1281 | public static isMessageTriggerSupported( |
1282 | chargingStation: ChargingStation, | |
5edd8ba0 | 1283 | messageTrigger: MessageTrigger, |
c60ed4b8 JB |
1284 | ): boolean { |
1285 | const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger); | |
1286 | if (isMessageTrigger === true && !chargingStation.stationInfo?.messageTriggerSupport) { | |
1287 | return true; | |
1c9de2b9 JB |
1288 | } else if ( |
1289 | isMessageTrigger === true && | |
1290 | chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger] | |
1291 | ) { | |
1292 | return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger]; | |
c60ed4b8 JB |
1293 | } |
1294 | logger.error( | |
5edd8ba0 | 1295 | `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`, |
c60ed4b8 JB |
1296 | ); |
1297 | return false; | |
1298 | } | |
1299 | ||
1300 | public static isConnectorIdValid( | |
1301 | chargingStation: ChargingStation, | |
1302 | ocppCommand: IncomingRequestCommand, | |
5edd8ba0 | 1303 | connectorId: number, |
c60ed4b8 JB |
1304 | ): boolean { |
1305 | if (connectorId < 0) { | |
1306 | logger.error( | |
5edd8ba0 | 1307 | `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`, |
c60ed4b8 JB |
1308 | ); |
1309 | return false; | |
1310 | } | |
1311 | return true; | |
1312 | } | |
1313 | ||
1799761a | 1314 | public static convertDateToISOString<T extends JsonType>(obj: T): void { |
a37fc6dc JB |
1315 | for (const key in obj) { |
1316 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion | |
3dcf7b67 | 1317 | if (isDate(obj![key])) { |
a37fc6dc JB |
1318 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion |
1319 | (obj![key] as string) = (obj![key] as Date).toISOString(); | |
1320 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion | |
e1d9a0f4 | 1321 | } else if (obj![key] !== null && typeof obj![key] === 'object') { |
a37fc6dc | 1322 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion |
e1d9a0f4 | 1323 | OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T); |
1799761a JB |
1324 | } |
1325 | } | |
1326 | } | |
1327 | ||
8f953431 | 1328 | public static startHeartbeatInterval(chargingStation: ChargingStation, interval: number): void { |
2a2ad81b | 1329 | if (chargingStation.heartbeatSetInterval === undefined) { |
8f953431 JB |
1330 | chargingStation.startHeartbeat(); |
1331 | } else if (chargingStation.getHeartbeatInterval() !== interval) { | |
1332 | chargingStation.restartHeartbeat(); | |
1333 | } | |
1334 | } | |
1335 | ||
7164966d | 1336 | protected static parseJsonSchemaFile<T extends JsonType>( |
51022aa0 | 1337 | relativePath: string, |
1b271a54 JB |
1338 | ocppVersion: OCPPVersion, |
1339 | moduleName?: string, | |
5edd8ba0 | 1340 | methodName?: string, |
7164966d | 1341 | ): JSONSchemaType<T> { |
d972af76 | 1342 | const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath); |
7164966d | 1343 | try { |
d972af76 | 1344 | return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>; |
7164966d | 1345 | } catch (error) { |
fa5995d6 | 1346 | handleFileException( |
7164966d JB |
1347 | filePath, |
1348 | FileType.JsonSchema, | |
1349 | error as NodeJS.ErrnoException, | |
1b271a54 | 1350 | OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName), |
5edd8ba0 | 1351 | { throwError: false }, |
7164966d | 1352 | ); |
e1d9a0f4 | 1353 | return {} as JSONSchemaType<T>; |
7164966d | 1354 | } |
130783a7 JB |
1355 | } |
1356 | ||
1b271a54 JB |
1357 | private static logPrefix = ( |
1358 | ocppVersion: OCPPVersion, | |
1359 | moduleName?: string, | |
5edd8ba0 | 1360 | methodName?: string, |
1b271a54 JB |
1361 | ): string => { |
1362 | const logMsg = | |
9bf0ef23 | 1363 | isNotEmptyString(moduleName) && isNotEmptyString(methodName) |
1b271a54 JB |
1364 | ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:` |
1365 | : ` OCPP ${ocppVersion} |`; | |
9bf0ef23 | 1366 | return logPrefix(logMsg); |
8b7072dc | 1367 | }; |
90befdb8 | 1368 | } |