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