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