refactor(simulator): switch utils to internal module export/import
[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 { type ChargingStation, ChargingStationConfigurationUtils } from '../../charging-station';
6 import { BaseError } from '../../exception';
7 import {
8 ChargePointErrorCode,
9 type ConnectorStatusEnum,
10 ErrorType,
11 FileType,
12 IncomingRequestCommand,
13 type JsonObject,
14 type JsonType,
15 MessageTrigger,
16 MessageType,
17 MeterValueMeasurand,
18 type MeterValuePhase,
19 type OCPP16StatusNotificationRequest,
20 type OCPP20StatusNotificationRequest,
21 OCPPVersion,
22 RequestCommand,
23 type SampledValueTemplate,
24 StandardParametersKey,
25 type StatusNotificationRequest,
26 } from '../../types';
27 import { Constants, FileUtils, Utils, logger } from '../../utils';
28
29 export class OCPPServiceUtils {
30 protected constructor() {
31 // This is intentional
32 }
33
34 public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType {
35 for (const error of errors as DefinedError[]) {
36 switch (error.keyword) {
37 case 'type':
38 return ErrorType.TYPE_CONSTRAINT_VIOLATION;
39 case 'dependencies':
40 case 'required':
41 return ErrorType.OCCURRENCE_CONSTRAINT_VIOLATION;
42 case 'pattern':
43 case 'format':
44 return ErrorType.PROPERTY_CONSTRAINT_VIOLATION;
45 }
46 }
47 return ErrorType.FORMAT_VIOLATION;
48 }
49
50 public static getMessageTypeString(messageType: MessageType): string {
51 switch (messageType) {
52 case MessageType.CALL_MESSAGE:
53 return 'request';
54 case MessageType.CALL_RESULT_MESSAGE:
55 return 'response';
56 case MessageType.CALL_ERROR_MESSAGE:
57 return 'error';
58 default:
59 return 'unknown';
60 }
61 }
62
63 public static isRequestCommandSupported(
64 chargingStation: ChargingStation,
65 command: RequestCommand
66 ): boolean {
67 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
68 if (
69 isRequestCommand === true &&
70 !chargingStation.stationInfo?.commandsSupport?.outgoingCommands
71 ) {
72 return true;
73 } else if (
74 isRequestCommand === true &&
75 chargingStation.stationInfo?.commandsSupport?.outgoingCommands
76 ) {
77 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command] ?? false;
78 }
79 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
80 return false;
81 }
82
83 public static isIncomingRequestCommandSupported(
84 chargingStation: ChargingStation,
85 command: IncomingRequestCommand
86 ): boolean {
87 const isIncomingRequestCommand =
88 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
89 if (
90 isIncomingRequestCommand === true &&
91 !chargingStation.stationInfo?.commandsSupport?.incomingCommands
92 ) {
93 return true;
94 } else if (
95 isIncomingRequestCommand === true &&
96 chargingStation.stationInfo?.commandsSupport?.incomingCommands
97 ) {
98 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] ?? false;
99 }
100 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
101 return false;
102 }
103
104 public static isMessageTriggerSupported(
105 chargingStation: ChargingStation,
106 messageTrigger: MessageTrigger
107 ): boolean {
108 const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger);
109 if (isMessageTrigger === true && !chargingStation.stationInfo?.messageTriggerSupport) {
110 return true;
111 } else if (isMessageTrigger === true && chargingStation.stationInfo?.messageTriggerSupport) {
112 return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger] ?? false;
113 }
114 logger.error(
115 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`
116 );
117 return false;
118 }
119
120 public static isConnectorIdValid(
121 chargingStation: ChargingStation,
122 ocppCommand: IncomingRequestCommand,
123 connectorId: number
124 ): boolean {
125 if (connectorId < 0) {
126 logger.error(
127 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector Id ${connectorId}`
128 );
129 return false;
130 }
131 return true;
132 }
133
134 public static convertDateToISOString<T extends JsonType>(obj: T): void {
135 for (const key in obj) {
136 if (obj[key] instanceof Date) {
137 (obj as JsonObject)[key] = (obj[key] as Date).toISOString();
138 } else if (obj[key] !== null && typeof obj[key] === 'object') {
139 this.convertDateToISOString<T>(obj[key] as T);
140 }
141 }
142 }
143
144 public static buildStatusNotificationRequest(
145 chargingStation: ChargingStation,
146 connectorId: number,
147 status: ConnectorStatusEnum
148 ): StatusNotificationRequest {
149 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
150 case OCPPVersion.VERSION_16:
151 return {
152 connectorId,
153 status,
154 errorCode: ChargePointErrorCode.NO_ERROR,
155 } as OCPP16StatusNotificationRequest;
156 case OCPPVersion.VERSION_20:
157 case OCPPVersion.VERSION_201:
158 return {
159 timestamp: new Date(),
160 connectorStatus: status,
161 connectorId,
162 evseId: connectorId,
163 } as OCPP20StatusNotificationRequest;
164 default:
165 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
166 }
167 }
168
169 protected static parseJsonSchemaFile<T extends JsonType>(
170 filePath: string,
171 ocppVersion: OCPPVersion,
172 moduleName?: string,
173 methodName?: string
174 ): JSONSchemaType<T> {
175 try {
176 return JSON.parse(fs.readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
177 } catch (error) {
178 FileUtils.handleFileException(
179 filePath,
180 FileType.JsonSchema,
181 error as NodeJS.ErrnoException,
182 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
183 { throwError: false }
184 );
185 }
186 }
187
188 protected static getSampledValueTemplate(
189 chargingStation: ChargingStation,
190 connectorId: number,
191 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
192 phase?: MeterValuePhase
193 ): SampledValueTemplate | undefined {
194 const onPhaseStr = phase ? `on phase ${phase} ` : '';
195 if (Constants.SUPPORTED_MEASURANDS.includes(measurand) === false) {
196 logger.warn(
197 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
198 );
199 return;
200 }
201 if (
202 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
203 ChargingStationConfigurationUtils.getConfigurationKey(
204 chargingStation,
205 StandardParametersKey.MeterValuesSampledData
206 )?.value?.includes(measurand) === false
207 ) {
208 logger.debug(
209 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
210 StandardParametersKey.MeterValuesSampledData
211 }' OCPP parameter`
212 );
213 return;
214 }
215 const sampledValueTemplates: SampledValueTemplate[] =
216 chargingStation.getConnectorStatus(connectorId)?.MeterValues;
217 for (
218 let index = 0;
219 Utils.isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
220 index++
221 ) {
222 if (
223 Constants.SUPPORTED_MEASURANDS.includes(
224 sampledValueTemplates[index]?.measurand ??
225 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
226 ) === false
227 ) {
228 logger.warn(
229 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
230 );
231 } else if (
232 phase &&
233 sampledValueTemplates[index]?.phase === phase &&
234 sampledValueTemplates[index]?.measurand === measurand &&
235 ChargingStationConfigurationUtils.getConfigurationKey(
236 chargingStation,
237 StandardParametersKey.MeterValuesSampledData
238 )?.value?.includes(measurand) === true
239 ) {
240 return sampledValueTemplates[index];
241 } else if (
242 !phase &&
243 !sampledValueTemplates[index].phase &&
244 sampledValueTemplates[index]?.measurand === measurand &&
245 ChargingStationConfigurationUtils.getConfigurationKey(
246 chargingStation,
247 StandardParametersKey.MeterValuesSampledData
248 )?.value?.includes(measurand) === true
249 ) {
250 return sampledValueTemplates[index];
251 } else if (
252 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
253 (!sampledValueTemplates[index].measurand ||
254 sampledValueTemplates[index].measurand === measurand)
255 ) {
256 return sampledValueTemplates[index];
257 }
258 }
259 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
260 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
261 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
262 throw new BaseError(errorMsg);
263 }
264 logger.debug(
265 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
266 );
267 }
268
269 protected static getLimitFromSampledValueTemplateCustomValue(
270 value: string,
271 limit: number,
272 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
273 limitationEnabled: true,
274 unitMultiplier: 1,
275 }
276 ): number {
277 options.limitationEnabled = options?.limitationEnabled ?? true;
278 options.unitMultiplier = options?.unitMultiplier ?? 1;
279 const parsedInt = parseInt(value);
280 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
281 return options?.limitationEnabled
282 ? Math.min(numberValue * options.unitMultiplier, limit)
283 : numberValue * options.unitMultiplier;
284 }
285
286 private static logPrefix = (
287 ocppVersion: OCPPVersion,
288 moduleName?: string,
289 methodName?: string
290 ): string => {
291 const logMsg =
292 Utils.isNotEmptyString(moduleName) && Utils.isNotEmptyString(methodName)
293 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
294 : ` OCPP ${ocppVersion} |`;
295 return Utils.logPrefix(logMsg);
296 };
297 }