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