build(deps-dev): apply updates
[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
7 import { OCPP16Constants } from './1.6/OCPP16Constants';
8 import { OCPP20Constants } from './2.0/OCPP20Constants';
9 import { OCPPConstants } from './OCPPConstants';
10 import { type ChargingStation, ChargingStationConfigurationUtils } from '../../charging-station';
11 import { BaseError } from '../../exception';
12 import {
13 ChargePointErrorCode,
14 type ConnectorStatusEnum,
15 ErrorType,
16 FileType,
17 IncomingRequestCommand,
18 type JsonObject,
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 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);
152 }
153 }
154 }
155
156 public static buildStatusNotificationRequest(
157 chargingStation: ChargingStation,
158 connectorId: number,
159 status: ConnectorStatusEnum,
160 evseId?: number,
161 ): StatusNotificationRequest {
162 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
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,
175 evseId,
176 } as OCPP20StatusNotificationRequest;
177 default:
178 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
179 }
180 }
181
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
190 public static async sendAndSetConnectorStatus(
191 chargingStation: ChargingStation,
192 connectorId: number,
193 status: ConnectorStatusEnum,
194 evseId?: number,
195 options?: { send: boolean },
196 ) {
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,
210 evseId,
211 ),
212 );
213 }
214 chargingStation.getConnectorStatus(connectorId)!.status = status;
215 }
216
217 protected static checkConnectorStatusTransition(
218 chargingStation: ChargingStation,
219 connectorId: number,
220 status: ConnectorStatusEnum,
221 ): boolean {
222 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
223 let transitionAllowed = false;
224 switch (chargingStation.stationInfo.ocppVersion) {
225 case OCPPVersion.VERSION_16:
226 if (
227 (connectorId === 0 &&
228 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
229 (transition) => transition.from === fromStatus && transition.to === status,
230 ) !== -1) ||
231 (connectorId > 0 &&
232 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
233 (transition) => transition.from === fromStatus && transition.to === status,
234 ) !== -1)
235 ) {
236 transitionAllowed = true;
237 }
238 break;
239 case OCPPVersion.VERSION_20:
240 case OCPPVersion.VERSION_201:
241 if (
242 (connectorId === 0 &&
243 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
244 (transition) => transition.from === fromStatus && transition.to === status,
245 ) !== -1) ||
246 (connectorId > 0 &&
247 OCPP20Constants.ConnectorStatusTransitions.findIndex(
248 (transition) => transition.from === fromStatus && transition.to === status,
249 ) !== -1)
250 ) {
251 transitionAllowed = true;
252 }
253 break;
254 default:
255 throw new BaseError(
256 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
257 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
258 );
259 }
260 if (transitionAllowed === false) {
261 logger.warn(
262 `${chargingStation.logPrefix()} OCPP ${
263 chargingStation.stationInfo.ocppVersion
264 } connector id ${connectorId} status transition from '${
265 chargingStation.getConnectorStatus(connectorId)!.status
266 }' to '${status}' is not allowed`,
267 );
268 }
269 return transitionAllowed;
270 }
271
272 protected static parseJsonSchemaFile<T extends JsonType>(
273 relativePath: string,
274 ocppVersion: OCPPVersion,
275 moduleName?: string,
276 methodName?: string,
277 ): JSONSchemaType<T> {
278 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
279 try {
280 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
281 } catch (error) {
282 handleFileException(
283 filePath,
284 FileType.JsonSchema,
285 error as NodeJS.ErrnoException,
286 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
287 { throwError: false },
288 );
289 return {} as JSONSchemaType<T>;
290 }
291 }
292
293 protected static getSampledValueTemplate(
294 chargingStation: ChargingStation,
295 connectorId: number,
296 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
297 phase?: MeterValuePhase,
298 ): SampledValueTemplate | undefined {
299 const onPhaseStr = phase ? `on phase ${phase} ` : '';
300 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
301 logger.warn(
302 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
303 );
304 return;
305 }
306 if (
307 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
308 ChargingStationConfigurationUtils.getConfigurationKey(
309 chargingStation,
310 StandardParametersKey.MeterValuesSampledData,
311 )?.value?.includes(measurand) === false
312 ) {
313 logger.debug(
314 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
315 StandardParametersKey.MeterValuesSampledData
316 }' OCPP parameter`,
317 );
318 return;
319 }
320 const sampledValueTemplates: SampledValueTemplate[] =
321 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
322 for (
323 let index = 0;
324 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
325 index++
326 ) {
327 if (
328 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
329 sampledValueTemplates[index]?.measurand ??
330 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
331 ) === false
332 ) {
333 logger.warn(
334 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
335 );
336 } else if (
337 phase &&
338 sampledValueTemplates[index]?.phase === phase &&
339 sampledValueTemplates[index]?.measurand === measurand &&
340 ChargingStationConfigurationUtils.getConfigurationKey(
341 chargingStation,
342 StandardParametersKey.MeterValuesSampledData,
343 )?.value?.includes(measurand) === true
344 ) {
345 return sampledValueTemplates[index];
346 } else if (
347 !phase &&
348 !sampledValueTemplates[index].phase &&
349 sampledValueTemplates[index]?.measurand === measurand &&
350 ChargingStationConfigurationUtils.getConfigurationKey(
351 chargingStation,
352 StandardParametersKey.MeterValuesSampledData,
353 )?.value?.includes(measurand) === true
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) {
365 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
366 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
367 throw new BaseError(errorMsg);
368 }
369 logger.debug(
370 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
371 );
372 }
373
374 protected static getLimitFromSampledValueTemplateCustomValue(
375 value: string,
376 limit: number,
377 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
378 limitationEnabled: true,
379 unitMultiplier: 1,
380 },
381 ): number {
382 options = {
383 ...{
384 limitationEnabled: true,
385 unitMultiplier: 1,
386 },
387 ...options,
388 };
389 const parsedInt = parseInt(value);
390 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
391 return options?.limitationEnabled
392 ? Math.min(numberValue * options.unitMultiplier!, limit)
393 : numberValue * options.unitMultiplier!;
394 }
395
396 private static logPrefix = (
397 ocppVersion: OCPPVersion,
398 moduleName?: string,
399 methodName?: string,
400 ): string => {
401 const logMsg =
402 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
403 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
404 : ` OCPP ${ocppVersion} |`;
405 return logPrefix(logMsg);
406 };
407 }