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