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