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