fix: avoid unscoped 'this' in static method
[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 OCPPServiceUtils.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 public static startHeartbeatInterval(chargingStation: ChargingStation, interval: number): void {
170 if (!chargingStation.heartbeatSetInterval) {
171 chargingStation.startHeartbeat();
172 } else if (chargingStation.getHeartbeatInterval() !== interval) {
173 chargingStation.restartHeartbeat();
174 }
175 }
176
177 protected static parseJsonSchemaFile<T extends JsonType>(
178 filePath: string,
179 ocppVersion: OCPPVersion,
180 moduleName?: string,
181 methodName?: string
182 ): JSONSchemaType<T> {
183 try {
184 return JSON.parse(fs.readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
185 } catch (error) {
186 FileUtils.handleFileException(
187 filePath,
188 FileType.JsonSchema,
189 error as NodeJS.ErrnoException,
190 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
191 { throwError: false }
192 );
193 }
194 }
195
196 protected static getSampledValueTemplate(
197 chargingStation: ChargingStation,
198 connectorId: number,
199 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
200 phase?: MeterValuePhase
201 ): SampledValueTemplate | undefined {
202 const onPhaseStr = phase ? `on phase ${phase} ` : '';
203 if (Constants.SUPPORTED_MEASURANDS.includes(measurand) === false) {
204 logger.warn(
205 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
206 );
207 return;
208 }
209 if (
210 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
211 ChargingStationConfigurationUtils.getConfigurationKey(
212 chargingStation,
213 StandardParametersKey.MeterValuesSampledData
214 )?.value?.includes(measurand) === false
215 ) {
216 logger.debug(
217 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
218 StandardParametersKey.MeterValuesSampledData
219 }' OCPP parameter`
220 );
221 return;
222 }
223 const sampledValueTemplates: SampledValueTemplate[] =
224 chargingStation.getConnectorStatus(connectorId)?.MeterValues;
225 for (
226 let index = 0;
227 Utils.isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
228 index++
229 ) {
230 if (
231 Constants.SUPPORTED_MEASURANDS.includes(
232 sampledValueTemplates[index]?.measurand ??
233 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
234 ) === false
235 ) {
236 logger.warn(
237 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
238 );
239 } else if (
240 phase &&
241 sampledValueTemplates[index]?.phase === phase &&
242 sampledValueTemplates[index]?.measurand === measurand &&
243 ChargingStationConfigurationUtils.getConfigurationKey(
244 chargingStation,
245 StandardParametersKey.MeterValuesSampledData
246 )?.value?.includes(measurand) === true
247 ) {
248 return sampledValueTemplates[index];
249 } else if (
250 !phase &&
251 !sampledValueTemplates[index].phase &&
252 sampledValueTemplates[index]?.measurand === measurand &&
253 ChargingStationConfigurationUtils.getConfigurationKey(
254 chargingStation,
255 StandardParametersKey.MeterValuesSampledData
256 )?.value?.includes(measurand) === true
257 ) {
258 return sampledValueTemplates[index];
259 } else if (
260 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
261 (!sampledValueTemplates[index].measurand ||
262 sampledValueTemplates[index].measurand === measurand)
263 ) {
264 return sampledValueTemplates[index];
265 }
266 }
267 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
268 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
269 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
270 throw new BaseError(errorMsg);
271 }
272 logger.debug(
273 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
274 );
275 }
276
277 protected static getLimitFromSampledValueTemplateCustomValue(
278 value: string,
279 limit: number,
280 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
281 limitationEnabled: true,
282 unitMultiplier: 1,
283 }
284 ): number {
285 options.limitationEnabled = options?.limitationEnabled ?? true;
286 options.unitMultiplier = options?.unitMultiplier ?? 1;
287 const parsedInt = parseInt(value);
288 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
289 return options?.limitationEnabled
290 ? Math.min(numberValue * options.unitMultiplier, limit)
291 : numberValue * options.unitMultiplier;
292 }
293
294 private static logPrefix = (
295 ocppVersion: OCPPVersion,
296 moduleName?: string,
297 methodName?: string
298 ): string => {
299 const logMsg =
300 Utils.isNotEmptyString(moduleName) && Utils.isNotEmptyString(methodName)
301 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
302 : ` OCPP ${ocppVersion} |`;
303 return Utils.logPrefix(logMsg);
304 };
305 }