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