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