test: code cleanup
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
1 import { readFileSync } from 'node:fs';
2 import { dirname, join } from 'node:path';
3 import { fileURLToPath } from 'node:url';
4
5 import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv';
6 import { isDate } from 'date-fns';
7
8 import { OCPP16Constants } from './1.6/OCPP16Constants';
9 import { OCPP20Constants } from './2.0/OCPP20Constants';
10 import { OCPPConstants } from './OCPPConstants';
11 import { type ChargingStation, getConfigurationKey } from '../../charging-station';
12 import { BaseError } from '../../exception';
13 import {
14 ChargePointErrorCode,
15 type ConnectorStatusEnum,
16 ErrorType,
17 FileType,
18 IncomingRequestCommand,
19 type JsonType,
20 MessageTrigger,
21 MessageType,
22 MeterValueMeasurand,
23 type MeterValuePhase,
24 type OCPP16StatusNotificationRequest,
25 type OCPP20StatusNotificationRequest,
26 OCPPVersion,
27 RequestCommand,
28 type SampledValueTemplate,
29 StandardParametersKey,
30 type StatusNotificationRequest,
31 type StatusNotificationResponse,
32 } from '../../types';
33 import {
34 handleFileException,
35 isNotEmptyArray,
36 isNotEmptyString,
37 logPrefix,
38 logger,
39 } from '../../utils';
40
41 export class OCPPServiceUtils {
42 protected constructor() {
43 // This is intentional
44 }
45
46 public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType {
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
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';
70 default:
71 return 'unknown';
72 }
73 }
74
75 public static isRequestCommandSupported(
76 chargingStation: ChargingStation,
77 command: RequestCommand,
78 ): boolean {
79 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
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(
96 chargingStation: ChargingStation,
97 command: IncomingRequestCommand,
98 ): boolean {
99 const isIncomingRequestCommand =
100 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
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
116 public static isMessageTriggerSupported(
117 chargingStation: ChargingStation,
118 messageTrigger: MessageTrigger,
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(
127 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
128 );
129 return false;
130 }
131
132 public static isConnectorIdValid(
133 chargingStation: ChargingStation,
134 ocppCommand: IncomingRequestCommand,
135 connectorId: number,
136 ): boolean {
137 if (connectorId < 0) {
138 logger.error(
139 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
140 );
141 return false;
142 }
143 return true;
144 }
145
146 public static convertDateToISOString<T extends JsonType>(obj: T): void {
147 for (const key in obj) {
148 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
149 if (isDate(obj![key])) {
150 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
151 (obj![key] as string) = (obj![key] as Date).toISOString();
152 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
153 } else if (obj![key] !== null && typeof obj![key] === 'object') {
154 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
155 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T);
156 }
157 }
158 }
159
160 public static buildStatusNotificationRequest(
161 chargingStation: ChargingStation,
162 connectorId: number,
163 status: ConnectorStatusEnum,
164 evseId?: number,
165 ): StatusNotificationRequest {
166 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
167 case OCPPVersion.VERSION_16:
168 return {
169 connectorId,
170 status,
171 errorCode: ChargePointErrorCode.NO_ERROR,
172 } as OCPP16StatusNotificationRequest;
173 case OCPPVersion.VERSION_20:
174 case OCPPVersion.VERSION_201:
175 return {
176 timestamp: new Date(),
177 connectorStatus: status,
178 connectorId,
179 evseId,
180 } as OCPP20StatusNotificationRequest;
181 default:
182 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
183 }
184 }
185
186 public static startHeartbeatInterval(chargingStation: ChargingStation, interval: number): void {
187 if (!chargingStation.heartbeatSetInterval) {
188 chargingStation.startHeartbeat();
189 } else if (chargingStation.getHeartbeatInterval() !== interval) {
190 chargingStation.restartHeartbeat();
191 }
192 }
193
194 public static async sendAndSetConnectorStatus(
195 chargingStation: ChargingStation,
196 connectorId: number,
197 status: ConnectorStatusEnum,
198 evseId?: number,
199 options?: { send: boolean },
200 ) {
201 options = { send: true, ...options };
202 if (options.send) {
203 OCPPServiceUtils.checkConnectorStatusTransition(chargingStation, connectorId, status);
204 await chargingStation.ocppRequestService.requestHandler<
205 StatusNotificationRequest,
206 StatusNotificationResponse
207 >(
208 chargingStation,
209 RequestCommand.STATUS_NOTIFICATION,
210 OCPPServiceUtils.buildStatusNotificationRequest(
211 chargingStation,
212 connectorId,
213 status,
214 evseId,
215 ),
216 );
217 }
218 chargingStation.getConnectorStatus(connectorId)!.status = status;
219 }
220
221 protected static checkConnectorStatusTransition(
222 chargingStation: ChargingStation,
223 connectorId: number,
224 status: ConnectorStatusEnum,
225 ): boolean {
226 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
227 let transitionAllowed = false;
228 switch (chargingStation.stationInfo.ocppVersion) {
229 case OCPPVersion.VERSION_16:
230 if (
231 (connectorId === 0 &&
232 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
233 (transition) => transition.from === fromStatus && transition.to === status,
234 ) !== -1) ||
235 (connectorId > 0 &&
236 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
237 (transition) => transition.from === fromStatus && transition.to === status,
238 ) !== -1)
239 ) {
240 transitionAllowed = true;
241 }
242 break;
243 case OCPPVersion.VERSION_20:
244 case OCPPVersion.VERSION_201:
245 if (
246 (connectorId === 0 &&
247 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
248 (transition) => transition.from === fromStatus && transition.to === status,
249 ) !== -1) ||
250 (connectorId > 0 &&
251 OCPP20Constants.ConnectorStatusTransitions.findIndex(
252 (transition) => transition.from === fromStatus && transition.to === status,
253 ) !== -1)
254 ) {
255 transitionAllowed = true;
256 }
257 break;
258 default:
259 throw new BaseError(
260 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
261 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
262 );
263 }
264 if (transitionAllowed === false) {
265 logger.warn(
266 `${chargingStation.logPrefix()} OCPP ${
267 chargingStation.stationInfo.ocppVersion
268 } connector id ${connectorId} status transition from '${
269 chargingStation.getConnectorStatus(connectorId)!.status
270 }' to '${status}' is not allowed`,
271 );
272 }
273 return transitionAllowed;
274 }
275
276 protected static parseJsonSchemaFile<T extends JsonType>(
277 relativePath: string,
278 ocppVersion: OCPPVersion,
279 moduleName?: string,
280 methodName?: string,
281 ): JSONSchemaType<T> {
282 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
283 try {
284 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
285 } catch (error) {
286 handleFileException(
287 filePath,
288 FileType.JsonSchema,
289 error as NodeJS.ErrnoException,
290 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
291 { throwError: false },
292 );
293 return {} as JSONSchemaType<T>;
294 }
295 }
296
297 protected static getSampledValueTemplate(
298 chargingStation: ChargingStation,
299 connectorId: number,
300 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
301 phase?: MeterValuePhase,
302 ): SampledValueTemplate | undefined {
303 const onPhaseStr = phase ? `on phase ${phase} ` : '';
304 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
305 logger.warn(
306 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
307 );
308 return;
309 }
310 if (
311 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
312 getConfigurationKey(
313 chargingStation,
314 StandardParametersKey.MeterValuesSampledData,
315 )?.value?.includes(measurand) === false
316 ) {
317 logger.debug(
318 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
319 StandardParametersKey.MeterValuesSampledData
320 }' OCPP parameter`,
321 );
322 return;
323 }
324 const sampledValueTemplates: SampledValueTemplate[] =
325 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
326 for (
327 let index = 0;
328 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
329 index++
330 ) {
331 if (
332 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
333 sampledValueTemplates[index]?.measurand ??
334 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
335 ) === false
336 ) {
337 logger.warn(
338 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
339 );
340 } else if (
341 phase &&
342 sampledValueTemplates[index]?.phase === phase &&
343 sampledValueTemplates[index]?.measurand === measurand &&
344 getConfigurationKey(
345 chargingStation,
346 StandardParametersKey.MeterValuesSampledData,
347 )?.value?.includes(measurand) === true
348 ) {
349 return sampledValueTemplates[index];
350 } else if (
351 !phase &&
352 !sampledValueTemplates[index].phase &&
353 sampledValueTemplates[index]?.measurand === measurand &&
354 getConfigurationKey(
355 chargingStation,
356 StandardParametersKey.MeterValuesSampledData,
357 )?.value?.includes(measurand) === true
358 ) {
359 return sampledValueTemplates[index];
360 } else if (
361 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
362 (!sampledValueTemplates[index].measurand ||
363 sampledValueTemplates[index].measurand === measurand)
364 ) {
365 return sampledValueTemplates[index];
366 }
367 }
368 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
369 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
370 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
371 throw new BaseError(errorMsg);
372 }
373 logger.debug(
374 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
375 );
376 }
377
378 protected static getLimitFromSampledValueTemplateCustomValue(
379 value: string,
380 limit: number,
381 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
382 limitationEnabled: true,
383 unitMultiplier: 1,
384 },
385 ): number {
386 options = {
387 ...{
388 limitationEnabled: true,
389 unitMultiplier: 1,
390 },
391 ...options,
392 };
393 const parsedInt = parseInt(value);
394 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
395 return options?.limitationEnabled
396 ? Math.min(numberValue * options.unitMultiplier!, limit)
397 : numberValue * options.unitMultiplier!;
398 }
399
400 private static logPrefix = (
401 ocppVersion: OCPPVersion,
402 moduleName?: string,
403 methodName?: string,
404 ): string => {
405 const logMsg =
406 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
407 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
408 : ` OCPP ${ocppVersion} |`;
409 return logPrefix(logMsg);
410 };
411 }