Move OCPP log message formatting helper into static helpers class
[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 }
57 }
58
59 public static isRequestCommandSupported(
60 chargingStation: ChargingStation,
61 command: RequestCommand
62 ): boolean {
63 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
64 if (
65 isRequestCommand === true &&
66 !chargingStation.stationInfo?.commandsSupport?.outgoingCommands
67 ) {
68 return true;
69 } else if (
70 isRequestCommand === true &&
71 chargingStation.stationInfo?.commandsSupport?.outgoingCommands
72 ) {
73 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command] ?? false;
74 }
75 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
76 return false;
77 }
78
79 public static isIncomingRequestCommandSupported(
80 chargingStation: ChargingStation,
81 command: IncomingRequestCommand
82 ): boolean {
83 const isIncomingRequestCommand =
84 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
85 if (
86 isIncomingRequestCommand === true &&
87 !chargingStation.stationInfo?.commandsSupport?.incomingCommands
88 ) {
89 return true;
90 } else if (
91 isIncomingRequestCommand === true &&
92 chargingStation.stationInfo?.commandsSupport?.incomingCommands
93 ) {
94 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] ?? false;
95 }
96 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
97 return false;
98 }
99
100 public static isMessageTriggerSupported(
101 chargingStation: ChargingStation,
102 messageTrigger: MessageTrigger
103 ): boolean {
104 const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger);
105 if (isMessageTrigger === true && !chargingStation.stationInfo?.messageTriggerSupport) {
106 return true;
107 } else if (isMessageTrigger === true && chargingStation.stationInfo?.messageTriggerSupport) {
108 return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger] ?? false;
109 }
110 logger.error(
111 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`
112 );
113 return false;
114 }
115
116 public static isConnectorIdValid(
117 chargingStation: ChargingStation,
118 ocppCommand: IncomingRequestCommand,
119 connectorId: number
120 ): boolean {
121 if (connectorId < 0) {
122 logger.error(
123 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector Id ${connectorId}`
124 );
125 return false;
126 }
127 return true;
128 }
129
130 public static convertDateToISOString<T extends JsonType>(obj: T): void {
131 for (const key in obj) {
132 if (obj[key] instanceof Date) {
133 (obj as JsonObject)[key] = (obj[key] as Date).toISOString();
134 } else if (obj[key] !== null && typeof obj[key] === 'object') {
135 this.convertDateToISOString<T>(obj[key] as T);
136 }
137 }
138 }
139
140 public static buildStatusNotificationRequest(
141 chargingStation: ChargingStation,
142 connectorId: number,
143 status: ConnectorStatusEnum
144 ): StatusNotificationRequest {
145 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
146 case OCPPVersion.VERSION_16:
147 return {
148 connectorId,
149 status,
150 errorCode: ChargePointErrorCode.NO_ERROR,
151 } as OCPP16StatusNotificationRequest;
152 case OCPPVersion.VERSION_20:
153 case OCPPVersion.VERSION_201:
154 return {
155 timestamp: new Date(),
156 connectorStatus: status,
157 connectorId,
158 evseId: connectorId,
159 } as OCPP20StatusNotificationRequest;
160 default:
161 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
162 }
163 }
164
165 protected static getSampledValueTemplate(
166 chargingStation: ChargingStation,
167 connectorId: number,
168 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
169 phase?: MeterValuePhase
170 ): SampledValueTemplate | undefined {
171 const onPhaseStr = phase ? `on phase ${phase} ` : '';
172 if (Constants.SUPPORTED_MEASURANDS.includes(measurand) === false) {
173 logger.warn(
174 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
175 );
176 return;
177 }
178 if (
179 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
180 ChargingStationConfigurationUtils.getConfigurationKey(
181 chargingStation,
182 StandardParametersKey.MeterValuesSampledData
183 )?.value.includes(measurand) === false
184 ) {
185 logger.debug(
186 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
187 StandardParametersKey.MeterValuesSampledData
188 }' OCPP parameter`
189 );
190 return;
191 }
192 const sampledValueTemplates: SampledValueTemplate[] =
193 chargingStation.getConnectorStatus(connectorId).MeterValues;
194 for (
195 let index = 0;
196 Utils.isEmptyArray(sampledValueTemplates) === false && index < sampledValueTemplates.length;
197 index++
198 ) {
199 if (
200 Constants.SUPPORTED_MEASURANDS.includes(
201 sampledValueTemplates[index]?.measurand ??
202 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
203 ) === false
204 ) {
205 logger.warn(
206 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
207 );
208 } else if (
209 phase &&
210 sampledValueTemplates[index]?.phase === phase &&
211 sampledValueTemplates[index]?.measurand === measurand &&
212 ChargingStationConfigurationUtils.getConfigurationKey(
213 chargingStation,
214 StandardParametersKey.MeterValuesSampledData
215 )?.value.includes(measurand) === true
216 ) {
217 return sampledValueTemplates[index];
218 } else if (
219 !phase &&
220 !sampledValueTemplates[index].phase &&
221 sampledValueTemplates[index]?.measurand === measurand &&
222 ChargingStationConfigurationUtils.getConfigurationKey(
223 chargingStation,
224 StandardParametersKey.MeterValuesSampledData
225 )?.value.includes(measurand) === true
226 ) {
227 return sampledValueTemplates[index];
228 } else if (
229 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
230 (!sampledValueTemplates[index].measurand ||
231 sampledValueTemplates[index].measurand === measurand)
232 ) {
233 return sampledValueTemplates[index];
234 }
235 }
236 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
237 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
238 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
239 throw new BaseError(errorMsg);
240 }
241 logger.debug(
242 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
243 );
244 }
245
246 protected static getLimitFromSampledValueTemplateCustomValue(
247 value: string,
248 limit: number,
249 options: { limitationEnabled?: boolean; unitMultiplier?: number } = {
250 limitationEnabled: true,
251 unitMultiplier: 1,
252 }
253 ): number {
254 options.limitationEnabled = options?.limitationEnabled ?? true;
255 options.unitMultiplier = options?.unitMultiplier ?? 1;
256 const parsedInt = parseInt(value);
257 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
258 return options?.limitationEnabled
259 ? Math.min(numberValue * options.unitMultiplier, limit)
260 : numberValue * options.unitMultiplier;
261 }
262 }