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