fix: ensure running transactions are stopped at CS stop
[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 {
cda96260 176 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
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> {
cfdf901d
JB
236 if (!chargingStation.getLocalAuthListEnabled() && !chargingStation.getRemoteAuthorization()) {
237 logger.warn(
238 `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`,
239 );
240 }
cfdf901d
JB
241 if (
242 chargingStation.getLocalAuthListEnabled() === true &&
243 OCPPServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)
244 ) {
ae725be3
JB
245 const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!;
246 connectorStatus.localAuthorizeIdTag = idTag;
247 connectorStatus.idTagLocalAuthorized = true;
ec54600d 248 return true;
cfdf901d 249 } else if (chargingStation.getRemoteAuthorization()) {
ec54600d 250 return await OCPPServiceUtils.isIdTagRemoteAuthorized(chargingStation, connectorId, idTag);
ae725be3 251 }
ec54600d 252 return false;
ae725be3
JB
253 }
254
48b75072
JB
255 protected static checkConnectorStatusTransition(
256 chargingStation: ChargingStation,
257 connectorId: number,
5edd8ba0 258 status: ConnectorStatusEnum,
48b75072 259 ): boolean {
e1d9a0f4 260 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
48b75072
JB
261 let transitionAllowed = false;
262 switch (chargingStation.stationInfo.ocppVersion) {
263 case OCPPVersion.VERSION_16:
264 if (
ff9d1031
JB
265 (connectorId === 0 &&
266 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
5edd8ba0 267 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
268 ) !== -1) ||
269 (connectorId > 0 &&
270 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
5edd8ba0 271 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 272 ) !== -1)
48b75072
JB
273 ) {
274 transitionAllowed = true;
275 }
276 break;
277 case OCPPVersion.VERSION_20:
278 case OCPPVersion.VERSION_201:
279 if (
ff9d1031
JB
280 (connectorId === 0 &&
281 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
5edd8ba0 282 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
283 ) !== -1) ||
284 (connectorId > 0 &&
285 OCPP20Constants.ConnectorStatusTransitions.findIndex(
5edd8ba0 286 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 287 ) !== -1)
48b75072
JB
288 ) {
289 transitionAllowed = true;
290 }
291 break;
292 default:
293 throw new BaseError(
294 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
5edd8ba0 295 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
48b75072
JB
296 );
297 }
298 if (transitionAllowed === false) {
299 logger.warn(
300 `${chargingStation.logPrefix()} OCPP ${
301 chargingStation.stationInfo.ocppVersion
54ebb82c 302 } connector id ${connectorId} status transition from '${
e1d9a0f4 303 chargingStation.getConnectorStatus(connectorId)!.status
5edd8ba0 304 }' to '${status}' is not allowed`,
48b75072
JB
305 );
306 }
307 return transitionAllowed;
308 }
309
7164966d 310 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 311 relativePath: string,
1b271a54
JB
312 ocppVersion: OCPPVersion,
313 moduleName?: string,
5edd8ba0 314 methodName?: string,
7164966d 315 ): JSONSchemaType<T> {
d972af76 316 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d 317 try {
d972af76 318 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
7164966d 319 } catch (error) {
fa5995d6 320 handleFileException(
7164966d
JB
321 filePath,
322 FileType.JsonSchema,
323 error as NodeJS.ErrnoException,
1b271a54 324 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
5edd8ba0 325 { throwError: false },
7164966d 326 );
e1d9a0f4 327 return {} as JSONSchemaType<T>;
7164966d 328 }
130783a7
JB
329 }
330
884a6fdf
JB
331 protected static getSampledValueTemplate(
332 chargingStation: ChargingStation,
333 connectorId: number,
334 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
5edd8ba0 335 phase?: MeterValuePhase,
884a6fdf
JB
336 ): SampledValueTemplate | undefined {
337 const onPhaseStr = phase ? `on phase ${phase} ` : '';
c3da35d4 338 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
884a6fdf 339 logger.warn(
5edd8ba0 340 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
341 );
342 return;
343 }
344 if (
345 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
f2d5e3d9 346 getConfigurationKey(
884a6fdf 347 chargingStation,
5edd8ba0 348 StandardParametersKey.MeterValuesSampledData,
72092cfc 349 )?.value?.includes(measurand) === false
884a6fdf
JB
350 ) {
351 logger.debug(
54ebb82c 352 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf 353 StandardParametersKey.MeterValuesSampledData
5edd8ba0 354 }' OCPP parameter`,
884a6fdf
JB
355 );
356 return;
357 }
358 const sampledValueTemplates: SampledValueTemplate[] =
e1d9a0f4 359 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
884a6fdf
JB
360 for (
361 let index = 0;
9bf0ef23 362 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
363 index++
364 ) {
365 if (
c3da35d4 366 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
884a6fdf 367 sampledValueTemplates[index]?.measurand ??
5edd8ba0 368 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
884a6fdf
JB
369 ) === false
370 ) {
371 logger.warn(
5edd8ba0 372 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
373 );
374 } else if (
375 phase &&
376 sampledValueTemplates[index]?.phase === phase &&
377 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 378 getConfigurationKey(
884a6fdf 379 chargingStation,
5edd8ba0 380 StandardParametersKey.MeterValuesSampledData,
72092cfc 381 )?.value?.includes(measurand) === true
884a6fdf
JB
382 ) {
383 return sampledValueTemplates[index];
384 } else if (
385 !phase &&
1c9de2b9 386 !sampledValueTemplates[index]?.phase &&
884a6fdf 387 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 388 getConfigurationKey(
884a6fdf 389 chargingStation,
5edd8ba0 390 StandardParametersKey.MeterValuesSampledData,
72092cfc 391 )?.value?.includes(measurand) === true
884a6fdf
JB
392 ) {
393 return sampledValueTemplates[index];
394 } else if (
395 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
1c9de2b9
JB
396 (!sampledValueTemplates[index]?.measurand ||
397 sampledValueTemplates[index]?.measurand === measurand)
884a6fdf
JB
398 ) {
399 return sampledValueTemplates[index];
400 }
401 }
402 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
54ebb82c 403 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
404 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
405 throw new BaseError(errorMsg);
406 }
407 logger.debug(
5edd8ba0 408 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
409 );
410 }
411
90befdb8
JB
412 protected static getLimitFromSampledValueTemplateCustomValue(
413 value: string,
414 limit: number,
7f3decca 415 options?: { limitationEnabled?: boolean; unitMultiplier?: number },
90befdb8 416 ): number {
20f0b76c
JB
417 options = {
418 ...{
419 limitationEnabled: true,
420 unitMultiplier: 1,
421 },
422 ...options,
423 };
231d1ecd
JB
424 const parsedInt = parseInt(value);
425 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
90befdb8 426 return options?.limitationEnabled
5adf6ca4 427 ? min(numberValue * options.unitMultiplier!, limit)
e1d9a0f4 428 : numberValue * options.unitMultiplier!;
90befdb8 429 }
7164966d 430
ae725be3
JB
431 private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean {
432 return (
ae725be3
JB
433 chargingStation.hasIdTags() === true &&
434 isNotEmptyString(
435 chargingStation.idTagsCache
436 .getIdTags(getIdTagsFile(chargingStation.stationInfo)!)
437 ?.find((tag) => tag === idTag),
438 )
439 );
440 }
441
442 private static async isIdTagRemoteAuthorized(
443 chargingStation: ChargingStation,
444 connectorId: number,
445 idTag: string,
446 ): Promise<boolean> {
447 chargingStation.getConnectorStatus(connectorId)!.authorizeIdTag = idTag;
448 return (
449 (
450 await chargingStation.ocppRequestService.requestHandler<
451 AuthorizeRequest,
452 AuthorizeResponse
453 >(chargingStation, RequestCommand.AUTHORIZE, {
454 idTag,
455 })
456 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
457 );
458 }
459
1b271a54
JB
460 private static logPrefix = (
461 ocppVersion: OCPPVersion,
462 moduleName?: string,
5edd8ba0 463 methodName?: string,
1b271a54
JB
464 ): string => {
465 const logMsg =
9bf0ef23 466 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54
JB
467 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
468 : ` OCPP ${ocppVersion} |`;
9bf0ef23 469 return logPrefix(logMsg);
8b7072dc 470 };
90befdb8 471}