build(simulator): switch to strict type checking
[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 18 type JsonType,
6e939d9e 19 MessageTrigger,
268a74bb
JB
20 MessageType,
21 MeterValueMeasurand,
22 type MeterValuePhase,
23 type OCPP16StatusNotificationRequest,
24 type OCPP20StatusNotificationRequest,
25 OCPPVersion,
6e939d9e 26 RequestCommand,
268a74bb
JB
27 type SampledValueTemplate,
28 StandardParametersKey,
6e939d9e 29 type StatusNotificationRequest,
48b75072 30 type StatusNotificationResponse,
268a74bb 31} from '../../types';
9bf0ef23
JB
32import {
33 handleFileException,
34 isNotEmptyArray,
35 isNotEmptyString,
36 logPrefix,
37 logger,
38} from '../../utils';
06ad945f 39
90befdb8 40export class OCPPServiceUtils {
d5bd1c00
JB
41 protected constructor() {
42 // This is intentional
43 }
44
01a4dcbb 45 public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType {
06ad945f
JB
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
2cc5d5ec
JB
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';
336f2829
JB
69 default:
70 return 'unknown';
2cc5d5ec
JB
71 }
72 }
73
ed3d2808 74 public static isRequestCommandSupported(
fd3c56d1 75 chargingStation: ChargingStation,
5edd8ba0 76 command: RequestCommand,
ed3d2808 77 ): boolean {
edd13439 78 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
ed3d2808
JB
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(
fd3c56d1 95 chargingStation: ChargingStation,
5edd8ba0 96 command: IncomingRequestCommand,
ed3d2808 97 ): boolean {
edd13439
JB
98 const isIncomingRequestCommand =
99 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
ed3d2808
JB
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
c60ed4b8
JB
115 public static isMessageTriggerSupported(
116 chargingStation: ChargingStation,
5edd8ba0 117 messageTrigger: MessageTrigger,
c60ed4b8
JB
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(
5edd8ba0 126 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
c60ed4b8
JB
127 );
128 return false;
129 }
130
131 public static isConnectorIdValid(
132 chargingStation: ChargingStation,
133 ocppCommand: IncomingRequestCommand,
5edd8ba0 134 connectorId: number,
c60ed4b8
JB
135 ): boolean {
136 if (connectorId < 0) {
137 logger.error(
5edd8ba0 138 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
c60ed4b8
JB
139 );
140 return false;
141 }
142 return true;
143 }
144
1799761a 145 public static convertDateToISOString<T extends JsonType>(obj: T): void {
a37fc6dc
JB
146 for (const key in obj) {
147 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 148 if (obj![key] instanceof Date) {
a37fc6dc
JB
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
e1d9a0f4 152 } else if (obj![key] !== null && typeof obj![key] === 'object') {
a37fc6dc 153 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 154 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T);
1799761a
JB
155 }
156 }
157 }
158
6e939d9e
JB
159 public static buildStatusNotificationRequest(
160 chargingStation: ChargingStation,
161 connectorId: number,
12f26d4a 162 status: ConnectorStatusEnum,
5edd8ba0 163 evseId?: number,
6e939d9e 164 ): StatusNotificationRequest {
cda96260 165 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
6e939d9e
JB
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,
12f26d4a 178 evseId,
6e939d9e 179 } as OCPP20StatusNotificationRequest;
cda96260
JB
180 default:
181 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
6e939d9e
JB
182 }
183 }
184
8f953431
JB
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
48b75072
JB
193 public static async sendAndSetConnectorStatus(
194 chargingStation: ChargingStation,
195 connectorId: number,
12f26d4a 196 status: ConnectorStatusEnum,
ec94a3cf 197 evseId?: number,
e1d9a0f4 198 options?: { send: boolean },
48b75072 199 ) {
ec94a3cf
JB
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,
5edd8ba0
JB
213 evseId,
214 ),
ec94a3cf
JB
215 );
216 }
e1d9a0f4 217 chargingStation.getConnectorStatus(connectorId)!.status = status;
48b75072
JB
218 }
219
220 protected static checkConnectorStatusTransition(
221 chargingStation: ChargingStation,
222 connectorId: number,
5edd8ba0 223 status: ConnectorStatusEnum,
48b75072 224 ): boolean {
e1d9a0f4 225 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
48b75072
JB
226 let transitionAllowed = false;
227 switch (chargingStation.stationInfo.ocppVersion) {
228 case OCPPVersion.VERSION_16:
229 if (
ff9d1031
JB
230 (connectorId === 0 &&
231 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
5edd8ba0 232 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
233 ) !== -1) ||
234 (connectorId > 0 &&
235 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
5edd8ba0 236 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 237 ) !== -1)
48b75072
JB
238 ) {
239 transitionAllowed = true;
240 }
241 break;
242 case OCPPVersion.VERSION_20:
243 case OCPPVersion.VERSION_201:
244 if (
ff9d1031
JB
245 (connectorId === 0 &&
246 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
5edd8ba0 247 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
248 ) !== -1) ||
249 (connectorId > 0 &&
250 OCPP20Constants.ConnectorStatusTransitions.findIndex(
5edd8ba0 251 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 252 ) !== -1)
48b75072
JB
253 ) {
254 transitionAllowed = true;
255 }
256 break;
257 default:
258 throw new BaseError(
259 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
5edd8ba0 260 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
48b75072
JB
261 );
262 }
263 if (transitionAllowed === false) {
264 logger.warn(
265 `${chargingStation.logPrefix()} OCPP ${
266 chargingStation.stationInfo.ocppVersion
54ebb82c 267 } connector id ${connectorId} status transition from '${
e1d9a0f4 268 chargingStation.getConnectorStatus(connectorId)!.status
5edd8ba0 269 }' to '${status}' is not allowed`,
48b75072
JB
270 );
271 }
272 return transitionAllowed;
273 }
274
7164966d 275 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 276 relativePath: string,
1b271a54
JB
277 ocppVersion: OCPPVersion,
278 moduleName?: string,
5edd8ba0 279 methodName?: string,
7164966d 280 ): JSONSchemaType<T> {
d972af76 281 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d 282 try {
d972af76 283 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
7164966d 284 } catch (error) {
fa5995d6 285 handleFileException(
7164966d
JB
286 filePath,
287 FileType.JsonSchema,
288 error as NodeJS.ErrnoException,
1b271a54 289 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
5edd8ba0 290 { throwError: false },
7164966d 291 );
e1d9a0f4 292 return {} as JSONSchemaType<T>;
7164966d 293 }
130783a7
JB
294 }
295
884a6fdf
JB
296 protected static getSampledValueTemplate(
297 chargingStation: ChargingStation,
298 connectorId: number,
299 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
5edd8ba0 300 phase?: MeterValuePhase,
884a6fdf
JB
301 ): SampledValueTemplate | undefined {
302 const onPhaseStr = phase ? `on phase ${phase} ` : '';
c3da35d4 303 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
884a6fdf 304 logger.warn(
5edd8ba0 305 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
306 );
307 return;
308 }
309 if (
310 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
23290150 311 ChargingStationConfigurationUtils.getConfigurationKey(
884a6fdf 312 chargingStation,
5edd8ba0 313 StandardParametersKey.MeterValuesSampledData,
72092cfc 314 )?.value?.includes(measurand) === false
884a6fdf
JB
315 ) {
316 logger.debug(
54ebb82c 317 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf 318 StandardParametersKey.MeterValuesSampledData
5edd8ba0 319 }' OCPP parameter`,
884a6fdf
JB
320 );
321 return;
322 }
323 const sampledValueTemplates: SampledValueTemplate[] =
e1d9a0f4 324 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
884a6fdf
JB
325 for (
326 let index = 0;
9bf0ef23 327 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
328 index++
329 ) {
330 if (
c3da35d4 331 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
884a6fdf 332 sampledValueTemplates[index]?.measurand ??
5edd8ba0 333 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
884a6fdf
JB
334 ) === false
335 ) {
336 logger.warn(
5edd8ba0 337 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
338 );
339 } else if (
340 phase &&
341 sampledValueTemplates[index]?.phase === phase &&
342 sampledValueTemplates[index]?.measurand === measurand &&
343 ChargingStationConfigurationUtils.getConfigurationKey(
344 chargingStation,
5edd8ba0 345 StandardParametersKey.MeterValuesSampledData,
72092cfc 346 )?.value?.includes(measurand) === true
884a6fdf
JB
347 ) {
348 return sampledValueTemplates[index];
349 } else if (
350 !phase &&
351 !sampledValueTemplates[index].phase &&
352 sampledValueTemplates[index]?.measurand === measurand &&
353 ChargingStationConfigurationUtils.getConfigurationKey(
354 chargingStation,
5edd8ba0 355 StandardParametersKey.MeterValuesSampledData,
72092cfc 356 )?.value?.includes(measurand) === true
884a6fdf
JB
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) {
54ebb82c 368 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
369 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
370 throw new BaseError(errorMsg);
371 }
372 logger.debug(
5edd8ba0 373 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
374 );
375 }
376
90befdb8
JB
377 protected static getLimitFromSampledValueTemplateCustomValue(
378 value: string,
379 limit: number,
380 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
381 limitationEnabled: true,
382 unitMultiplier: 1,
5edd8ba0 383 },
90befdb8 384 ): number {
20f0b76c
JB
385 options = {
386 ...{
387 limitationEnabled: true,
388 unitMultiplier: 1,
389 },
390 ...options,
391 };
231d1ecd
JB
392 const parsedInt = parseInt(value);
393 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
90befdb8 394 return options?.limitationEnabled
e1d9a0f4
JB
395 ? Math.min(numberValue * options.unitMultiplier!, limit)
396 : numberValue * options.unitMultiplier!;
90befdb8 397 }
7164966d 398
1b271a54
JB
399 private static logPrefix = (
400 ocppVersion: OCPPVersion,
401 moduleName?: string,
5edd8ba0 402 methodName?: string,
1b271a54
JB
403 ): string => {
404 const logMsg =
9bf0ef23 405 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54
JB
406 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
407 : ` OCPP ${ocppVersion} |`;
9bf0ef23 408 return logPrefix(logMsg);
8b7072dc 409 };
90befdb8 410}