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