build(deps-dev): apply updates
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
CommitLineData
7164966d
JB
1import fs from 'node:fs';
2
3import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv';
06ad945f 4
2896e06d 5import { type ChargingStation, ChargingStationConfigurationUtils } from '../../charging-station';
268a74bb 6import { BaseError } from '../../exception';
6e939d9e 7import {
268a74bb
JB
8 ChargePointErrorCode,
9 type ConnectorStatusEnum,
10 ErrorType,
11 FileType,
6e939d9e 12 IncomingRequestCommand,
268a74bb
JB
13 type JsonObject,
14 type JsonType,
6e939d9e 15 MessageTrigger,
268a74bb
JB
16 MessageType,
17 MeterValueMeasurand,
18 type MeterValuePhase,
19 type OCPP16StatusNotificationRequest,
20 type OCPP20StatusNotificationRequest,
21 OCPPVersion,
6e939d9e 22 RequestCommand,
268a74bb
JB
23 type SampledValueTemplate,
24 StandardParametersKey,
6e939d9e 25 type StatusNotificationRequest,
268a74bb 26} from '../../types';
60a74391 27import { Constants, FileUtils, Utils, logger } from '../../utils';
06ad945f 28
90befdb8 29export class OCPPServiceUtils {
d5bd1c00
JB
30 protected constructor() {
31 // This is intentional
32 }
33
01a4dcbb 34 public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType {
06ad945f
JB
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
2cc5d5ec
JB
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';
336f2829
JB
58 default:
59 return 'unknown';
2cc5d5ec
JB
60 }
61 }
62
ed3d2808 63 public static isRequestCommandSupported(
fd3c56d1
JB
64 chargingStation: ChargingStation,
65 command: RequestCommand
ed3d2808 66 ): boolean {
edd13439 67 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
ed3d2808
JB
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(
fd3c56d1
JB
84 chargingStation: ChargingStation,
85 command: IncomingRequestCommand
ed3d2808 86 ): boolean {
edd13439
JB
87 const isIncomingRequestCommand =
88 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
ed3d2808
JB
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
c60ed4b8
JB
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(
8eb3b688 127 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector Id ${connectorId}`
c60ed4b8
JB
128 );
129 return false;
130 }
131 return true;
132 }
133
1799761a 134 public static convertDateToISOString<T extends JsonType>(obj: T): void {
02887891
JB
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') {
b30ea3f0 139 OCPPServiceUtils.convertDateToISOString<T>(obj[key] as T);
1799761a
JB
140 }
141 }
142 }
143
6e939d9e
JB
144 public static buildStatusNotificationRequest(
145 chargingStation: ChargingStation,
146 connectorId: number,
147 status: ConnectorStatusEnum
148 ): StatusNotificationRequest {
cda96260 149 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
6e939d9e
JB
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;
cda96260
JB
164 default:
165 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
6e939d9e
JB
166 }
167 }
168
8f953431
JB
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
7164966d
JB
177 protected static parseJsonSchemaFile<T extends JsonType>(
178 filePath: string,
1b271a54
JB
179 ocppVersion: OCPPVersion,
180 moduleName?: string,
181 methodName?: string
7164966d
JB
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,
1b271a54 190 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
7164966d
JB
191 { throwError: false }
192 );
193 }
130783a7
JB
194 }
195
884a6fdf
JB
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 &&
23290150 211 ChargingStationConfigurationUtils.getConfigurationKey(
884a6fdf
JB
212 chargingStation,
213 StandardParametersKey.MeterValuesSampledData
72092cfc 214 )?.value?.includes(measurand) === false
884a6fdf
JB
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[] =
72092cfc 224 chargingStation.getConnectorStatus(connectorId)?.MeterValues;
884a6fdf
JB
225 for (
226 let index = 0;
53ac516c 227 Utils.isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
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
72092cfc 246 )?.value?.includes(measurand) === true
884a6fdf
JB
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
72092cfc 256 )?.value?.includes(measurand) === true
884a6fdf
JB
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
90befdb8
JB
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;
231d1ecd
JB
287 const parsedInt = parseInt(value);
288 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
90befdb8 289 return options?.limitationEnabled
f126aa15
JB
290 ? Math.min(numberValue * options.unitMultiplier, limit)
291 : numberValue * options.unitMultiplier;
90befdb8 292 }
7164966d 293
1b271a54
JB
294 private static logPrefix = (
295 ocppVersion: OCPPVersion,
296 moduleName?: string,
297 methodName?: string
298 ): string => {
299 const logMsg =
5a2a53cf 300 Utils.isNotEmptyString(moduleName) && Utils.isNotEmptyString(methodName)
1b271a54
JB
301 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
302 : ` OCPP ${ocppVersion} |`;
303 return Utils.logPrefix(logMsg);
8b7072dc 304 };
90befdb8 305}