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