perf: minimize OCPPUtils exports
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
CommitLineData
d972af76
JB
1import { readFileSync } from 'node:fs';
2import { dirname, join } from 'node:path';
51022aa0 3import { fileURLToPath } from 'node:url';
7164966d
JB
4
5import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv';
3dcf7b67 6import { isDate } from 'date-fns';
06ad945f 7
4c3c0d59
JB
8import { OCPP16Constants } from './1.6/OCPP16Constants';
9import { OCPP20Constants } from './2.0/OCPP20Constants';
c3da35d4 10import { OCPPConstants } from './OCPPConstants';
ae725be3 11import { type ChargingStation, getConfigurationKey, getIdTagsFile } from '../../charging-station';
41f3983a 12import { BaseError, OCPPError } from '../../exception';
6e939d9e 13import {
ae725be3
JB
14 AuthorizationStatus,
15 type AuthorizeRequest,
16 type AuthorizeResponse,
268a74bb 17 ChargePointErrorCode,
3e888c65 18 ChargingStationEvents,
ae725be3 19 type ConnectorStatus,
268a74bb 20 type ConnectorStatusEnum,
41f3983a 21 CurrentType,
268a74bb
JB
22 ErrorType,
23 FileType,
6e939d9e 24 IncomingRequestCommand,
268a74bb 25 type JsonType,
41f3983a
JB
26 type MeasurandPerPhaseSampledValueTemplates,
27 type MeasurandValues,
6e939d9e 28 MessageTrigger,
268a74bb 29 MessageType,
41f3983a
JB
30 type MeterValue,
31 MeterValueContext,
32 MeterValueLocation,
268a74bb 33 MeterValueMeasurand,
41f3983a
JB
34 MeterValuePhase,
35 MeterValueUnit,
268a74bb
JB
36 type OCPP16StatusNotificationRequest,
37 type OCPP20StatusNotificationRequest,
38 OCPPVersion,
6e939d9e 39 RequestCommand,
41f3983a 40 type SampledValue,
268a74bb
JB
41 type SampledValueTemplate,
42 StandardParametersKey,
6e939d9e 43 type StatusNotificationRequest,
48b75072 44 type StatusNotificationResponse,
268a74bb 45} from '../../types';
9bf0ef23 46import {
41f3983a
JB
47 ACElectricUtils,
48 Constants,
49 DCElectricUtils,
50 convertToFloat,
51 convertToInt,
52 getRandomFloatFluctuatedRounded,
53 getRandomFloatRounded,
54 getRandomInteger,
9bf0ef23
JB
55 handleFileException,
56 isNotEmptyArray,
57 isNotEmptyString,
41f3983a
JB
58 isNullOrUndefined,
59 isUndefined,
9bf0ef23
JB
60 logPrefix,
61 logger,
d71ce3fa 62 max,
5adf6ca4 63 min,
41f3983a 64 roundTo,
9bf0ef23 65} from '../../utils';
06ad945f 66
041365be
JB
67export 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
80export 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
106export 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
133const 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
144const 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
163export 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
189const 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
41f3983a
JB
243export 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
996export 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
1031const 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
1050const 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
1074const 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
1155const 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
1179const 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
90befdb8 1210export class OCPPServiceUtils {
041365be
JB
1211 public static getMessageTypeString = getMessageTypeString;
1212 public static sendAndSetConnectorStatus = sendAndSetConnectorStatus;
1213 public static isIdTagAuthorized = isIdTagAuthorized;
41f3983a
JB
1214 public static buildTransactionEndMeterValue = buildTransactionEndMeterValue;
1215 protected static getSampledValueTemplate = getSampledValueTemplate;
1216 protected static buildSampledValue = buildSampledValue;
041365be 1217
d5bd1c00
JB
1218 protected constructor() {
1219 // This is intentional
1220 }
1221
9ff486f4
JB
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 }
06ad945f
JB
1235 }
1236 }
1237 return ErrorType.FORMAT_VIOLATION;
1238 }
1239
ed3d2808 1240 public static isRequestCommandSupported(
fd3c56d1 1241 chargingStation: ChargingStation,
5edd8ba0 1242 command: RequestCommand,
ed3d2808 1243 ): boolean {
edd13439 1244 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
ed3d2808
JB
1245 if (
1246 isRequestCommand === true &&
1247 !chargingStation.stationInfo?.commandsSupport?.outgoingCommands
1248 ) {
1249 return true;
1250 } else if (
1251 isRequestCommand === true &&
1c9de2b9 1252 chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command]
ed3d2808 1253 ) {
1c9de2b9 1254 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command];
ed3d2808
JB
1255 }
1256 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
1257 return false;
1258 }
1259
1260 public static isIncomingRequestCommandSupported(
fd3c56d1 1261 chargingStation: ChargingStation,
5edd8ba0 1262 command: IncomingRequestCommand,
ed3d2808 1263 ): boolean {
edd13439
JB
1264 const isIncomingRequestCommand =
1265 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
ed3d2808
JB
1266 if (
1267 isIncomingRequestCommand === true &&
1268 !chargingStation.stationInfo?.commandsSupport?.incomingCommands
1269 ) {
1270 return true;
1271 } else if (
1272 isIncomingRequestCommand === true &&
1c9de2b9 1273 chargingStation.stationInfo?.commandsSupport?.incomingCommands?.[command]
ed3d2808 1274 ) {
1c9de2b9 1275 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command];
ed3d2808
JB
1276 }
1277 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
1278 return false;
1279 }
1280
c60ed4b8
JB
1281 public static isMessageTriggerSupported(
1282 chargingStation: ChargingStation,
5edd8ba0 1283 messageTrigger: MessageTrigger,
c60ed4b8
JB
1284 ): boolean {
1285 const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger);
1286 if (isMessageTrigger === true && !chargingStation.stationInfo?.messageTriggerSupport) {
1287 return true;
1c9de2b9
JB
1288 } else if (
1289 isMessageTrigger === true &&
1290 chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger]
1291 ) {
1292 return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger];
c60ed4b8
JB
1293 }
1294 logger.error(
5edd8ba0 1295 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
c60ed4b8
JB
1296 );
1297 return false;
1298 }
1299
1300 public static isConnectorIdValid(
1301 chargingStation: ChargingStation,
1302 ocppCommand: IncomingRequestCommand,
5edd8ba0 1303 connectorId: number,
c60ed4b8
JB
1304 ): boolean {
1305 if (connectorId < 0) {
1306 logger.error(
5edd8ba0 1307 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
c60ed4b8
JB
1308 );
1309 return false;
1310 }
1311 return true;
1312 }
1313
1799761a 1314 public static convertDateToISOString<T extends JsonType>(obj: T): void {
a37fc6dc
JB
1315 for (const key in obj) {
1316 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
3dcf7b67 1317 if (isDate(obj![key])) {
a37fc6dc
JB
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
e1d9a0f4 1321 } else if (obj![key] !== null && typeof obj![key] === 'object') {
a37fc6dc 1322 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 1323 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T);
1799761a
JB
1324 }
1325 }
1326 }
1327
8f953431 1328 public static startHeartbeatInterval(chargingStation: ChargingStation, interval: number): void {
2a2ad81b 1329 if (chargingStation.heartbeatSetInterval === undefined) {
8f953431
JB
1330 chargingStation.startHeartbeat();
1331 } else if (chargingStation.getHeartbeatInterval() !== interval) {
1332 chargingStation.restartHeartbeat();
1333 }
1334 }
1335
7164966d 1336 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 1337 relativePath: string,
1b271a54
JB
1338 ocppVersion: OCPPVersion,
1339 moduleName?: string,
5edd8ba0 1340 methodName?: string,
7164966d 1341 ): JSONSchemaType<T> {
d972af76 1342 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d 1343 try {
d972af76 1344 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
7164966d 1345 } catch (error) {
fa5995d6 1346 handleFileException(
7164966d
JB
1347 filePath,
1348 FileType.JsonSchema,
1349 error as NodeJS.ErrnoException,
1b271a54 1350 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
5edd8ba0 1351 { throwError: false },
7164966d 1352 );
e1d9a0f4 1353 return {} as JSONSchemaType<T>;
7164966d 1354 }
130783a7
JB
1355 }
1356
1b271a54
JB
1357 private static logPrefix = (
1358 ocppVersion: OCPPVersion,
1359 moduleName?: string,
5edd8ba0 1360 methodName?: string,
1b271a54
JB
1361 ): string => {
1362 const logMsg =
9bf0ef23 1363 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54
JB
1364 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
1365 : ` OCPP ${ocppVersion} |`;
9bf0ef23 1366 return logPrefix(logMsg);
8b7072dc 1367 };
90befdb8 1368}