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