refactor: remove unneeded code in configuration handling
[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 &&
1c9de2b9 91 chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command]
ed3d2808 92 ) {
1c9de2b9 93 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command];
ed3d2808
JB
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 &&
1c9de2b9 112 chargingStation.stationInfo?.commandsSupport?.incomingCommands?.[command]
ed3d2808 113 ) {
1c9de2b9 114 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command];
ed3d2808
JB
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;
1c9de2b9
JB
127 } else if (
128 isMessageTrigger === true &&
129 chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger]
130 ) {
131 return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger];
c60ed4b8
JB
132 }
133 logger.error(
5edd8ba0 134 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
c60ed4b8
JB
135 );
136 return false;
137 }
138
139 public static isConnectorIdValid(
140 chargingStation: ChargingStation,
141 ocppCommand: IncomingRequestCommand,
5edd8ba0 142 connectorId: number,
c60ed4b8
JB
143 ): boolean {
144 if (connectorId < 0) {
145 logger.error(
5edd8ba0 146 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
c60ed4b8
JB
147 );
148 return false;
149 }
150 return true;
151 }
152
1799761a 153 public static convertDateToISOString<T extends JsonType>(obj: T): void {
a37fc6dc
JB
154 for (const key in obj) {
155 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
3dcf7b67 156 if (isDate(obj![key])) {
a37fc6dc
JB
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
e1d9a0f4 160 } else if (obj![key] !== null && typeof obj![key] === 'object') {
a37fc6dc 161 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 162 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T);
1799761a
JB
163 }
164 }
165 }
166
6e939d9e
JB
167 public static buildStatusNotificationRequest(
168 chargingStation: ChargingStation,
169 connectorId: number,
12f26d4a 170 status: ConnectorStatusEnum,
5edd8ba0 171 evseId?: number,
6e939d9e 172 ): StatusNotificationRequest {
cda96260 173 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
6e939d9e
JB
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,
12f26d4a 186 evseId,
6e939d9e 187 } as OCPP20StatusNotificationRequest;
cda96260
JB
188 default:
189 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
6e939d9e
JB
190 }
191 }
192
8f953431
JB
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
48b75072
JB
201 public static async sendAndSetConnectorStatus(
202 chargingStation: ChargingStation,
203 connectorId: number,
12f26d4a 204 status: ConnectorStatusEnum,
ec94a3cf 205 evseId?: number,
e1d9a0f4 206 options?: { send: boolean },
48b75072 207 ) {
ec94a3cf
JB
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,
5edd8ba0
JB
221 evseId,
222 ),
ec94a3cf
JB
223 );
224 }
e1d9a0f4 225 chargingStation.getConnectorStatus(connectorId)!.status = status;
48b75072
JB
226 }
227
ae725be3
JB
228 public static async isIdTagAuthorized(
229 chargingStation: ChargingStation,
230 connectorId: number,
231 idTag: string,
232 ): Promise<boolean> {
cfdf901d
JB
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 }
cfdf901d
JB
238 if (
239 chargingStation.getLocalAuthListEnabled() === true &&
240 OCPPServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)
241 ) {
ae725be3
JB
242 const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!;
243 connectorStatus.localAuthorizeIdTag = idTag;
244 connectorStatus.idTagLocalAuthorized = true;
ec54600d 245 return true;
cfdf901d 246 } else if (chargingStation.getRemoteAuthorization()) {
ec54600d 247 return await OCPPServiceUtils.isIdTagRemoteAuthorized(chargingStation, connectorId, idTag);
ae725be3 248 }
ec54600d 249 return false;
ae725be3
JB
250 }
251
48b75072
JB
252 protected static checkConnectorStatusTransition(
253 chargingStation: ChargingStation,
254 connectorId: number,
5edd8ba0 255 status: ConnectorStatusEnum,
48b75072 256 ): boolean {
e1d9a0f4 257 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
48b75072
JB
258 let transitionAllowed = false;
259 switch (chargingStation.stationInfo.ocppVersion) {
260 case OCPPVersion.VERSION_16:
261 if (
ff9d1031
JB
262 (connectorId === 0 &&
263 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
5edd8ba0 264 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
265 ) !== -1) ||
266 (connectorId > 0 &&
267 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
5edd8ba0 268 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 269 ) !== -1)
48b75072
JB
270 ) {
271 transitionAllowed = true;
272 }
273 break;
274 case OCPPVersion.VERSION_20:
275 case OCPPVersion.VERSION_201:
276 if (
ff9d1031
JB
277 (connectorId === 0 &&
278 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
5edd8ba0 279 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
280 ) !== -1) ||
281 (connectorId > 0 &&
282 OCPP20Constants.ConnectorStatusTransitions.findIndex(
5edd8ba0 283 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 284 ) !== -1)
48b75072
JB
285 ) {
286 transitionAllowed = true;
287 }
288 break;
289 default:
290 throw new BaseError(
291 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
5edd8ba0 292 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
48b75072
JB
293 );
294 }
295 if (transitionAllowed === false) {
296 logger.warn(
297 `${chargingStation.logPrefix()} OCPP ${
298 chargingStation.stationInfo.ocppVersion
54ebb82c 299 } connector id ${connectorId} status transition from '${
e1d9a0f4 300 chargingStation.getConnectorStatus(connectorId)!.status
5edd8ba0 301 }' to '${status}' is not allowed`,
48b75072
JB
302 );
303 }
304 return transitionAllowed;
305 }
306
7164966d 307 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 308 relativePath: string,
1b271a54
JB
309 ocppVersion: OCPPVersion,
310 moduleName?: string,
5edd8ba0 311 methodName?: string,
7164966d 312 ): JSONSchemaType<T> {
d972af76 313 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d 314 try {
d972af76 315 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
7164966d 316 } catch (error) {
fa5995d6 317 handleFileException(
7164966d
JB
318 filePath,
319 FileType.JsonSchema,
320 error as NodeJS.ErrnoException,
1b271a54 321 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
5edd8ba0 322 { throwError: false },
7164966d 323 );
e1d9a0f4 324 return {} as JSONSchemaType<T>;
7164966d 325 }
130783a7
JB
326 }
327
884a6fdf
JB
328 protected static getSampledValueTemplate(
329 chargingStation: ChargingStation,
330 connectorId: number,
331 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
5edd8ba0 332 phase?: MeterValuePhase,
884a6fdf
JB
333 ): SampledValueTemplate | undefined {
334 const onPhaseStr = phase ? `on phase ${phase} ` : '';
c3da35d4 335 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
884a6fdf 336 logger.warn(
5edd8ba0 337 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
338 );
339 return;
340 }
341 if (
342 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
f2d5e3d9 343 getConfigurationKey(
884a6fdf 344 chargingStation,
5edd8ba0 345 StandardParametersKey.MeterValuesSampledData,
72092cfc 346 )?.value?.includes(measurand) === false
884a6fdf
JB
347 ) {
348 logger.debug(
54ebb82c 349 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf 350 StandardParametersKey.MeterValuesSampledData
5edd8ba0 351 }' OCPP parameter`,
884a6fdf
JB
352 );
353 return;
354 }
355 const sampledValueTemplates: SampledValueTemplate[] =
e1d9a0f4 356 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
884a6fdf
JB
357 for (
358 let index = 0;
9bf0ef23 359 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
360 index++
361 ) {
362 if (
c3da35d4 363 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
884a6fdf 364 sampledValueTemplates[index]?.measurand ??
5edd8ba0 365 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
884a6fdf
JB
366 ) === false
367 ) {
368 logger.warn(
5edd8ba0 369 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
370 );
371 } else if (
372 phase &&
373 sampledValueTemplates[index]?.phase === phase &&
374 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 375 getConfigurationKey(
884a6fdf 376 chargingStation,
5edd8ba0 377 StandardParametersKey.MeterValuesSampledData,
72092cfc 378 )?.value?.includes(measurand) === true
884a6fdf
JB
379 ) {
380 return sampledValueTemplates[index];
381 } else if (
382 !phase &&
1c9de2b9 383 !sampledValueTemplates[index]?.phase &&
884a6fdf 384 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 385 getConfigurationKey(
884a6fdf 386 chargingStation,
5edd8ba0 387 StandardParametersKey.MeterValuesSampledData,
72092cfc 388 )?.value?.includes(measurand) === true
884a6fdf
JB
389 ) {
390 return sampledValueTemplates[index];
391 } else if (
392 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
1c9de2b9
JB
393 (!sampledValueTemplates[index]?.measurand ||
394 sampledValueTemplates[index]?.measurand === measurand)
884a6fdf
JB
395 ) {
396 return sampledValueTemplates[index];
397 }
398 }
399 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
54ebb82c 400 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
401 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
402 throw new BaseError(errorMsg);
403 }
404 logger.debug(
5edd8ba0 405 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
406 );
407 }
408
90befdb8
JB
409 protected static getLimitFromSampledValueTemplateCustomValue(
410 value: string,
411 limit: number,
7f3decca 412 options?: { limitationEnabled?: boolean; unitMultiplier?: number },
90befdb8 413 ): number {
20f0b76c
JB
414 options = {
415 ...{
416 limitationEnabled: true,
417 unitMultiplier: 1,
418 },
419 ...options,
420 };
231d1ecd
JB
421 const parsedInt = parseInt(value);
422 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
90befdb8 423 return options?.limitationEnabled
e1d9a0f4
JB
424 ? Math.min(numberValue * options.unitMultiplier!, limit)
425 : numberValue * options.unitMultiplier!;
90befdb8 426 }
7164966d 427
ae725be3
JB
428 private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean {
429 return (
ae725be3
JB
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
1b271a54
JB
457 private static logPrefix = (
458 ocppVersion: OCPPVersion,
459 moduleName?: string,
5edd8ba0 460 methodName?: string,
1b271a54
JB
461 ): string => {
462 const logMsg =
9bf0ef23 463 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54
JB
464 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
465 : ` OCPP ${ocppVersion} |`;
9bf0ef23 466 return logPrefix(logMsg);
8b7072dc 467 };
90befdb8 468}