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