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