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