refactor: cleanup RFID tags authorization code
[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 let authorized = false;
236 if (
237 chargingStation.getLocalAuthListEnabled() === true &&
238 OCPPServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)
239 ) {
240 const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!;
241 connectorStatus.localAuthorizeIdTag = idTag;
242 connectorStatus.idTagLocalAuthorized = true;
243 authorized = true;
244 } else if (chargingStation.getRemoteAuthorization()) {
245 authorized = await OCPPServiceUtils.isIdTagRemoteAuthorized(
246 chargingStation,
247 connectorId,
248 idTag,
249 );
250 }
251 return authorized;
252 }
253
254 protected static checkConnectorStatusTransition(
255 chargingStation: ChargingStation,
256 connectorId: number,
257 status: ConnectorStatusEnum,
258 ): boolean {
259 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
260 let transitionAllowed = false;
261 switch (chargingStation.stationInfo.ocppVersion) {
262 case OCPPVersion.VERSION_16:
263 if (
264 (connectorId === 0 &&
265 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
266 (transition) => transition.from === fromStatus && transition.to === status,
267 ) !== -1) ||
268 (connectorId > 0 &&
269 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
270 (transition) => transition.from === fromStatus && transition.to === status,
271 ) !== -1)
272 ) {
273 transitionAllowed = true;
274 }
275 break;
276 case OCPPVersion.VERSION_20:
277 case OCPPVersion.VERSION_201:
278 if (
279 (connectorId === 0 &&
280 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
281 (transition) => transition.from === fromStatus && transition.to === status,
282 ) !== -1) ||
283 (connectorId > 0 &&
284 OCPP20Constants.ConnectorStatusTransitions.findIndex(
285 (transition) => transition.from === fromStatus && transition.to === status,
286 ) !== -1)
287 ) {
288 transitionAllowed = true;
289 }
290 break;
291 default:
292 throw new BaseError(
293 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
294 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
295 );
296 }
297 if (transitionAllowed === false) {
298 logger.warn(
299 `${chargingStation.logPrefix()} OCPP ${
300 chargingStation.stationInfo.ocppVersion
301 } connector id ${connectorId} status transition from '${
302 chargingStation.getConnectorStatus(connectorId)!.status
303 }' to '${status}' is not allowed`,
304 );
305 }
306 return transitionAllowed;
307 }
308
309 protected static parseJsonSchemaFile<T extends JsonType>(
310 relativePath: string,
311 ocppVersion: OCPPVersion,
312 moduleName?: string,
313 methodName?: string,
314 ): JSONSchemaType<T> {
315 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
316 try {
317 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
318 } catch (error) {
319 handleFileException(
320 filePath,
321 FileType.JsonSchema,
322 error as NodeJS.ErrnoException,
323 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
324 { throwError: false },
325 );
326 return {} as JSONSchemaType<T>;
327 }
328 }
329
330 protected static getSampledValueTemplate(
331 chargingStation: ChargingStation,
332 connectorId: number,
333 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
334 phase?: MeterValuePhase,
335 ): SampledValueTemplate | undefined {
336 const onPhaseStr = phase ? `on phase ${phase} ` : '';
337 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
338 logger.warn(
339 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
340 );
341 return;
342 }
343 if (
344 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
345 getConfigurationKey(
346 chargingStation,
347 StandardParametersKey.MeterValuesSampledData,
348 )?.value?.includes(measurand) === false
349 ) {
350 logger.debug(
351 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
352 StandardParametersKey.MeterValuesSampledData
353 }' OCPP parameter`,
354 );
355 return;
356 }
357 const sampledValueTemplates: SampledValueTemplate[] =
358 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
359 for (
360 let index = 0;
361 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
362 index++
363 ) {
364 if (
365 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
366 sampledValueTemplates[index]?.measurand ??
367 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
368 ) === false
369 ) {
370 logger.warn(
371 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
372 );
373 } else if (
374 phase &&
375 sampledValueTemplates[index]?.phase === phase &&
376 sampledValueTemplates[index]?.measurand === measurand &&
377 getConfigurationKey(
378 chargingStation,
379 StandardParametersKey.MeterValuesSampledData,
380 )?.value?.includes(measurand) === true
381 ) {
382 return sampledValueTemplates[index];
383 } else if (
384 !phase &&
385 !sampledValueTemplates[index].phase &&
386 sampledValueTemplates[index]?.measurand === measurand &&
387 getConfigurationKey(
388 chargingStation,
389 StandardParametersKey.MeterValuesSampledData,
390 )?.value?.includes(measurand) === true
391 ) {
392 return sampledValueTemplates[index];
393 } else if (
394 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
395 (!sampledValueTemplates[index].measurand ||
396 sampledValueTemplates[index].measurand === measurand)
397 ) {
398 return sampledValueTemplates[index];
399 }
400 }
401 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
402 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
403 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
404 throw new BaseError(errorMsg);
405 }
406 logger.debug(
407 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
408 );
409 }
410
411 protected static getLimitFromSampledValueTemplateCustomValue(
412 value: string,
413 limit: number,
414 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
415 limitationEnabled: true,
416 unitMultiplier: 1,
417 },
418 ): number {
419 options = {
420 ...{
421 limitationEnabled: true,
422 unitMultiplier: 1,
423 },
424 ...options,
425 };
426 const parsedInt = parseInt(value);
427 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
428 return options?.limitationEnabled
429 ? Math.min(numberValue * options.unitMultiplier!, limit)
430 : numberValue * options.unitMultiplier!;
431 }
432
433 private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean {
434 return (
435 chargingStation.hasIdTags() === true &&
436 isNotEmptyString(
437 chargingStation.idTagsCache
438 .getIdTags(getIdTagsFile(chargingStation.stationInfo)!)
439 ?.find((tag) => tag === idTag),
440 )
441 );
442 }
443
444 private static async isIdTagRemoteAuthorized(
445 chargingStation: ChargingStation,
446 connectorId: number,
447 idTag: string,
448 ): Promise<boolean> {
449 chargingStation.getConnectorStatus(connectorId)!.authorizeIdTag = idTag;
450 return (
451 (
452 await chargingStation.ocppRequestService.requestHandler<
453 AuthorizeRequest,
454 AuthorizeResponse
455 >(chargingStation, RequestCommand.AUTHORIZE, {
456 idTag,
457 })
458 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
459 );
460 }
461
462 private static logPrefix = (
463 ocppVersion: OCPPVersion,
464 moduleName?: string,
465 methodName?: string,
466 ): string => {
467 const logMsg =
468 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
469 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
470 : ` OCPP ${ocppVersion} |`;
471 return logPrefix(logMsg);
472 };
473 }