refactor: group conditions in connector status change check
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
1 import fs from 'node:fs';
2
3 import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv';
4
5 import { OCPP16Constants, OCPP20Constants } from './internal';
6 import { type ChargingStation, ChargingStationConfigurationUtils } from '../../charging-station';
7 import { BaseError } from '../../exception';
8 import {
9 ChargePointErrorCode,
10 type ConnectorStatusEnum,
11 ErrorType,
12 FileType,
13 IncomingRequestCommand,
14 type JsonObject,
15 type JsonType,
16 MessageTrigger,
17 MessageType,
18 MeterValueMeasurand,
19 type MeterValuePhase,
20 type OCPP16StatusNotificationRequest,
21 type OCPP20StatusNotificationRequest,
22 OCPPVersion,
23 RequestCommand,
24 type SampledValueTemplate,
25 StandardParametersKey,
26 type StatusNotificationRequest,
27 type StatusNotificationResponse,
28 } from '../../types';
29 import { Constants, FileUtils, Utils, logger } from '../../utils';
30
31 export class OCPPServiceUtils {
32 protected constructor() {
33 // This is intentional
34 }
35
36 public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType {
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
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';
60 default:
61 return 'unknown';
62 }
63 }
64
65 public static isRequestCommandSupported(
66 chargingStation: ChargingStation,
67 command: RequestCommand
68 ): boolean {
69 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
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(
86 chargingStation: ChargingStation,
87 command: IncomingRequestCommand
88 ): boolean {
89 const isIncomingRequestCommand =
90 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
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
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(
129 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`
130 );
131 return false;
132 }
133 return true;
134 }
135
136 public static convertDateToISOString<T extends JsonType>(obj: T): void {
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') {
141 OCPPServiceUtils.convertDateToISOString<T>(obj[key] as T);
142 }
143 }
144 }
145
146 public static buildStatusNotificationRequest(
147 chargingStation: ChargingStation,
148 connectorId: number,
149 status: ConnectorStatusEnum,
150 evseId?: number
151 ): StatusNotificationRequest {
152 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
153 case OCPPVersion.VERSION_16:
154 return {
155 connectorId,
156 status,
157 errorCode: ChargePointErrorCode.NO_ERROR,
158 } as OCPP16StatusNotificationRequest;
159 case OCPPVersion.VERSION_20:
160 case OCPPVersion.VERSION_201:
161 return {
162 timestamp: new Date(),
163 connectorStatus: status,
164 connectorId,
165 evseId,
166 } as OCPP20StatusNotificationRequest;
167 default:
168 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
169 }
170 }
171
172 public static startHeartbeatInterval(chargingStation: ChargingStation, interval: number): void {
173 if (!chargingStation.heartbeatSetInterval) {
174 chargingStation.startHeartbeat();
175 } else if (chargingStation.getHeartbeatInterval() !== interval) {
176 chargingStation.restartHeartbeat();
177 }
178 }
179
180 public static async sendAndSetConnectorStatus(
181 chargingStation: ChargingStation,
182 connectorId: number,
183 status: ConnectorStatusEnum,
184 evseId?: number
185 ) {
186 OCPPServiceUtils.checkConnectorStatusTransition(chargingStation, connectorId, status);
187 await chargingStation.ocppRequestService.requestHandler<
188 StatusNotificationRequest,
189 StatusNotificationResponse
190 >(
191 chargingStation,
192 RequestCommand.STATUS_NOTIFICATION,
193 OCPPServiceUtils.buildStatusNotificationRequest(chargingStation, connectorId, status, evseId)
194 );
195 chargingStation.getConnectorStatus(connectorId).status = status;
196 }
197
198 protected static checkConnectorStatusTransition(
199 chargingStation: ChargingStation,
200 connectorId: number,
201 status: ConnectorStatusEnum
202 ): boolean {
203 const fromStatus = chargingStation.getConnectorStatus(connectorId).status;
204 let transitionAllowed = false;
205 switch (chargingStation.stationInfo.ocppVersion) {
206 case OCPPVersion.VERSION_16:
207 if (
208 (connectorId === 0 &&
209 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
210 (transition) => transition.from === fromStatus && transition.to === status
211 ) !== -1) ||
212 (connectorId > 0 &&
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 (connectorId > 0 &&
228 OCPP20Constants.ConnectorStatusTransitions.findIndex(
229 (transition) => transition.from === fromStatus && transition.to === status
230 ) !== -1)
231 ) {
232 transitionAllowed = true;
233 }
234 break;
235 default:
236 throw new BaseError(
237 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
238 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`
239 );
240 }
241 if (transitionAllowed === false) {
242 logger.warn(
243 `${chargingStation.logPrefix()} OCPP ${
244 chargingStation.stationInfo.ocppVersion
245 } connector id ${connectorId} status transition from '${
246 chargingStation.getConnectorStatus(connectorId).status
247 }' to '${status}' is not allowed`
248 );
249 }
250 return transitionAllowed;
251 }
252
253 protected static parseJsonSchemaFile<T extends JsonType>(
254 filePath: string,
255 ocppVersion: OCPPVersion,
256 moduleName?: string,
257 methodName?: string
258 ): JSONSchemaType<T> {
259 try {
260 return JSON.parse(fs.readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
261 } catch (error) {
262 FileUtils.handleFileException(
263 filePath,
264 FileType.JsonSchema,
265 error as NodeJS.ErrnoException,
266 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
267 { throwError: false }
268 );
269 }
270 }
271
272 protected static getSampledValueTemplate(
273 chargingStation: ChargingStation,
274 connectorId: number,
275 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
276 phase?: MeterValuePhase
277 ): SampledValueTemplate | undefined {
278 const onPhaseStr = phase ? `on phase ${phase} ` : '';
279 if (Constants.SUPPORTED_MEASURANDS.includes(measurand) === false) {
280 logger.warn(
281 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
282 );
283 return;
284 }
285 if (
286 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
287 ChargingStationConfigurationUtils.getConfigurationKey(
288 chargingStation,
289 StandardParametersKey.MeterValuesSampledData
290 )?.value?.includes(measurand) === false
291 ) {
292 logger.debug(
293 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
294 StandardParametersKey.MeterValuesSampledData
295 }' OCPP parameter`
296 );
297 return;
298 }
299 const sampledValueTemplates: SampledValueTemplate[] =
300 chargingStation.getConnectorStatus(connectorId)?.MeterValues;
301 for (
302 let index = 0;
303 Utils.isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
304 index++
305 ) {
306 if (
307 Constants.SUPPORTED_MEASURANDS.includes(
308 sampledValueTemplates[index]?.measurand ??
309 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
310 ) === false
311 ) {
312 logger.warn(
313 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
314 );
315 } else if (
316 phase &&
317 sampledValueTemplates[index]?.phase === phase &&
318 sampledValueTemplates[index]?.measurand === measurand &&
319 ChargingStationConfigurationUtils.getConfigurationKey(
320 chargingStation,
321 StandardParametersKey.MeterValuesSampledData
322 )?.value?.includes(measurand) === true
323 ) {
324 return sampledValueTemplates[index];
325 } else if (
326 !phase &&
327 !sampledValueTemplates[index].phase &&
328 sampledValueTemplates[index]?.measurand === measurand &&
329 ChargingStationConfigurationUtils.getConfigurationKey(
330 chargingStation,
331 StandardParametersKey.MeterValuesSampledData
332 )?.value?.includes(measurand) === true
333 ) {
334 return sampledValueTemplates[index];
335 } else if (
336 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
337 (!sampledValueTemplates[index].measurand ||
338 sampledValueTemplates[index].measurand === measurand)
339 ) {
340 return sampledValueTemplates[index];
341 }
342 }
343 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
344 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
345 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
346 throw new BaseError(errorMsg);
347 }
348 logger.debug(
349 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
350 );
351 }
352
353 protected static getLimitFromSampledValueTemplateCustomValue(
354 value: string,
355 limit: number,
356 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
357 limitationEnabled: true,
358 unitMultiplier: 1,
359 }
360 ): number {
361 options.limitationEnabled = options?.limitationEnabled ?? true;
362 options.unitMultiplier = options?.unitMultiplier ?? 1;
363 const parsedInt = parseInt(value);
364 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
365 return options?.limitationEnabled
366 ? Math.min(numberValue * options.unitMultiplier, limit)
367 : numberValue * options.unitMultiplier;
368 }
369
370 private static logPrefix = (
371 ocppVersion: OCPPVersion,
372 moduleName?: string,
373 methodName?: string
374 ): string => {
375 const logMsg =
376 Utils.isNotEmptyString(moduleName) && Utils.isNotEmptyString(methodName)
377 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
378 : ` OCPP ${ocppVersion} |`;
379 return Utils.logPrefix(logMsg);
380 };
381 }