refactor(ui): order types export
[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
4c3c0d59
JB
5import { OCPP16Constants } from './1.6/OCPP16Constants';
6import { OCPP20Constants } from './2.0/OCPP20Constants';
2896e06d 7import { type ChargingStation, ChargingStationConfigurationUtils } from '../../charging-station';
268a74bb 8import { BaseError } from '../../exception';
6e939d9e 9import {
268a74bb
JB
10 ChargePointErrorCode,
11 type ConnectorStatusEnum,
12 ErrorType,
13 FileType,
6e939d9e 14 IncomingRequestCommand,
268a74bb
JB
15 type JsonObject,
16 type JsonType,
6e939d9e 17 MessageTrigger,
268a74bb
JB
18 MessageType,
19 MeterValueMeasurand,
20 type MeterValuePhase,
21 type OCPP16StatusNotificationRequest,
22 type OCPP20StatusNotificationRequest,
23 OCPPVersion,
6e939d9e 24 RequestCommand,
268a74bb
JB
25 type SampledValueTemplate,
26 StandardParametersKey,
6e939d9e 27 type StatusNotificationRequest,
48b75072 28 type StatusNotificationResponse,
268a74bb 29} from '../../types';
60a74391 30import { Constants, FileUtils, Utils, logger } from '../../utils';
06ad945f 31
90befdb8 32export class OCPPServiceUtils {
d5bd1c00
JB
33 protected constructor() {
34 // This is intentional
35 }
36
01a4dcbb 37 public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType {
06ad945f
JB
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
2cc5d5ec
JB
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';
336f2829
JB
61 default:
62 return 'unknown';
2cc5d5ec
JB
63 }
64 }
65
ed3d2808 66 public static isRequestCommandSupported(
fd3c56d1
JB
67 chargingStation: ChargingStation,
68 command: RequestCommand
ed3d2808 69 ): boolean {
edd13439 70 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
ed3d2808
JB
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(
fd3c56d1
JB
87 chargingStation: ChargingStation,
88 command: IncomingRequestCommand
ed3d2808 89 ): boolean {
edd13439
JB
90 const isIncomingRequestCommand =
91 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
ed3d2808
JB
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
c60ed4b8
JB
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(
2585c6e9 130 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`
c60ed4b8
JB
131 );
132 return false;
133 }
134 return true;
135 }
136
1799761a 137 public static convertDateToISOString<T extends JsonType>(obj: T): void {
02887891
JB
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') {
b30ea3f0 142 OCPPServiceUtils.convertDateToISOString<T>(obj[key] as T);
1799761a
JB
143 }
144 }
145 }
146
6e939d9e
JB
147 public static buildStatusNotificationRequest(
148 chargingStation: ChargingStation,
149 connectorId: number,
12f26d4a
JB
150 status: ConnectorStatusEnum,
151 evseId?: number
6e939d9e 152 ): StatusNotificationRequest {
cda96260 153 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
6e939d9e
JB
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,
12f26d4a 166 evseId,
6e939d9e 167 } as OCPP20StatusNotificationRequest;
cda96260
JB
168 default:
169 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
6e939d9e
JB
170 }
171 }
172
8f953431
JB
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
48b75072
JB
181 public static async sendAndSetConnectorStatus(
182 chargingStation: ChargingStation,
183 connectorId: number,
12f26d4a
JB
184 status: ConnectorStatusEnum,
185 evseId?: number
48b75072
JB
186 ) {
187 OCPPServiceUtils.checkConnectorStatusTransition(chargingStation, connectorId, status);
188 await chargingStation.ocppRequestService.requestHandler<
189 StatusNotificationRequest,
190 StatusNotificationResponse
191 >(
192 chargingStation,
193 RequestCommand.STATUS_NOTIFICATION,
12f26d4a 194 OCPPServiceUtils.buildStatusNotificationRequest(chargingStation, connectorId, status, evseId)
48b75072
JB
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 (
ff9d1031
JB
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)
48b75072
JB
217 ) {
218 transitionAllowed = true;
219 }
220 break;
221 case OCPPVersion.VERSION_20:
222 case OCPPVersion.VERSION_201:
223 if (
ff9d1031
JB
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)
48b75072
JB
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
54ebb82c 246 } connector id ${connectorId} status transition from '${
48b75072
JB
247 chargingStation.getConnectorStatus(connectorId).status
248 }' to '${status}' is not allowed`
249 );
250 }
251 return transitionAllowed;
252 }
253
7164966d
JB
254 protected static parseJsonSchemaFile<T extends JsonType>(
255 filePath: string,
1b271a54
JB
256 ocppVersion: OCPPVersion,
257 moduleName?: string,
258 methodName?: string
7164966d
JB
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,
1b271a54 267 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
7164966d
JB
268 { throwError: false }
269 );
270 }
130783a7
JB
271 }
272
884a6fdf
JB
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(
54ebb82c 282 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
884a6fdf
JB
283 );
284 return;
285 }
286 if (
287 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
23290150 288 ChargingStationConfigurationUtils.getConfigurationKey(
884a6fdf
JB
289 chargingStation,
290 StandardParametersKey.MeterValuesSampledData
72092cfc 291 )?.value?.includes(measurand) === false
884a6fdf
JB
292 ) {
293 logger.debug(
54ebb82c 294 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf
JB
295 StandardParametersKey.MeterValuesSampledData
296 }' OCPP parameter`
297 );
298 return;
299 }
300 const sampledValueTemplates: SampledValueTemplate[] =
72092cfc 301 chargingStation.getConnectorStatus(connectorId)?.MeterValues;
884a6fdf
JB
302 for (
303 let index = 0;
53ac516c 304 Utils.isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
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(
54ebb82c 314 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
884a6fdf
JB
315 );
316 } else if (
317 phase &&
318 sampledValueTemplates[index]?.phase === phase &&
319 sampledValueTemplates[index]?.measurand === measurand &&
320 ChargingStationConfigurationUtils.getConfigurationKey(
321 chargingStation,
322 StandardParametersKey.MeterValuesSampledData
72092cfc 323 )?.value?.includes(measurand) === true
884a6fdf
JB
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
72092cfc 333 )?.value?.includes(measurand) === true
884a6fdf
JB
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) {
54ebb82c 345 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
346 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
347 throw new BaseError(errorMsg);
348 }
349 logger.debug(
54ebb82c 350 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
884a6fdf
JB
351 );
352 }
353
90befdb8
JB
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;
231d1ecd
JB
364 const parsedInt = parseInt(value);
365 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
90befdb8 366 return options?.limitationEnabled
f126aa15
JB
367 ? Math.min(numberValue * options.unitMultiplier, limit)
368 : numberValue * options.unitMultiplier;
90befdb8 369 }
7164966d 370
1b271a54
JB
371 private static logPrefix = (
372 ocppVersion: OCPPVersion,
373 moduleName?: string,
374 methodName?: string
375 ): string => {
376 const logMsg =
5a2a53cf 377 Utils.isNotEmptyString(moduleName) && Utils.isNotEmptyString(methodName)
1b271a54
JB
378 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
379 : ` OCPP ${ocppVersion} |`;
380 return Utils.logPrefix(logMsg);
8b7072dc 381 };
90befdb8 382}