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) {
148 if (obj[key] instanceof Date) {
149 (obj as JsonObject)[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 } = { send: true },
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 }
290 }
291
292 protected static getSampledValueTemplate(
293 chargingStation: ChargingStation,
294 connectorId: number,
295 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
296 phase?: MeterValuePhase,
297 ): SampledValueTemplate | undefined {
298 const onPhaseStr = phase ? `on phase ${phase} ` : '';
299 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
300 logger.warn(
301 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
302 );
303 return;
304 }
305 if (
306 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
307 ChargingStationConfigurationUtils.getConfigurationKey(
308 chargingStation,
309 StandardParametersKey.MeterValuesSampledData,
310 )?.value?.includes(measurand) === false
311 ) {
312 logger.debug(
313 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
314 StandardParametersKey.MeterValuesSampledData
315 }' OCPP parameter`,
316 );
317 return;
318 }
319 const sampledValueTemplates: SampledValueTemplate[] =
320 chargingStation.getConnectorStatus(connectorId)?.MeterValues;
321 for (
322 let index = 0;
323 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
324 index++
325 ) {
326 if (
327 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
328 sampledValueTemplates[index]?.measurand ??
329 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
330 ) === false
331 ) {
332 logger.warn(
333 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
334 );
335 } else if (
336 phase &&
337 sampledValueTemplates[index]?.phase === phase &&
338 sampledValueTemplates[index]?.measurand === measurand &&
339 ChargingStationConfigurationUtils.getConfigurationKey(
340 chargingStation,
341 StandardParametersKey.MeterValuesSampledData,
342 )?.value?.includes(measurand) === true
343 ) {
344 return sampledValueTemplates[index];
345 } else if (
346 !phase &&
347 !sampledValueTemplates[index].phase &&
348 sampledValueTemplates[index]?.measurand === measurand &&
349 ChargingStationConfigurationUtils.getConfigurationKey(
350 chargingStation,
351 StandardParametersKey.MeterValuesSampledData,
352 )?.value?.includes(measurand) === true
353 ) {
354 return sampledValueTemplates[index];
355 } else if (
356 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
357 (!sampledValueTemplates[index].measurand ||
358 sampledValueTemplates[index].measurand === measurand)
359 ) {
360 return sampledValueTemplates[index];
361 }
362 }
363 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
364 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
365 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
366 throw new BaseError(errorMsg);
367 }
368 logger.debug(
369 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
370 );
371 }
372
373 protected static getLimitFromSampledValueTemplateCustomValue(
374 value: string,
375 limit: number,
376 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
377 limitationEnabled: true,
378 unitMultiplier: 1,
379 },
380 ): number {
381 options = {
382 ...{
383 limitationEnabled: true,
384 unitMultiplier: 1,
385 },
386 ...options,
387 };
388 const parsedInt = parseInt(value);
389 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
390 return options?.limitationEnabled
391 ? Math.min(numberValue * options.unitMultiplier, limit)
392 : numberValue * options.unitMultiplier;
393 }
394
395 private static logPrefix = (
396 ocppVersion: OCPPVersion,
397 moduleName?: string,
398 methodName?: string,
399 ): string => {
400 const logMsg =
401 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
402 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
403 : ` OCPP ${ocppVersion} |`;
404 return logPrefix(logMsg);
405 };
406 }