refactor: cleanup imports
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
CommitLineData
d972af76
JB
1import { readFileSync } from 'node:fs';
2import { dirname, join } from 'node:path';
51022aa0 3import { fileURLToPath } from 'node:url';
7164966d
JB
4
5import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv';
06ad945f 6
4c3c0d59
JB
7import { OCPP16Constants } from './1.6/OCPP16Constants';
8import { OCPP20Constants } from './2.0/OCPP20Constants';
c3da35d4 9import { OCPPConstants } from './OCPPConstants';
2896e06d 10import { type ChargingStation, ChargingStationConfigurationUtils } from '../../charging-station';
268a74bb 11import { BaseError } from '../../exception';
6e939d9e 12import {
268a74bb
JB
13 ChargePointErrorCode,
14 type ConnectorStatusEnum,
15 ErrorType,
16 FileType,
6e939d9e 17 IncomingRequestCommand,
268a74bb
JB
18 type JsonObject,
19 type JsonType,
6e939d9e 20 MessageTrigger,
268a74bb
JB
21 MessageType,
22 MeterValueMeasurand,
23 type MeterValuePhase,
24 type OCPP16StatusNotificationRequest,
25 type OCPP20StatusNotificationRequest,
26 OCPPVersion,
6e939d9e 27 RequestCommand,
268a74bb
JB
28 type SampledValueTemplate,
29 StandardParametersKey,
6e939d9e 30 type StatusNotificationRequest,
48b75072 31 type StatusNotificationResponse,
268a74bb 32} from '../../types';
fa5995d6 33import { Utils, handleFileException, logger } from '../../utils';
06ad945f 34
90befdb8 35export class OCPPServiceUtils {
d5bd1c00
JB
36 protected constructor() {
37 // This is intentional
38 }
39
01a4dcbb 40 public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType {
06ad945f
JB
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
2cc5d5ec
JB
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';
336f2829
JB
64 default:
65 return 'unknown';
2cc5d5ec
JB
66 }
67 }
68
ed3d2808 69 public static isRequestCommandSupported(
fd3c56d1
JB
70 chargingStation: ChargingStation,
71 command: RequestCommand
ed3d2808 72 ): boolean {
edd13439 73 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
ed3d2808
JB
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(
fd3c56d1
JB
90 chargingStation: ChargingStation,
91 command: IncomingRequestCommand
ed3d2808 92 ): boolean {
edd13439
JB
93 const isIncomingRequestCommand =
94 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
ed3d2808
JB
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
c60ed4b8
JB
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(
2585c6e9 133 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`
c60ed4b8
JB
134 );
135 return false;
136 }
137 return true;
138 }
139
1799761a 140 public static convertDateToISOString<T extends JsonType>(obj: T): void {
02887891
JB
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') {
b30ea3f0 145 OCPPServiceUtils.convertDateToISOString<T>(obj[key] as T);
1799761a
JB
146 }
147 }
148 }
149
6e939d9e
JB
150 public static buildStatusNotificationRequest(
151 chargingStation: ChargingStation,
152 connectorId: number,
12f26d4a
JB
153 status: ConnectorStatusEnum,
154 evseId?: number
6e939d9e 155 ): StatusNotificationRequest {
cda96260 156 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
6e939d9e
JB
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,
12f26d4a 169 evseId,
6e939d9e 170 } as OCPP20StatusNotificationRequest;
cda96260
JB
171 default:
172 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
6e939d9e
JB
173 }
174 }
175
8f953431
JB
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
48b75072
JB
184 public static async sendAndSetConnectorStatus(
185 chargingStation: ChargingStation,
186 connectorId: number,
12f26d4a 187 status: ConnectorStatusEnum,
ec94a3cf
JB
188 evseId?: number,
189 options: { send: boolean } = { send: true }
48b75072 190 ) {
ec94a3cf
JB
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 }
48b75072
JB
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 (
ff9d1031
JB
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)
48b75072
JB
229 ) {
230 transitionAllowed = true;
231 }
232 break;
233 case OCPPVersion.VERSION_20:
234 case OCPPVersion.VERSION_201:
235 if (
ff9d1031
JB
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)
48b75072
JB
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
54ebb82c 258 } connector id ${connectorId} status transition from '${
48b75072
JB
259 chargingStation.getConnectorStatus(connectorId).status
260 }' to '${status}' is not allowed`
261 );
262 }
263 return transitionAllowed;
264 }
265
7164966d 266 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 267 relativePath: string,
1b271a54
JB
268 ocppVersion: OCPPVersion,
269 moduleName?: string,
270 methodName?: string
7164966d 271 ): JSONSchemaType<T> {
d972af76 272 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d 273 try {
d972af76 274 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
7164966d 275 } catch (error) {
fa5995d6 276 handleFileException(
7164966d
JB
277 filePath,
278 FileType.JsonSchema,
279 error as NodeJS.ErrnoException,
1b271a54 280 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
7164966d
JB
281 { throwError: false }
282 );
283 }
130783a7
JB
284 }
285
884a6fdf
JB
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} ` : '';
c3da35d4 293 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
884a6fdf 294 logger.warn(
54ebb82c 295 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
884a6fdf
JB
296 );
297 return;
298 }
299 if (
300 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
23290150 301 ChargingStationConfigurationUtils.getConfigurationKey(
884a6fdf
JB
302 chargingStation,
303 StandardParametersKey.MeterValuesSampledData
72092cfc 304 )?.value?.includes(measurand) === false
884a6fdf
JB
305 ) {
306 logger.debug(
54ebb82c 307 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf
JB
308 StandardParametersKey.MeterValuesSampledData
309 }' OCPP parameter`
310 );
311 return;
312 }
313 const sampledValueTemplates: SampledValueTemplate[] =
72092cfc 314 chargingStation.getConnectorStatus(connectorId)?.MeterValues;
884a6fdf
JB
315 for (
316 let index = 0;
53ac516c 317 Utils.isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
318 index++
319 ) {
320 if (
c3da35d4 321 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
884a6fdf
JB
322 sampledValueTemplates[index]?.measurand ??
323 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
324 ) === false
325 ) {
326 logger.warn(
54ebb82c 327 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
884a6fdf
JB
328 );
329 } else if (
330 phase &&
331 sampledValueTemplates[index]?.phase === 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 !phase &&
341 !sampledValueTemplates[index].phase &&
342 sampledValueTemplates[index]?.measurand === measurand &&
343 ChargingStationConfigurationUtils.getConfigurationKey(
344 chargingStation,
345 StandardParametersKey.MeterValuesSampledData
72092cfc 346 )?.value?.includes(measurand) === true
884a6fdf
JB
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) {
54ebb82c 358 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
359 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
360 throw new BaseError(errorMsg);
361 }
362 logger.debug(
54ebb82c 363 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
884a6fdf
JB
364 );
365 }
366
90befdb8
JB
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 {
20f0b76c
JB
375 options = {
376 ...{
377 limitationEnabled: true,
378 unitMultiplier: 1,
379 },
380 ...options,
381 };
231d1ecd
JB
382 const parsedInt = parseInt(value);
383 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
90befdb8 384 return options?.limitationEnabled
f126aa15
JB
385 ? Math.min(numberValue * options.unitMultiplier, limit)
386 : numberValue * options.unitMultiplier;
90befdb8 387 }
7164966d 388
1b271a54
JB
389 private static logPrefix = (
390 ocppVersion: OCPPVersion,
391 moduleName?: string,
392 methodName?: string
393 ): string => {
394 const logMsg =
5a2a53cf 395 Utils.isNotEmptyString(moduleName) && Utils.isNotEmptyString(methodName)
1b271a54
JB
396 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
397 : ` OCPP ${ocppVersion} |`;
398 return Utils.logPrefix(logMsg);
8b7072dc 399 };
90befdb8 400}