3fced606ba29e6b003aa1b16c0bafdcb4a3f533e
[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/BaseError';
6 import { FileType } from '../../types/FileType';
7 import type { JsonObject, JsonType } from '../../types/JsonType';
8 import type { SampledValueTemplate } from '../../types/MeasurandPerPhaseSampledValueTemplates';
9 import type { OCPP16StatusNotificationRequest } from '../../types/ocpp/1.6/Requests';
10 import type { OCPP20StatusNotificationRequest } from '../../types/ocpp/2.0/Requests';
11 import { ChargePointErrorCode } from '../../types/ocpp/ChargePointErrorCode';
12 import { StandardParametersKey } from '../../types/ocpp/Configuration';
13 import type { ConnectorStatusEnum } from '../../types/ocpp/ConnectorStatusEnum';
14 import { ErrorType } from '../../types/ocpp/ErrorType';
15 import { MessageType } from '../../types/ocpp/MessageType';
16 import { MeterValueMeasurand, type MeterValuePhase } from '../../types/ocpp/MeterValues';
17 import { OCPPVersion } from '../../types/ocpp/OCPPVersion';
18 import {
19 IncomingRequestCommand,
20 MessageTrigger,
21 RequestCommand,
22 type StatusNotificationRequest,
23 } from '../../types/ocpp/Requests';
24 import Constants from '../../utils/Constants';
25 import FileUtils from '../../utils/FileUtils';
26 import logger from '../../utils/Logger';
27 import Utils from '../../utils/Utils';
28 import type ChargingStation from '../ChargingStation';
29 import { ChargingStationConfigurationUtils } from '../ChargingStationConfigurationUtils';
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 this.convertDateToISOString<T>(obj[key] as T);
142 }
143 }
144 }
145
146 public static buildStatusNotificationRequest(
147 chargingStation: ChargingStation,
148 connectorId: number,
149 status: ConnectorStatusEnum
150 ): StatusNotificationRequest {
151 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
152 case OCPPVersion.VERSION_16:
153 return {
154 connectorId,
155 status,
156 errorCode: ChargePointErrorCode.NO_ERROR,
157 } as OCPP16StatusNotificationRequest;
158 case OCPPVersion.VERSION_20:
159 case OCPPVersion.VERSION_201:
160 return {
161 timestamp: new Date(),
162 connectorStatus: status,
163 connectorId,
164 evseId: connectorId,
165 } as OCPP20StatusNotificationRequest;
166 default:
167 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
168 }
169 }
170
171 protected static parseJsonSchemaFile<T extends JsonType>(
172 filePath: string,
173 ocppVersion: OCPPVersion,
174 moduleName?: string,
175 methodName?: string
176 ): JSONSchemaType<T> {
177 try {
178 return JSON.parse(fs.readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
179 } catch (error) {
180 FileUtils.handleFileException(
181 filePath,
182 FileType.JsonSchema,
183 error as NodeJS.ErrnoException,
184 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
185 { throwError: false }
186 );
187 }
188 }
189
190 protected static getSampledValueTemplate(
191 chargingStation: ChargingStation,
192 connectorId: number,
193 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
194 phase?: MeterValuePhase
195 ): SampledValueTemplate | undefined {
196 const onPhaseStr = phase ? `on phase ${phase} ` : '';
197 if (Constants.SUPPORTED_MEASURANDS.includes(measurand) === false) {
198 logger.warn(
199 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
200 );
201 return;
202 }
203 if (
204 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
205 ChargingStationConfigurationUtils.getConfigurationKey(
206 chargingStation,
207 StandardParametersKey.MeterValuesSampledData
208 )?.value?.includes(measurand) === false
209 ) {
210 logger.debug(
211 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
212 StandardParametersKey.MeterValuesSampledData
213 }' OCPP parameter`
214 );
215 return;
216 }
217 const sampledValueTemplates: SampledValueTemplate[] =
218 chargingStation.getConnectorStatus(connectorId)?.MeterValues;
219 for (
220 let index = 0;
221 Utils.isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
222 index++
223 ) {
224 if (
225 Constants.SUPPORTED_MEASURANDS.includes(
226 sampledValueTemplates[index]?.measurand ??
227 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
228 ) === false
229 ) {
230 logger.warn(
231 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
232 );
233 } else if (
234 phase &&
235 sampledValueTemplates[index]?.phase === phase &&
236 sampledValueTemplates[index]?.measurand === measurand &&
237 ChargingStationConfigurationUtils.getConfigurationKey(
238 chargingStation,
239 StandardParametersKey.MeterValuesSampledData
240 )?.value?.includes(measurand) === true
241 ) {
242 return sampledValueTemplates[index];
243 } else if (
244 !phase &&
245 !sampledValueTemplates[index].phase &&
246 sampledValueTemplates[index]?.measurand === measurand &&
247 ChargingStationConfigurationUtils.getConfigurationKey(
248 chargingStation,
249 StandardParametersKey.MeterValuesSampledData
250 )?.value?.includes(measurand) === true
251 ) {
252 return sampledValueTemplates[index];
253 } else if (
254 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
255 (!sampledValueTemplates[index].measurand ||
256 sampledValueTemplates[index].measurand === measurand)
257 ) {
258 return sampledValueTemplates[index];
259 }
260 }
261 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
262 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
263 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
264 throw new BaseError(errorMsg);
265 }
266 logger.debug(
267 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
268 );
269 }
270
271 protected static getLimitFromSampledValueTemplateCustomValue(
272 value: string,
273 limit: number,
274 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
275 limitationEnabled: true,
276 unitMultiplier: 1,
277 }
278 ): number {
279 options.limitationEnabled = options?.limitationEnabled ?? true;
280 options.unitMultiplier = options?.unitMultiplier ?? 1;
281 const parsedInt = parseInt(value);
282 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
283 return options?.limitationEnabled
284 ? Math.min(numberValue * options.unitMultiplier, limit)
285 : numberValue * options.unitMultiplier;
286 }
287
288 private static logPrefix = (
289 ocppVersion: OCPPVersion,
290 moduleName?: string,
291 methodName?: string
292 ): string => {
293 const logMsg =
294 Utils.isNotEmptyString(moduleName) && Utils.isNotEmptyString(methodName)
295 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
296 : ` OCPP ${ocppVersion} |`;
297 return Utils.logPrefix(logMsg);
298 };
299 }