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