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