build(simulator): don't preserve modules
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
1 import fs from 'node:fs';
2 import path from 'node:path';
3 import { fileURLToPath } from 'node:url';
4
5 import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv';
6
7 import { OCPP16Constants } from './1.6/OCPP16Constants';
8 import { OCPP20Constants } from './2.0/OCPP20Constants';
9 import { type ChargingStation, ChargingStationConfigurationUtils } from '../../charging-station';
10 import { BaseError } from '../../exception';
11 import {
12 ChargePointErrorCode,
13 type ConnectorStatusEnum,
14 ErrorType,
15 FileType,
16 IncomingRequestCommand,
17 type JsonObject,
18 type JsonType,
19 MessageTrigger,
20 MessageType,
21 MeterValueMeasurand,
22 type MeterValuePhase,
23 type OCPP16StatusNotificationRequest,
24 type OCPP20StatusNotificationRequest,
25 OCPPVersion,
26 RequestCommand,
27 type SampledValueTemplate,
28 StandardParametersKey,
29 type StatusNotificationRequest,
30 type StatusNotificationResponse,
31 } from '../../types';
32 import { Constants, ErrorUtils, Utils, logger } from '../../utils';
33
34 export class OCPPServiceUtils {
35 protected constructor() {
36 // This is intentional
37 }
38
39 public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType {
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
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';
63 default:
64 return 'unknown';
65 }
66 }
67
68 public static isRequestCommandSupported(
69 chargingStation: ChargingStation,
70 command: RequestCommand
71 ): boolean {
72 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
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(
89 chargingStation: ChargingStation,
90 command: IncomingRequestCommand
91 ): boolean {
92 const isIncomingRequestCommand =
93 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
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
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(
132 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`
133 );
134 return false;
135 }
136 return true;
137 }
138
139 public static convertDateToISOString<T extends JsonType>(obj: T): void {
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') {
144 OCPPServiceUtils.convertDateToISOString<T>(obj[key] as T);
145 }
146 }
147 }
148
149 public static buildStatusNotificationRequest(
150 chargingStation: ChargingStation,
151 connectorId: number,
152 status: ConnectorStatusEnum,
153 evseId?: number
154 ): StatusNotificationRequest {
155 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
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,
168 evseId,
169 } as OCPP20StatusNotificationRequest;
170 default:
171 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
172 }
173 }
174
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
183 public static async sendAndSetConnectorStatus(
184 chargingStation: ChargingStation,
185 connectorId: number,
186 status: ConnectorStatusEnum,
187 evseId?: number
188 ) {
189 OCPPServiceUtils.checkConnectorStatusTransition(chargingStation, connectorId, status);
190 await chargingStation.ocppRequestService.requestHandler<
191 StatusNotificationRequest,
192 StatusNotificationResponse
193 >(
194 chargingStation,
195 RequestCommand.STATUS_NOTIFICATION,
196 OCPPServiceUtils.buildStatusNotificationRequest(chargingStation, connectorId, status, evseId)
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 (
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)
219 ) {
220 transitionAllowed = true;
221 }
222 break;
223 case OCPPVersion.VERSION_20:
224 case OCPPVersion.VERSION_201:
225 if (
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)
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
248 } connector id ${connectorId} status transition from '${
249 chargingStation.getConnectorStatus(connectorId).status
250 }' to '${status}' is not allowed`
251 );
252 }
253 return transitionAllowed;
254 }
255
256 protected static parseJsonSchemaFile<T extends JsonType>(
257 relativePath: string,
258 ocppVersion: OCPPVersion,
259 moduleName?: string,
260 methodName?: string
261 ): JSONSchemaType<T> {
262 const filePath = path.join(path.dirname(fileURLToPath(import.meta.url)), relativePath);
263 try {
264 return JSON.parse(fs.readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
265 } catch (error) {
266 ErrorUtils.handleFileException(
267 filePath,
268 FileType.JsonSchema,
269 error as NodeJS.ErrnoException,
270 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
271 { throwError: false }
272 );
273 }
274 }
275
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(
285 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
286 );
287 return;
288 }
289 if (
290 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
291 ChargingStationConfigurationUtils.getConfigurationKey(
292 chargingStation,
293 StandardParametersKey.MeterValuesSampledData
294 )?.value?.includes(measurand) === false
295 ) {
296 logger.debug(
297 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
298 StandardParametersKey.MeterValuesSampledData
299 }' OCPP parameter`
300 );
301 return;
302 }
303 const sampledValueTemplates: SampledValueTemplate[] =
304 chargingStation.getConnectorStatus(connectorId)?.MeterValues;
305 for (
306 let index = 0;
307 Utils.isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
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(
317 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
318 );
319 } else if (
320 phase &&
321 sampledValueTemplates[index]?.phase === phase &&
322 sampledValueTemplates[index]?.measurand === measurand &&
323 ChargingStationConfigurationUtils.getConfigurationKey(
324 chargingStation,
325 StandardParametersKey.MeterValuesSampledData
326 )?.value?.includes(measurand) === true
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
336 )?.value?.includes(measurand) === true
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) {
348 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
349 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
350 throw new BaseError(errorMsg);
351 }
352 logger.debug(
353 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
354 );
355 }
356
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;
367 const parsedInt = parseInt(value);
368 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
369 return options?.limitationEnabled
370 ? Math.min(numberValue * options.unitMultiplier, limit)
371 : numberValue * options.unitMultiplier;
372 }
373
374 private static logPrefix = (
375 ocppVersion: OCPPVersion,
376 moduleName?: string,
377 methodName?: string
378 ): string => {
379 const logMsg =
380 Utils.isNotEmptyString(moduleName) && Utils.isNotEmptyString(methodName)
381 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
382 : ` OCPP ${ocppVersion} |`;
383 return Utils.logPrefix(logMsg);
384 };
385 }