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