fix: fallback to minimum measurand value at meterValues generation
[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';
268a74bb 12import { BaseError } 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
JB
20 type ConnectorStatusEnum,
21 ErrorType,
22 FileType,
6e939d9e 23 IncomingRequestCommand,
268a74bb 24 type JsonType,
6e939d9e 25 MessageTrigger,
268a74bb
JB
26 MessageType,
27 MeterValueMeasurand,
28 type MeterValuePhase,
29 type OCPP16StatusNotificationRequest,
30 type OCPP20StatusNotificationRequest,
31 OCPPVersion,
6e939d9e 32 RequestCommand,
268a74bb
JB
33 type SampledValueTemplate,
34 StandardParametersKey,
6e939d9e 35 type StatusNotificationRequest,
48b75072 36 type StatusNotificationResponse,
268a74bb 37} from '../../types';
9bf0ef23
JB
38import {
39 handleFileException,
40 isNotEmptyArray,
41 isNotEmptyString,
42 logPrefix,
43 logger,
5adf6ca4 44 min,
9bf0ef23 45} from '../../utils';
06ad945f 46
90befdb8 47export class OCPPServiceUtils {
d5bd1c00
JB
48 protected constructor() {
49 // This is intentional
50 }
51
9ff486f4
JB
52 public static ajvErrorsToErrorType(errors: ErrorObject[] | null | undefined): ErrorType {
53 if (isNotEmptyArray(errors) === true) {
54 for (const error of errors as DefinedError[]) {
55 switch (error.keyword) {
56 case 'type':
57 return ErrorType.TYPE_CONSTRAINT_VIOLATION;
58 case 'dependencies':
59 case 'required':
60 return ErrorType.OCCURRENCE_CONSTRAINT_VIOLATION;
61 case 'pattern':
62 case 'format':
63 return ErrorType.PROPERTY_CONSTRAINT_VIOLATION;
64 }
06ad945f
JB
65 }
66 }
67 return ErrorType.FORMAT_VIOLATION;
68 }
69
2cc5d5ec
JB
70 public static getMessageTypeString(messageType: MessageType): string {
71 switch (messageType) {
72 case MessageType.CALL_MESSAGE:
73 return 'request';
74 case MessageType.CALL_RESULT_MESSAGE:
75 return 'response';
76 case MessageType.CALL_ERROR_MESSAGE:
77 return 'error';
336f2829
JB
78 default:
79 return 'unknown';
2cc5d5ec
JB
80 }
81 }
82
ed3d2808 83 public static isRequestCommandSupported(
fd3c56d1 84 chargingStation: ChargingStation,
5edd8ba0 85 command: RequestCommand,
ed3d2808 86 ): boolean {
edd13439 87 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
ed3d2808
JB
88 if (
89 isRequestCommand === true &&
90 !chargingStation.stationInfo?.commandsSupport?.outgoingCommands
91 ) {
92 return true;
93 } else if (
94 isRequestCommand === true &&
1c9de2b9 95 chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command]
ed3d2808 96 ) {
1c9de2b9 97 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command];
ed3d2808
JB
98 }
99 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
100 return false;
101 }
102
103 public static isIncomingRequestCommandSupported(
fd3c56d1 104 chargingStation: ChargingStation,
5edd8ba0 105 command: IncomingRequestCommand,
ed3d2808 106 ): boolean {
edd13439
JB
107 const isIncomingRequestCommand =
108 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
ed3d2808
JB
109 if (
110 isIncomingRequestCommand === true &&
111 !chargingStation.stationInfo?.commandsSupport?.incomingCommands
112 ) {
113 return true;
114 } else if (
115 isIncomingRequestCommand === true &&
1c9de2b9 116 chargingStation.stationInfo?.commandsSupport?.incomingCommands?.[command]
ed3d2808 117 ) {
1c9de2b9 118 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command];
ed3d2808
JB
119 }
120 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
121 return false;
122 }
123
c60ed4b8
JB
124 public static isMessageTriggerSupported(
125 chargingStation: ChargingStation,
5edd8ba0 126 messageTrigger: MessageTrigger,
c60ed4b8
JB
127 ): boolean {
128 const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger);
129 if (isMessageTrigger === true && !chargingStation.stationInfo?.messageTriggerSupport) {
130 return true;
1c9de2b9
JB
131 } else if (
132 isMessageTrigger === true &&
133 chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger]
134 ) {
135 return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger];
c60ed4b8
JB
136 }
137 logger.error(
5edd8ba0 138 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
c60ed4b8
JB
139 );
140 return false;
141 }
142
143 public static isConnectorIdValid(
144 chargingStation: ChargingStation,
145 ocppCommand: IncomingRequestCommand,
5edd8ba0 146 connectorId: number,
c60ed4b8
JB
147 ): boolean {
148 if (connectorId < 0) {
149 logger.error(
5edd8ba0 150 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
c60ed4b8
JB
151 );
152 return false;
153 }
154 return true;
155 }
156
1799761a 157 public static convertDateToISOString<T extends JsonType>(obj: T): void {
a37fc6dc
JB
158 for (const key in obj) {
159 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
3dcf7b67 160 if (isDate(obj![key])) {
a37fc6dc
JB
161 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
162 (obj![key] as string) = (obj![key] as Date).toISOString();
163 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 164 } else if (obj![key] !== null && typeof obj![key] === 'object') {
a37fc6dc 165 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 166 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T);
1799761a
JB
167 }
168 }
169 }
170
6e939d9e
JB
171 public static buildStatusNotificationRequest(
172 chargingStation: ChargingStation,
173 connectorId: number,
12f26d4a 174 status: ConnectorStatusEnum,
5edd8ba0 175 evseId?: number,
6e939d9e 176 ): StatusNotificationRequest {
5398cecf 177 switch (chargingStation.stationInfo?.ocppVersion) {
6e939d9e
JB
178 case OCPPVersion.VERSION_16:
179 return {
180 connectorId,
181 status,
182 errorCode: ChargePointErrorCode.NO_ERROR,
183 } as OCPP16StatusNotificationRequest;
184 case OCPPVersion.VERSION_20:
185 case OCPPVersion.VERSION_201:
186 return {
187 timestamp: new Date(),
188 connectorStatus: status,
189 connectorId,
12f26d4a 190 evseId,
6e939d9e 191 } as OCPP20StatusNotificationRequest;
cda96260
JB
192 default:
193 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
6e939d9e
JB
194 }
195 }
196
8f953431
JB
197 public static startHeartbeatInterval(chargingStation: ChargingStation, interval: number): void {
198 if (!chargingStation.heartbeatSetInterval) {
199 chargingStation.startHeartbeat();
200 } else if (chargingStation.getHeartbeatInterval() !== interval) {
201 chargingStation.restartHeartbeat();
202 }
203 }
204
48b75072
JB
205 public static async sendAndSetConnectorStatus(
206 chargingStation: ChargingStation,
207 connectorId: number,
12f26d4a 208 status: ConnectorStatusEnum,
ec94a3cf 209 evseId?: number,
e1d9a0f4 210 options?: { send: boolean },
48b75072 211 ) {
ec94a3cf
JB
212 options = { send: true, ...options };
213 if (options.send) {
214 OCPPServiceUtils.checkConnectorStatusTransition(chargingStation, connectorId, status);
215 await chargingStation.ocppRequestService.requestHandler<
216 StatusNotificationRequest,
217 StatusNotificationResponse
218 >(
219 chargingStation,
220 RequestCommand.STATUS_NOTIFICATION,
221 OCPPServiceUtils.buildStatusNotificationRequest(
222 chargingStation,
223 connectorId,
224 status,
5edd8ba0
JB
225 evseId,
226 ),
ec94a3cf
JB
227 );
228 }
e1d9a0f4 229 chargingStation.getConnectorStatus(connectorId)!.status = status;
3e888c65
JB
230 chargingStation.emit(ChargingStationEvents.connectorStatusChanged, {
231 connectorId,
232 ...chargingStation.getConnectorStatus(connectorId),
233 });
48b75072
JB
234 }
235
ae725be3
JB
236 public static async isIdTagAuthorized(
237 chargingStation: ChargingStation,
238 connectorId: number,
239 idTag: string,
240 ): Promise<boolean> {
5398cecf
JB
241 if (
242 !chargingStation.getLocalAuthListEnabled() &&
243 !chargingStation.stationInfo?.remoteAuthorization
244 ) {
cfdf901d
JB
245 logger.warn(
246 `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`,
247 );
248 }
cfdf901d
JB
249 if (
250 chargingStation.getLocalAuthListEnabled() === true &&
251 OCPPServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)
252 ) {
ae725be3
JB
253 const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!;
254 connectorStatus.localAuthorizeIdTag = idTag;
255 connectorStatus.idTagLocalAuthorized = true;
ec54600d 256 return true;
5398cecf 257 } else if (chargingStation.stationInfo?.remoteAuthorization) {
ec54600d 258 return await OCPPServiceUtils.isIdTagRemoteAuthorized(chargingStation, connectorId, idTag);
ae725be3 259 }
ec54600d 260 return false;
ae725be3
JB
261 }
262
48b75072
JB
263 protected static checkConnectorStatusTransition(
264 chargingStation: ChargingStation,
265 connectorId: number,
5edd8ba0 266 status: ConnectorStatusEnum,
48b75072 267 ): boolean {
e1d9a0f4 268 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
48b75072 269 let transitionAllowed = false;
5398cecf 270 switch (chargingStation.stationInfo?.ocppVersion) {
48b75072
JB
271 case OCPPVersion.VERSION_16:
272 if (
ff9d1031
JB
273 (connectorId === 0 &&
274 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
5edd8ba0 275 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
276 ) !== -1) ||
277 (connectorId > 0 &&
278 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
5edd8ba0 279 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 280 ) !== -1)
48b75072
JB
281 ) {
282 transitionAllowed = true;
283 }
284 break;
285 case OCPPVersion.VERSION_20:
286 case OCPPVersion.VERSION_201:
287 if (
ff9d1031
JB
288 (connectorId === 0 &&
289 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
5edd8ba0 290 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
291 ) !== -1) ||
292 (connectorId > 0 &&
293 OCPP20Constants.ConnectorStatusTransitions.findIndex(
5edd8ba0 294 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 295 ) !== -1)
48b75072
JB
296 ) {
297 transitionAllowed = true;
298 }
299 break;
300 default:
301 throw new BaseError(
302 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
5398cecf 303 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
48b75072
JB
304 );
305 }
306 if (transitionAllowed === false) {
307 logger.warn(
5398cecf
JB
308 `${chargingStation.logPrefix()} OCPP ${chargingStation.stationInfo
309 ?.ocppVersion} connector id ${connectorId} status transition from '${
e1d9a0f4 310 chargingStation.getConnectorStatus(connectorId)!.status
5edd8ba0 311 }' to '${status}' is not allowed`,
48b75072
JB
312 );
313 }
314 return transitionAllowed;
315 }
316
7164966d 317 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 318 relativePath: string,
1b271a54
JB
319 ocppVersion: OCPPVersion,
320 moduleName?: string,
5edd8ba0 321 methodName?: string,
7164966d 322 ): JSONSchemaType<T> {
d972af76 323 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d 324 try {
d972af76 325 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
7164966d 326 } catch (error) {
fa5995d6 327 handleFileException(
7164966d
JB
328 filePath,
329 FileType.JsonSchema,
330 error as NodeJS.ErrnoException,
1b271a54 331 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
5edd8ba0 332 { throwError: false },
7164966d 333 );
e1d9a0f4 334 return {} as JSONSchemaType<T>;
7164966d 335 }
130783a7
JB
336 }
337
884a6fdf
JB
338 protected static getSampledValueTemplate(
339 chargingStation: ChargingStation,
340 connectorId: number,
341 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
5edd8ba0 342 phase?: MeterValuePhase,
884a6fdf
JB
343 ): SampledValueTemplate | undefined {
344 const onPhaseStr = phase ? `on phase ${phase} ` : '';
c3da35d4 345 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
884a6fdf 346 logger.warn(
5edd8ba0 347 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
348 );
349 return;
350 }
351 if (
352 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
f2d5e3d9 353 getConfigurationKey(
884a6fdf 354 chargingStation,
5edd8ba0 355 StandardParametersKey.MeterValuesSampledData,
72092cfc 356 )?.value?.includes(measurand) === false
884a6fdf
JB
357 ) {
358 logger.debug(
54ebb82c 359 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf 360 StandardParametersKey.MeterValuesSampledData
5edd8ba0 361 }' OCPP parameter`,
884a6fdf
JB
362 );
363 return;
364 }
365 const sampledValueTemplates: SampledValueTemplate[] =
e1d9a0f4 366 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
884a6fdf
JB
367 for (
368 let index = 0;
9bf0ef23 369 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
370 index++
371 ) {
372 if (
c3da35d4 373 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
884a6fdf 374 sampledValueTemplates[index]?.measurand ??
5edd8ba0 375 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
884a6fdf
JB
376 ) === false
377 ) {
378 logger.warn(
5edd8ba0 379 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
380 );
381 } else if (
382 phase &&
383 sampledValueTemplates[index]?.phase === phase &&
384 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 385 getConfigurationKey(
884a6fdf 386 chargingStation,
5edd8ba0 387 StandardParametersKey.MeterValuesSampledData,
72092cfc 388 )?.value?.includes(measurand) === true
884a6fdf
JB
389 ) {
390 return sampledValueTemplates[index];
391 } else if (
392 !phase &&
1c9de2b9 393 !sampledValueTemplates[index]?.phase &&
884a6fdf 394 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 395 getConfigurationKey(
884a6fdf 396 chargingStation,
5edd8ba0 397 StandardParametersKey.MeterValuesSampledData,
72092cfc 398 )?.value?.includes(measurand) === true
884a6fdf
JB
399 ) {
400 return sampledValueTemplates[index];
401 } else if (
402 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
1c9de2b9
JB
403 (!sampledValueTemplates[index]?.measurand ||
404 sampledValueTemplates[index]?.measurand === measurand)
884a6fdf
JB
405 ) {
406 return sampledValueTemplates[index];
407 }
408 }
409 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
54ebb82c 410 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
411 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
412 throw new BaseError(errorMsg);
413 }
414 logger.debug(
5edd8ba0 415 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
416 );
417 }
418
90befdb8
JB
419 protected static getLimitFromSampledValueTemplateCustomValue(
420 value: string,
421 limit: number,
77684af8 422 options?: { limitationEnabled?: boolean; unitMultiplier?: number; defaultValue?: number },
90befdb8 423 ): number {
20f0b76c
JB
424 options = {
425 ...{
426 limitationEnabled: true,
427 unitMultiplier: 1,
77684af8 428 defaultValue: 0,
20f0b76c
JB
429 },
430 ...options,
431 };
77684af8
JB
432 const parsedValue = parseInt(value);
433 if (options?.limitationEnabled) {
16aee3cf 434 return min((!isNaN(parsedValue) ? parsedValue : Infinity) * options.unitMultiplier!, limit);
77684af8
JB
435 }
436 return (!isNaN(parsedValue) ? parsedValue : options.defaultValue!) * options.unitMultiplier!;
90befdb8 437 }
7164966d 438
ae725be3
JB
439 private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean {
440 return (
ae725be3
JB
441 chargingStation.hasIdTags() === true &&
442 isNotEmptyString(
443 chargingStation.idTagsCache
444 .getIdTags(getIdTagsFile(chargingStation.stationInfo)!)
445 ?.find((tag) => tag === idTag),
446 )
447 );
448 }
449
450 private static async isIdTagRemoteAuthorized(
451 chargingStation: ChargingStation,
452 connectorId: number,
453 idTag: string,
454 ): Promise<boolean> {
455 chargingStation.getConnectorStatus(connectorId)!.authorizeIdTag = idTag;
456 return (
457 (
458 await chargingStation.ocppRequestService.requestHandler<
459 AuthorizeRequest,
460 AuthorizeResponse
461 >(chargingStation, RequestCommand.AUTHORIZE, {
462 idTag,
463 })
464 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
465 );
466 }
467
1b271a54
JB
468 private static logPrefix = (
469 ocppVersion: OCPPVersion,
470 moduleName?: string,
5edd8ba0 471 methodName?: string,
1b271a54
JB
472 ): string => {
473 const logMsg =
9bf0ef23 474 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54
JB
475 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
476 : ` OCPP ${ocppVersion} |`;
9bf0ef23 477 return logPrefix(logMsg);
8b7072dc 478 };
90befdb8 479}