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