fix: ensure connectors status are saved is configuration
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / OCPPServiceUtils.ts
CommitLineData
d972af76
JB
1import { readFileSync } from 'node:fs';
2import { dirname, join } from 'node:path';
51022aa0 3import { fileURLToPath } from 'node:url';
7164966d
JB
4
5import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv';
3dcf7b67 6import { isDate } from 'date-fns';
06ad945f 7
4c3c0d59
JB
8import { OCPP16Constants } from './1.6/OCPP16Constants';
9import { OCPP20Constants } from './2.0/OCPP20Constants';
c3da35d4 10import { OCPPConstants } from './OCPPConstants';
ae725be3 11import { type ChargingStation, getConfigurationKey, getIdTagsFile } from '../../charging-station';
268a74bb 12import { BaseError } from '../../exception';
6e939d9e 13import {
ae725be3
JB
14 AuthorizationStatus,
15 type AuthorizeRequest,
16 type AuthorizeResponse,
268a74bb 17 ChargePointErrorCode,
ae725be3 18 type ConnectorStatus,
268a74bb
JB
19 type ConnectorStatusEnum,
20 ErrorType,
21 FileType,
6e939d9e 22 IncomingRequestCommand,
268a74bb 23 type JsonType,
6e939d9e 24 MessageTrigger,
268a74bb
JB
25 MessageType,
26 MeterValueMeasurand,
27 type MeterValuePhase,
28 type OCPP16StatusNotificationRequest,
29 type OCPP20StatusNotificationRequest,
30 OCPPVersion,
6e939d9e 31 RequestCommand,
268a74bb
JB
32 type SampledValueTemplate,
33 StandardParametersKey,
6e939d9e 34 type StatusNotificationRequest,
48b75072 35 type StatusNotificationResponse,
268a74bb 36} from '../../types';
9bf0ef23
JB
37import {
38 handleFileException,
39 isNotEmptyArray,
40 isNotEmptyString,
41 logPrefix,
42 logger,
5adf6ca4 43 min,
9bf0ef23 44} from '../../utils';
06ad945f 45
90befdb8 46export class OCPPServiceUtils {
d5bd1c00
JB
47 protected constructor() {
48 // This is intentional
49 }
50
01a4dcbb 51 public static ajvErrorsToErrorType(errors: ErrorObject[]): ErrorType {
06ad945f
JB
52 for (const error of errors as DefinedError[]) {
53 switch (error.keyword) {
54 case 'type':
55 return ErrorType.TYPE_CONSTRAINT_VIOLATION;
56 case 'dependencies':
57 case 'required':
58 return ErrorType.OCCURRENCE_CONSTRAINT_VIOLATION;
59 case 'pattern':
60 case 'format':
61 return ErrorType.PROPERTY_CONSTRAINT_VIOLATION;
62 }
63 }
64 return ErrorType.FORMAT_VIOLATION;
65 }
66
2cc5d5ec
JB
67 public static getMessageTypeString(messageType: MessageType): string {
68 switch (messageType) {
69 case MessageType.CALL_MESSAGE:
70 return 'request';
71 case MessageType.CALL_RESULT_MESSAGE:
72 return 'response';
73 case MessageType.CALL_ERROR_MESSAGE:
74 return 'error';
336f2829
JB
75 default:
76 return 'unknown';
2cc5d5ec
JB
77 }
78 }
79
ed3d2808 80 public static isRequestCommandSupported(
fd3c56d1 81 chargingStation: ChargingStation,
5edd8ba0 82 command: RequestCommand,
ed3d2808 83 ): boolean {
edd13439 84 const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command);
ed3d2808
JB
85 if (
86 isRequestCommand === true &&
87 !chargingStation.stationInfo?.commandsSupport?.outgoingCommands
88 ) {
89 return true;
90 } else if (
91 isRequestCommand === true &&
1c9de2b9 92 chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command]
ed3d2808 93 ) {
1c9de2b9 94 return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command];
ed3d2808
JB
95 }
96 logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
97 return false;
98 }
99
100 public static isIncomingRequestCommandSupported(
fd3c56d1 101 chargingStation: ChargingStation,
5edd8ba0 102 command: IncomingRequestCommand,
ed3d2808 103 ): boolean {
edd13439
JB
104 const isIncomingRequestCommand =
105 Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command);
ed3d2808
JB
106 if (
107 isIncomingRequestCommand === true &&
108 !chargingStation.stationInfo?.commandsSupport?.incomingCommands
109 ) {
110 return true;
111 } else if (
112 isIncomingRequestCommand === true &&
1c9de2b9 113 chargingStation.stationInfo?.commandsSupport?.incomingCommands?.[command]
ed3d2808 114 ) {
1c9de2b9 115 return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command];
ed3d2808
JB
116 }
117 logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
118 return false;
119 }
120
c60ed4b8
JB
121 public static isMessageTriggerSupported(
122 chargingStation: ChargingStation,
5edd8ba0 123 messageTrigger: MessageTrigger,
c60ed4b8
JB
124 ): boolean {
125 const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger);
126 if (isMessageTrigger === true && !chargingStation.stationInfo?.messageTriggerSupport) {
127 return true;
1c9de2b9
JB
128 } else if (
129 isMessageTrigger === true &&
130 chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger]
131 ) {
132 return chargingStation.stationInfo?.messageTriggerSupport[messageTrigger];
c60ed4b8
JB
133 }
134 logger.error(
5edd8ba0 135 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
c60ed4b8
JB
136 );
137 return false;
138 }
139
140 public static isConnectorIdValid(
141 chargingStation: ChargingStation,
142 ocppCommand: IncomingRequestCommand,
5edd8ba0 143 connectorId: number,
c60ed4b8
JB
144 ): boolean {
145 if (connectorId < 0) {
146 logger.error(
5edd8ba0 147 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
c60ed4b8
JB
148 );
149 return false;
150 }
151 return true;
152 }
153
1799761a 154 public static convertDateToISOString<T extends JsonType>(obj: T): void {
a37fc6dc
JB
155 for (const key in obj) {
156 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
3dcf7b67 157 if (isDate(obj![key])) {
a37fc6dc
JB
158 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
159 (obj![key] as string) = (obj![key] as Date).toISOString();
160 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 161 } else if (obj![key] !== null && typeof obj![key] === 'object') {
a37fc6dc 162 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
e1d9a0f4 163 OCPPServiceUtils.convertDateToISOString<T>(obj![key] as T);
1799761a
JB
164 }
165 }
166 }
167
6e939d9e
JB
168 public static buildStatusNotificationRequest(
169 chargingStation: ChargingStation,
170 connectorId: number,
12f26d4a 171 status: ConnectorStatusEnum,
5edd8ba0 172 evseId?: number,
6e939d9e 173 ): StatusNotificationRequest {
cda96260 174 switch (chargingStation.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16) {
6e939d9e
JB
175 case OCPPVersion.VERSION_16:
176 return {
177 connectorId,
178 status,
179 errorCode: ChargePointErrorCode.NO_ERROR,
180 } as OCPP16StatusNotificationRequest;
181 case OCPPVersion.VERSION_20:
182 case OCPPVersion.VERSION_201:
183 return {
184 timestamp: new Date(),
185 connectorStatus: status,
186 connectorId,
12f26d4a 187 evseId,
6e939d9e 188 } as OCPP20StatusNotificationRequest;
cda96260
JB
189 default:
190 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
6e939d9e
JB
191 }
192 }
193
8f953431
JB
194 public static startHeartbeatInterval(chargingStation: ChargingStation, interval: number): void {
195 if (!chargingStation.heartbeatSetInterval) {
196 chargingStation.startHeartbeat();
197 } else if (chargingStation.getHeartbeatInterval() !== interval) {
198 chargingStation.restartHeartbeat();
199 }
200 }
201
48b75072
JB
202 public static async sendAndSetConnectorStatus(
203 chargingStation: ChargingStation,
204 connectorId: number,
12f26d4a 205 status: ConnectorStatusEnum,
ec94a3cf 206 evseId?: number,
e1d9a0f4 207 options?: { send: boolean },
48b75072 208 ) {
ec94a3cf
JB
209 options = { send: true, ...options };
210 if (options.send) {
211 OCPPServiceUtils.checkConnectorStatusTransition(chargingStation, connectorId, status);
212 await chargingStation.ocppRequestService.requestHandler<
213 StatusNotificationRequest,
214 StatusNotificationResponse
215 >(
216 chargingStation,
217 RequestCommand.STATUS_NOTIFICATION,
218 OCPPServiceUtils.buildStatusNotificationRequest(
219 chargingStation,
220 connectorId,
221 status,
5edd8ba0
JB
222 evseId,
223 ),
ec94a3cf
JB
224 );
225 }
e1d9a0f4 226 chargingStation.getConnectorStatus(connectorId)!.status = status;
48b75072
JB
227 }
228
ae725be3
JB
229 public static async isIdTagAuthorized(
230 chargingStation: ChargingStation,
231 connectorId: number,
232 idTag: string,
233 ): Promise<boolean> {
cfdf901d
JB
234 if (!chargingStation.getLocalAuthListEnabled() && !chargingStation.getRemoteAuthorization()) {
235 logger.warn(
236 `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`,
237 );
238 }
cfdf901d
JB
239 if (
240 chargingStation.getLocalAuthListEnabled() === true &&
241 OCPPServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)
242 ) {
ae725be3
JB
243 const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!;
244 connectorStatus.localAuthorizeIdTag = idTag;
245 connectorStatus.idTagLocalAuthorized = true;
ec54600d 246 return true;
cfdf901d 247 } else if (chargingStation.getRemoteAuthorization()) {
ec54600d 248 return await OCPPServiceUtils.isIdTagRemoteAuthorized(chargingStation, connectorId, idTag);
ae725be3 249 }
ec54600d 250 return false;
ae725be3
JB
251 }
252
48b75072
JB
253 protected static checkConnectorStatusTransition(
254 chargingStation: ChargingStation,
255 connectorId: number,
5edd8ba0 256 status: ConnectorStatusEnum,
48b75072 257 ): boolean {
e1d9a0f4 258 const fromStatus = chargingStation.getConnectorStatus(connectorId)!.status;
48b75072
JB
259 let transitionAllowed = false;
260 switch (chargingStation.stationInfo.ocppVersion) {
261 case OCPPVersion.VERSION_16:
262 if (
ff9d1031
JB
263 (connectorId === 0 &&
264 OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
5edd8ba0 265 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
266 ) !== -1) ||
267 (connectorId > 0 &&
268 OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
5edd8ba0 269 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 270 ) !== -1)
48b75072
JB
271 ) {
272 transitionAllowed = true;
273 }
274 break;
275 case OCPPVersion.VERSION_20:
276 case OCPPVersion.VERSION_201:
277 if (
ff9d1031
JB
278 (connectorId === 0 &&
279 OCPP20Constants.ChargingStationStatusTransitions.findIndex(
5edd8ba0 280 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031
JB
281 ) !== -1) ||
282 (connectorId > 0 &&
283 OCPP20Constants.ConnectorStatusTransitions.findIndex(
5edd8ba0 284 (transition) => transition.from === fromStatus && transition.to === status,
ff9d1031 285 ) !== -1)
48b75072
JB
286 ) {
287 transitionAllowed = true;
288 }
289 break;
290 default:
291 throw new BaseError(
292 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
5edd8ba0 293 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
48b75072
JB
294 );
295 }
296 if (transitionAllowed === false) {
297 logger.warn(
298 `${chargingStation.logPrefix()} OCPP ${
299 chargingStation.stationInfo.ocppVersion
54ebb82c 300 } connector id ${connectorId} status transition from '${
e1d9a0f4 301 chargingStation.getConnectorStatus(connectorId)!.status
5edd8ba0 302 }' to '${status}' is not allowed`,
48b75072
JB
303 );
304 }
305 return transitionAllowed;
306 }
307
7164966d 308 protected static parseJsonSchemaFile<T extends JsonType>(
51022aa0 309 relativePath: string,
1b271a54
JB
310 ocppVersion: OCPPVersion,
311 moduleName?: string,
5edd8ba0 312 methodName?: string,
7164966d 313 ): JSONSchemaType<T> {
d972af76 314 const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath);
7164966d 315 try {
d972af76 316 return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T>;
7164966d 317 } catch (error) {
fa5995d6 318 handleFileException(
7164966d
JB
319 filePath,
320 FileType.JsonSchema,
321 error as NodeJS.ErrnoException,
1b271a54 322 OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName),
5edd8ba0 323 { throwError: false },
7164966d 324 );
e1d9a0f4 325 return {} as JSONSchemaType<T>;
7164966d 326 }
130783a7
JB
327 }
328
884a6fdf
JB
329 protected static getSampledValueTemplate(
330 chargingStation: ChargingStation,
331 connectorId: number,
332 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
5edd8ba0 333 phase?: MeterValuePhase,
884a6fdf
JB
334 ): SampledValueTemplate | undefined {
335 const onPhaseStr = phase ? `on phase ${phase} ` : '';
c3da35d4 336 if (OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand) === false) {
884a6fdf 337 logger.warn(
5edd8ba0 338 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
339 );
340 return;
341 }
342 if (
343 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
f2d5e3d9 344 getConfigurationKey(
884a6fdf 345 chargingStation,
5edd8ba0 346 StandardParametersKey.MeterValuesSampledData,
72092cfc 347 )?.value?.includes(measurand) === false
884a6fdf
JB
348 ) {
349 logger.debug(
54ebb82c 350 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
884a6fdf 351 StandardParametersKey.MeterValuesSampledData
5edd8ba0 352 }' OCPP parameter`,
884a6fdf
JB
353 );
354 return;
355 }
356 const sampledValueTemplates: SampledValueTemplate[] =
e1d9a0f4 357 chargingStation.getConnectorStatus(connectorId)!.MeterValues;
884a6fdf
JB
358 for (
359 let index = 0;
9bf0ef23 360 isNotEmptyArray(sampledValueTemplates) === true && index < sampledValueTemplates.length;
884a6fdf
JB
361 index++
362 ) {
363 if (
c3da35d4 364 OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
884a6fdf 365 sampledValueTemplates[index]?.measurand ??
5edd8ba0 366 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
884a6fdf
JB
367 ) === false
368 ) {
369 logger.warn(
5edd8ba0 370 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
371 );
372 } else if (
373 phase &&
374 sampledValueTemplates[index]?.phase === phase &&
375 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 376 getConfigurationKey(
884a6fdf 377 chargingStation,
5edd8ba0 378 StandardParametersKey.MeterValuesSampledData,
72092cfc 379 )?.value?.includes(measurand) === true
884a6fdf
JB
380 ) {
381 return sampledValueTemplates[index];
382 } else if (
383 !phase &&
1c9de2b9 384 !sampledValueTemplates[index]?.phase &&
884a6fdf 385 sampledValueTemplates[index]?.measurand === measurand &&
f2d5e3d9 386 getConfigurationKey(
884a6fdf 387 chargingStation,
5edd8ba0 388 StandardParametersKey.MeterValuesSampledData,
72092cfc 389 )?.value?.includes(measurand) === true
884a6fdf
JB
390 ) {
391 return sampledValueTemplates[index];
392 } else if (
393 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
1c9de2b9
JB
394 (!sampledValueTemplates[index]?.measurand ||
395 sampledValueTemplates[index]?.measurand === measurand)
884a6fdf
JB
396 ) {
397 return sampledValueTemplates[index];
398 }
399 }
400 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
54ebb82c 401 const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
884a6fdf
JB
402 logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
403 throw new BaseError(errorMsg);
404 }
405 logger.debug(
5edd8ba0 406 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
884a6fdf
JB
407 );
408 }
409
90befdb8
JB
410 protected static getLimitFromSampledValueTemplateCustomValue(
411 value: string,
412 limit: number,
7f3decca 413 options?: { limitationEnabled?: boolean; unitMultiplier?: number },
90befdb8 414 ): number {
20f0b76c
JB
415 options = {
416 ...{
417 limitationEnabled: true,
418 unitMultiplier: 1,
419 },
420 ...options,
421 };
231d1ecd
JB
422 const parsedInt = parseInt(value);
423 const numberValue = isNaN(parsedInt) ? Infinity : parsedInt;
90befdb8 424 return options?.limitationEnabled
5adf6ca4 425 ? min(numberValue * options.unitMultiplier!, limit)
e1d9a0f4 426 : numberValue * options.unitMultiplier!;
90befdb8 427 }
7164966d 428
ae725be3
JB
429 private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean {
430 return (
ae725be3
JB
431 chargingStation.hasIdTags() === true &&
432 isNotEmptyString(
433 chargingStation.idTagsCache
434 .getIdTags(getIdTagsFile(chargingStation.stationInfo)!)
435 ?.find((tag) => tag === idTag),
436 )
437 );
438 }
439
440 private static async isIdTagRemoteAuthorized(
441 chargingStation: ChargingStation,
442 connectorId: number,
443 idTag: string,
444 ): Promise<boolean> {
445 chargingStation.getConnectorStatus(connectorId)!.authorizeIdTag = idTag;
446 return (
447 (
448 await chargingStation.ocppRequestService.requestHandler<
449 AuthorizeRequest,
450 AuthorizeResponse
451 >(chargingStation, RequestCommand.AUTHORIZE, {
452 idTag,
453 })
454 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
455 );
456 }
457
1b271a54
JB
458 private static logPrefix = (
459 ocppVersion: OCPPVersion,
460 moduleName?: string,
5edd8ba0 461 methodName?: string,
1b271a54
JB
462 ): string => {
463 const logMsg =
9bf0ef23 464 isNotEmptyString(moduleName) && isNotEmptyString(methodName)
1b271a54
JB
465 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
466 : ` OCPP ${ocppVersion} |`;
9bf0ef23 467 return logPrefix(logMsg);
8b7072dc 468 };
90befdb8 469}