refactor: cleanup RFID tags authorization code
[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,
ae725be3 18 type ConnectorStatus,
268a74bb
JB
19 type ConnectorStatusEnum,
20 ErrorType,
21 FileType,
6e939d9e 22 IncomingRequestCommand,
268a74bb 23 type JsonType,
6e939d9e 24 MessageTrigger,
268a74bb
JB
25 MessageType,
26 MeterValueMeasurand,
27 type MeterValuePhase,
28 type OCPP16StatusNotificationRequest,
29 type OCPP20StatusNotificationRequest,
30 OCPPVersion,
6e939d9e 31 RequestCommand,
268a74bb
JB
32 type SampledValueTemplate,
33 StandardParametersKey,
6e939d9e 34 type StatusNotificationRequest,
48b75072 35 type StatusNotificationResponse,
268a74bb 36} from '../../types';
9bf0ef23
JB
37import {
38 handleFileException,
39 isNotEmptyArray,
40 isNotEmptyString,
41 logPrefix,
42 logger,
43} from '../../utils';
06ad945f 44
90befdb8 45export class OCPPServiceUtils {
d5bd1c00
JB
46 protected constructor() {
47 // This is intentional
48 }
49
01a4dcbb 50 public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType {
06ad945f
JB
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
2cc5d5ec
JB
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';
336f2829
JB
74 default:
75 return 'unknown';
2cc5d5ec
JB
76 }
77 }
78
ed3d2808 79 public static isRequestCommandSupported(
fd3c56d1 80 chargingStation: ChargingStation,
5edd8ba0 81 command: RequestCommand,
ed3d2808 82 ): boolean {
edd13439 83 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
ed3d2808
JB
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(
fd3c56d1 100 chargingStation: ChargingStation,
5edd8ba0 101 command: IncomingRequestCommand,
ed3d2808 102 ): boolean {
edd13439
JB
103 const isIncomingRequestCommand =
104 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
ed3d2808
JB
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
c60ed4b8
JB
120 public static isMessageTriggerSupported(
121 chargingStation: ChargingStation,
5edd8ba0 122 messageTrigger: MessageTrigger,
c60ed4b8
JB
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(
5edd8ba0 131 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
c60ed4b8
JB
132 );
133 return false;
134 }
135
136 public static isConnectorIdValid(
137 chargingStation: ChargingStation,
138 ocppCommand: IncomingRequestCommand,
5edd8ba0 139 connectorId: number,
c60ed4b8
JB
140 ): boolean {
141 if (connectorId < 0) {
142 logger.error(
5edd8ba0 143 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
c60ed4b8
JB
144 );
145 return false;
146 }
147 return true;
148 }
149
1799761a 150 public static convertDateToISOString<T extends JsonType>(obj: T): void {
a37fc6dc
JB
151 for (const key in obj) {
152 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
3dcf7b67 153 if (isDate(obj![key])) {
a37fc6dc
JB
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
e1d9a0f4 157 } else if (obj![key] !== null && typeof obj![key] === 'object') {
a37fc6dc 158 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 159 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T);
1799761a
JB
160 }
161 }
162 }
163
6e939d9e
JB
164 public static buildStatusNotificationRequest(
165 chargingStation: ChargingStation,
166 connectorId: number,
12f26d4a 167 status: ConnectorStatusEnum,
5edd8ba0 168 evseId?: number,
6e939d9e 169 ): StatusNotificationRequest {
cda96260 170 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
6e939d9e
JB
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,
12f26d4a 183 evseId,
6e939d9e 184 } as OCPP20StatusNotificationRequest;
cda96260
JB
185 default:
186 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
6e939d9e
JB
187 }
188 }
189
8f953431
JB
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
48b75072
JB
198 public static async sendAndSetConnectorStatus(
199 chargingStation: ChargingStation,
200 connectorId: number,
12f26d4a 201 status: ConnectorStatusEnum,
ec94a3cf 202 evseId?: number,
e1d9a0f4 203 options?: { send: boolean },
48b75072 204 ) {
ec94a3cf
JB
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,
5edd8ba0
JB
218 evseId,
219 ),
ec94a3cf
JB
220 );
221 }
e1d9a0f4 222 chargingStation.getConnectorStatus(connectorId)!.status = status;
48b75072
JB
223 }
224
ae725be3
JB
225 public static async isIdTagAuthorized(
226 chargingStation: ChargingStation,
227 connectorId: number,
228 idTag: string,
229 ): Promise<boolean> {
cfdf901d
JB
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 }
ae725be3 235 let authorized = false;
cfdf901d
JB
236 if (
237 chargingStation.getLocalAuthListEnabled() === true &&
238 OCPPServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)
239 ) {
ae725be3
JB
240 const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!;
241 connectorStatus.localAuthorizeIdTag = idTag;
242 connectorStatus.idTagLocalAuthorized = true;
243 authorized = true;
cfdf901d 244 } else if (chargingStation.getRemoteAuthorization()) {
ae725be3
JB
245 authorized = await OCPPServiceUtils.isIdTagRemoteAuthorized(
246 chargingStation,
247 connectorId,
248 idTag,
249 );
250 }
251 return authorized;
252 }
253
48b75072
JB
254 protected static checkConnectorStatusTransition(
255 chargingStation: ChargingStation,
256 connectorId: number,
5edd8ba0 257 status: ConnectorStatusEnum,
48b75072 258 ): boolean {
e1d9a0f4 259 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
48b75072
JB
260 let transitionAllowed = false;
261 switch (chargingStation.stationInfo.ocppVersion) {
262 case OCPPVersion.VERSION_16:
263 if (
ff9d1031
JB
264 (connectorId === 0 &&
265 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
5edd8ba0 266 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
267 ) !== -1) ||
268 (connectorId > 0 &&
269 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
5edd8ba0 270 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 271 ) !== -1)
48b75072
JB
272 ) {
273 transitionAllowed = true;
274 }
275 break;
276 case OCPPVersion.VERSION_20:
277 case OCPPVersion.VERSION_201:
278 if (
ff9d1031
JB
279 (connectorId === 0 &&
280 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
5edd8ba0 281 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
282 ) !== -1) ||
283 (connectorId > 0 &&
284 OCPP20Constants.ConnectorStatusTransitions.findIndex(
5edd8ba0 285 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 286 ) !== -1)
48b75072
JB
287 ) {
288 transitionAllowed = true;
289 }
290 break;
291 default:
292 throw new BaseError(
293 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
5edd8ba0 294 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
48b75072
JB
295 );
296 }
297 if (transitionAllowed === false) {
298 logger.warn(
299 `${chargingStation.logPrefix()} OCPP ${
300 chargingStation.stationInfo.ocppVersion
54ebb82c 301 } connector id ${connectorId} status transition from '${
e1d9a0f4 302 chargingStation.getConnectorStatus(connectorId)!.status
5edd8ba0 303 }' to '${status}' is not allowed`,
48b75072
JB
304 );
305 }
306 return transitionAllowed;
307 }
308
7164966d 309 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 310 relativePath: string,
1b271a54
JB
311 ocppVersion: OCPPVersion,
312 moduleName?: string,
5edd8ba0 313 methodName?: string,
7164966d 314 ): JSONSchemaType<T> {
d972af76 315 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d 316 try {
d972af76 317 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
7164966d 318 } catch (error) {
fa5995d6 319 handleFileException(
7164966d
JB
320 filePath,
321 FileType.JsonSchema,
322 error as NodeJS.ErrnoException,
1b271a54 323 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
5edd8ba0 324 { throwError: false },
7164966d 325 );
e1d9a0f4 326 return {} as JSONSchemaType<T>;
7164966d 327 }
130783a7
JB
328 }
329
884a6fdf
JB
330 protected static getSampledValueTemplate(
331 chargingStation: ChargingStation,
332 connectorId: number,
333 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
5edd8ba0 334 phase?: MeterValuePhase,
884a6fdf
JB
335 ): SampledValueTemplate | undefined {
336 const onPhaseStr = phase ? `on phase ${phase} ` : '';
c3da35d4 337 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
884a6fdf 338 logger.warn(
5edd8ba0 339 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
340 );
341 return;
342 }
343 if (
344 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
f2d5e3d9 345 getConfigurationKey(
884a6fdf 346 chargingStation,
5edd8ba0 347 StandardParametersKey.MeterValuesSampledData,
72092cfc 348 )?.value?.includes(measurand) === false
884a6fdf
JB
349 ) {
350 logger.debug(
54ebb82c 351 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf 352 StandardParametersKey.MeterValuesSampledData
5edd8ba0 353 }' OCPP parameter`,
884a6fdf
JB
354 );
355 return;
356 }
357 const sampledValueTemplates: SampledValueTemplate[] =
e1d9a0f4 358 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
884a6fdf
JB
359 for (
360 let index = 0;
9bf0ef23 361 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
362 index++
363 ) {
364 if (
c3da35d4 365 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
884a6fdf 366 sampledValueTemplates[index]?.measurand ??
5edd8ba0 367 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
884a6fdf
JB
368 ) === false
369 ) {
370 logger.warn(
5edd8ba0 371 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
372 );
373 } else if (
374 phase &&
375 sampledValueTemplates[index]?.phase === phase &&
376 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 377 getConfigurationKey(
884a6fdf 378 chargingStation,
5edd8ba0 379 StandardParametersKey.MeterValuesSampledData,
72092cfc 380 )?.value?.includes(measurand) === true
884a6fdf
JB
381 ) {
382 return sampledValueTemplates[index];
383 } else if (
384 !phase &&
385 !sampledValueTemplates[index].phase &&
386 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 387 getConfigurationKey(
884a6fdf 388 chargingStation,
5edd8ba0 389 StandardParametersKey.MeterValuesSampledData,
72092cfc 390 )?.value?.includes(measurand) === true
884a6fdf
JB
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) {
54ebb82c 402 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
403 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
404 throw new BaseError(errorMsg);
405 }
406 logger.debug(
5edd8ba0 407 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
408 );
409 }
410
90befdb8
JB
411 protected static getLimitFromSampledValueTemplateCustomValue(
412 value: string,
413 limit: number,
414 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
415 limitationEnabled: true,
416 unitMultiplier: 1,
5edd8ba0 417 },
90befdb8 418 ): number {
20f0b76c
JB
419 options = {
420 ...{
421 limitationEnabled: true,
422 unitMultiplier: 1,
423 },
424 ...options,
425 };
231d1ecd
JB
426 const parsedInt = parseInt(value);
427 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
90befdb8 428 return options?.limitationEnabled
e1d9a0f4
JB
429 ? Math.min(numberValue * options.unitMultiplier!, limit)
430 : numberValue * options.unitMultiplier!;
90befdb8 431 }
7164966d 432
ae725be3
JB
433 private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean {
434 return (
ae725be3
JB
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
1b271a54
JB
462 private static logPrefix = (
463 ocppVersion: OCPPVersion,
464 moduleName?: string,
5edd8ba0 465 methodName?: string,
1b271a54
JB
466 ): string => {
467 const logMsg =
9bf0ef23 468 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54
JB
469 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
470 : ` OCPP ${ocppVersion} |`;
9bf0ef23 471 return logPrefix(logMsg);
8b7072dc 472 };
90befdb8 473}