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