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