refactor: organize constants
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
CommitLineData
7164966d 1import fs from 'node:fs';
51022aa0
JB
2import path from 'node:path';
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';
51022aa0 33import { Constants, ErrorUtils, Utils, 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
JB
187 status: ConnectorStatusEnum,
188 evseId?: number
48b75072
JB
189 ) {
190 OCPPServiceUtils.checkConnectorStatusTransition(chargingStation, connectorId, status);
191 await chargingStation.ocppRequestService.requestHandler<
192 StatusNotificationRequest,
193 StatusNotificationResponse
194 >(
195 chargingStation,
196 RequestCommand.STATUS_NOTIFICATION,
12f26d4a 197 OCPPServiceUtils.buildStatusNotificationRequest(chargingStation, connectorId, status, evseId)
48b75072
JB
198 );
199 chargingStation.getConnectorStatus(connectorId).status = status;
200 }
201
202 protected static checkConnectorStatusTransition(
203 chargingStation: ChargingStation,
204 connectorId: number,
205 status: ConnectorStatusEnum
206 ): boolean {
207 const fromStatus = chargingStation.getConnectorStatus(connectorId).status;
208 let transitionAllowed = false;
209 switch (chargingStation.stationInfo.ocppVersion) {
210 case OCPPVersion.VERSION_16:
211 if (
ff9d1031
JB
212 (connectorId === 0 &&
213 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
214 (transition) => transition.from === fromStatus && transition.to === status
215 ) !== -1) ||
216 (connectorId > 0 &&
217 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
218 (transition) => transition.from === fromStatus && transition.to === status
219 ) !== -1)
48b75072
JB
220 ) {
221 transitionAllowed = true;
222 }
223 break;
224 case OCPPVersion.VERSION_20:
225 case OCPPVersion.VERSION_201:
226 if (
ff9d1031
JB
227 (connectorId === 0 &&
228 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
229 (transition) => transition.from === fromStatus && transition.to === status
230 ) !== -1) ||
231 (connectorId > 0 &&
232 OCPP20Constants.ConnectorStatusTransitions.findIndex(
233 (transition) => transition.from === fromStatus && transition.to === status
234 ) !== -1)
48b75072
JB
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 257 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 258 relativePath: string,
1b271a54
JB
259 ocppVersion: OCPPVersion,
260 moduleName?: string,
261 methodName?: string
7164966d 262 ): JSONSchemaType<T> {
51022aa0 263 const filePath = path.join(path.dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d
JB
264 try {
265 return JSON.parse(fs.readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
266 } catch (error) {
51022aa0 267 ErrorUtils.handleFileException(
7164966d
JB
268 filePath,
269 FileType.JsonSchema,
270 error as NodeJS.ErrnoException,
1b271a54 271 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
7164966d
JB
272 { throwError: false }
273 );
274 }
130783a7
JB
275 }
276
884a6fdf
JB
277 protected static getSampledValueTemplate(
278 chargingStation: ChargingStation,
279 connectorId: number,
280 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
281 phase?: MeterValuePhase
282 ): SampledValueTemplate | undefined {
283 const onPhaseStr = phase ? `on phase ${phase} ` : '';
c3da35d4 284 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
884a6fdf 285 logger.warn(
54ebb82c 286 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
884a6fdf
JB
287 );
288 return;
289 }
290 if (
291 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
23290150 292 ChargingStationConfigurationUtils.getConfigurationKey(
884a6fdf
JB
293 chargingStation,
294 StandardParametersKey.MeterValuesSampledData
72092cfc 295 )?.value?.includes(measurand) === false
884a6fdf
JB
296 ) {
297 logger.debug(
54ebb82c 298 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf
JB
299 StandardParametersKey.MeterValuesSampledData
300 }' OCPP parameter`
301 );
302 return;
303 }
304 const sampledValueTemplates: SampledValueTemplate[] =
72092cfc 305 chargingStation.getConnectorStatus(connectorId)?.MeterValues;
884a6fdf
JB
306 for (
307 let index = 0;
53ac516c 308 Utils.isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
309 index++
310 ) {
311 if (
c3da35d4 312 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
884a6fdf
JB
313 sampledValueTemplates[index]?.measurand ??
314 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
315 ) === false
316 ) {
317 logger.warn(
54ebb82c 318 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
884a6fdf
JB
319 );
320 } else if (
321 phase &&
322 sampledValueTemplates[index]?.phase === phase &&
323 sampledValueTemplates[index]?.measurand === measurand &&
324 ChargingStationConfigurationUtils.getConfigurationKey(
325 chargingStation,
326 StandardParametersKey.MeterValuesSampledData
72092cfc 327 )?.value?.includes(measurand) === true
884a6fdf
JB
328 ) {
329 return sampledValueTemplates[index];
330 } else if (
331 !phase &&
332 !sampledValueTemplates[index].phase &&
333 sampledValueTemplates[index]?.measurand === measurand &&
334 ChargingStationConfigurationUtils.getConfigurationKey(
335 chargingStation,
336 StandardParametersKey.MeterValuesSampledData
72092cfc 337 )?.value?.includes(measurand) === true
884a6fdf
JB
338 ) {
339 return sampledValueTemplates[index];
340 } else if (
341 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
342 (!sampledValueTemplates[index].measurand ||
343 sampledValueTemplates[index].measurand === measurand)
344 ) {
345 return sampledValueTemplates[index];
346 }
347 }
348 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
54ebb82c 349 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
350 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
351 throw new BaseError(errorMsg);
352 }
353 logger.debug(
54ebb82c 354 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
884a6fdf
JB
355 );
356 }
357
90befdb8
JB
358 protected static getLimitFromSampledValueTemplateCustomValue(
359 value: string,
360 limit: number,
361 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
362 limitationEnabled: true,
363 unitMultiplier: 1,
364 }
365 ): number {
366 options.limitationEnabled = options?.limitationEnabled ?? true;
367 options.unitMultiplier = options?.unitMultiplier ?? 1;
231d1ecd
JB
368 const parsedInt = parseInt(value);
369 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
90befdb8 370 return options?.limitationEnabled
f126aa15
JB
371 ? Math.min(numberValue * options.unitMultiplier, limit)
372 : numberValue * options.unitMultiplier;
90befdb8 373 }
7164966d 374
1b271a54
JB
375 private static logPrefix = (
376 ocppVersion: OCPPVersion,
377 moduleName?: string,
378 methodName?: string
379 ): string => {
380 const logMsg =
5a2a53cf 381 Utils.isNotEmptyString(moduleName) && Utils.isNotEmptyString(methodName)
1b271a54
JB
382 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
383 : ` OCPP ${ocppVersion} |`;
384 return Utils.logPrefix(logMsg);
8b7072dc 385 };
90befdb8 386}