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