test: code cleanup
[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';
3dcf7b67 6import { isDate } from 'date-fns';
06ad945f 7
4c3c0d59
JB
8import { OCPP16Constants } from './1.6/OCPP16Constants';
9import { OCPP20Constants } from './2.0/OCPP20Constants';
c3da35d4 10import { OCPPConstants } from './OCPPConstants';
f2d5e3d9 11import { type ChargingStation, getConfigurationKey } from '../../charging-station';
268a74bb 12import { BaseError } from '../../exception';
6e939d9e 13import {
268a74bb
JB
14 ChargePointErrorCode,
15 type ConnectorStatusEnum,
16 ErrorType,
17 FileType,
6e939d9e 18 IncomingRequestCommand,
268a74bb 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 {
a37fc6dc
JB
147 for (const key in obj) {
148 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
3dcf7b67 149 if (isDate(obj![key])) {
a37fc6dc
JB
150 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
151 (obj![key] as string) = (obj![key] as Date).toISOString();
152 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 153 } else if (obj![key] !== null && typeof obj![key] === 'object') {
a37fc6dc 154 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 155 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T);
1799761a
JB
156 }
157 }
158 }
159
6e939d9e
JB
160 public static buildStatusNotificationRequest(
161 chargingStation: ChargingStation,
162 connectorId: number,
12f26d4a 163 status: ConnectorStatusEnum,
5edd8ba0 164 evseId?: number,
6e939d9e 165 ): StatusNotificationRequest {
cda96260 166 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
6e939d9e
JB
167 case OCPPVersion.VERSION_16:
168 return {
169 connectorId,
170 status,
171 errorCode: ChargePointErrorCode.NO_ERROR,
172 } as OCPP16StatusNotificationRequest;
173 case OCPPVersion.VERSION_20:
174 case OCPPVersion.VERSION_201:
175 return {
176 timestamp: new Date(),
177 connectorStatus: status,
178 connectorId,
12f26d4a 179 evseId,
6e939d9e 180 } as OCPP20StatusNotificationRequest;
cda96260
JB
181 default:
182 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
6e939d9e
JB
183 }
184 }
185
8f953431
JB
186 public static startHeartbeatInterval(chargingStation: ChargingStation, interval: number): void {
187 if (!chargingStation.heartbeatSetInterval) {
188 chargingStation.startHeartbeat();
189 } else if (chargingStation.getHeartbeatInterval() !== interval) {
190 chargingStation.restartHeartbeat();
191 }
192 }
193
48b75072
JB
194 public static async sendAndSetConnectorStatus(
195 chargingStation: ChargingStation,
196 connectorId: number,
12f26d4a 197 status: ConnectorStatusEnum,
ec94a3cf 198 evseId?: number,
e1d9a0f4 199 options?: { send: boolean },
48b75072 200 ) {
ec94a3cf
JB
201 options = { send: true, ...options };
202 if (options.send) {
203 OCPPServiceUtils.checkConnectorStatusTransition(chargingStation, connectorId, status);
204 await chargingStation.ocppRequestService.requestHandler<
205 StatusNotificationRequest,
206 StatusNotificationResponse
207 >(
208 chargingStation,
209 RequestCommand.STATUS_NOTIFICATION,
210 OCPPServiceUtils.buildStatusNotificationRequest(
211 chargingStation,
212 connectorId,
213 status,
5edd8ba0
JB
214 evseId,
215 ),
ec94a3cf
JB
216 );
217 }
e1d9a0f4 218 chargingStation.getConnectorStatus(connectorId)!.status = status;
48b75072
JB
219 }
220
221 protected static checkConnectorStatusTransition(
222 chargingStation: ChargingStation,
223 connectorId: number,
5edd8ba0 224 status: ConnectorStatusEnum,
48b75072 225 ): boolean {
e1d9a0f4 226 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
48b75072
JB
227 let transitionAllowed = false;
228 switch (chargingStation.stationInfo.ocppVersion) {
229 case OCPPVersion.VERSION_16:
230 if (
ff9d1031
JB
231 (connectorId === 0 &&
232 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
5edd8ba0 233 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
234 ) !== -1) ||
235 (connectorId > 0 &&
236 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
5edd8ba0 237 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 238 ) !== -1)
48b75072
JB
239 ) {
240 transitionAllowed = true;
241 }
242 break;
243 case OCPPVersion.VERSION_20:
244 case OCPPVersion.VERSION_201:
245 if (
ff9d1031
JB
246 (connectorId === 0 &&
247 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
5edd8ba0 248 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
249 ) !== -1) ||
250 (connectorId > 0 &&
251 OCPP20Constants.ConnectorStatusTransitions.findIndex(
5edd8ba0 252 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 253 ) !== -1)
48b75072
JB
254 ) {
255 transitionAllowed = true;
256 }
257 break;
258 default:
259 throw new BaseError(
260 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
5edd8ba0 261 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
48b75072
JB
262 );
263 }
264 if (transitionAllowed === false) {
265 logger.warn(
266 `${chargingStation.logPrefix()} OCPP ${
267 chargingStation.stationInfo.ocppVersion
54ebb82c 268 } connector id ${connectorId} status transition from '${
e1d9a0f4 269 chargingStation.getConnectorStatus(connectorId)!.status
5edd8ba0 270 }' to '${status}' is not allowed`,
48b75072
JB
271 );
272 }
273 return transitionAllowed;
274 }
275
7164966d 276 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 277 relativePath: string,
1b271a54
JB
278 ocppVersion: OCPPVersion,
279 moduleName?: string,
5edd8ba0 280 methodName?: string,
7164966d 281 ): JSONSchemaType<T> {
d972af76 282 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d 283 try {
d972af76 284 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
7164966d 285 } catch (error) {
fa5995d6 286 handleFileException(
7164966d
JB
287 filePath,
288 FileType.JsonSchema,
289 error as NodeJS.ErrnoException,
1b271a54 290 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
5edd8ba0 291 { throwError: false },
7164966d 292 );
e1d9a0f4 293 return {} as JSONSchemaType<T>;
7164966d 294 }
130783a7
JB
295 }
296
884a6fdf
JB
297 protected static getSampledValueTemplate(
298 chargingStation: ChargingStation,
299 connectorId: number,
300 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
5edd8ba0 301 phase?: MeterValuePhase,
884a6fdf
JB
302 ): SampledValueTemplate | undefined {
303 const onPhaseStr = phase ? `on phase ${phase} ` : '';
c3da35d4 304 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
884a6fdf 305 logger.warn(
5edd8ba0 306 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
307 );
308 return;
309 }
310 if (
311 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
f2d5e3d9 312 getConfigurationKey(
884a6fdf 313 chargingStation,
5edd8ba0 314 StandardParametersKey.MeterValuesSampledData,
72092cfc 315 )?.value?.includes(measurand) === false
884a6fdf
JB
316 ) {
317 logger.debug(
54ebb82c 318 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf 319 StandardParametersKey.MeterValuesSampledData
5edd8ba0 320 }' OCPP parameter`,
884a6fdf
JB
321 );
322 return;
323 }
324 const sampledValueTemplates: SampledValueTemplate[] =
e1d9a0f4 325 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
884a6fdf
JB
326 for (
327 let index = 0;
9bf0ef23 328 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
329 index++
330 ) {
331 if (
c3da35d4 332 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
884a6fdf 333 sampledValueTemplates[index]?.measurand ??
5edd8ba0 334 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
884a6fdf
JB
335 ) === false
336 ) {
337 logger.warn(
5edd8ba0 338 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
339 );
340 } else if (
341 phase &&
342 sampledValueTemplates[index]?.phase === phase &&
343 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 344 getConfigurationKey(
884a6fdf 345 chargingStation,
5edd8ba0 346 StandardParametersKey.MeterValuesSampledData,
72092cfc 347 )?.value?.includes(measurand) === true
884a6fdf
JB
348 ) {
349 return sampledValueTemplates[index];
350 } else if (
351 !phase &&
352 !sampledValueTemplates[index].phase &&
353 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 354 getConfigurationKey(
884a6fdf 355 chargingStation,
5edd8ba0 356 StandardParametersKey.MeterValuesSampledData,
72092cfc 357 )?.value?.includes(measurand) === true
884a6fdf
JB
358 ) {
359 return sampledValueTemplates[index];
360 } else if (
361 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
362 (!sampledValueTemplates[index].measurand ||
363 sampledValueTemplates[index].measurand === measurand)
364 ) {
365 return sampledValueTemplates[index];
366 }
367 }
368 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
54ebb82c 369 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
370 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
371 throw new BaseError(errorMsg);
372 }
373 logger.debug(
5edd8ba0 374 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
375 );
376 }
377
90befdb8
JB
378 protected static getLimitFromSampledValueTemplateCustomValue(
379 value: string,
380 limit: number,
381 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
382 limitationEnabled: true,
383 unitMultiplier: 1,
5edd8ba0 384 },
90befdb8 385 ): number {
20f0b76c
JB
386 options = {
387 ...{
388 limitationEnabled: true,
389 unitMultiplier: 1,
390 },
391 ...options,
392 };
231d1ecd
JB
393 const parsedInt = parseInt(value);
394 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
90befdb8 395 return options?.limitationEnabled
e1d9a0f4
JB
396 ? Math.min(numberValue * options.unitMultiplier!, limit)
397 : numberValue * options.unitMultiplier!;
90befdb8 398 }
7164966d 399
1b271a54
JB
400 private static logPrefix = (
401 ocppVersion: OCPPVersion,
402 moduleName?: string,
5edd8ba0 403 methodName?: string,
1b271a54
JB
404 ): string => {
405 const logMsg =
9bf0ef23 406 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54
JB
407 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
408 : ` OCPP ${ocppVersion} |`;
9bf0ef23 409 return logPrefix(logMsg);
8b7072dc 410 };
90befdb8 411}