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