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