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