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