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