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