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