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