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