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