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