1 import { readFileSync
} from
'node:fs';
2 import { dirname
, join
} from
'node:path';
3 import { fileURLToPath
} from
'node:url';
5 import type { DefinedError
, ErrorObject
, JSONSchemaType
} from
'ajv';
6 import { isDate
} from
'date-fns';
8 import { OCPP16Constants
} from
'./1.6/OCPP16Constants';
9 import { OCPP20Constants
} from
'./2.0/OCPP20Constants';
10 import { OCPPConstants
} from
'./OCPPConstants';
11 import { type ChargingStation
, getConfigurationKey
, getIdTagsFile
} from
'../../charging-station';
12 import { BaseError
} from
'../../exception';
15 type AuthorizeRequest
,
16 type AuthorizeResponse
,
19 type ConnectorStatusEnum
,
22 IncomingRequestCommand
,
28 type OCPP16StatusNotificationRequest
,
29 type OCPP20StatusNotificationRequest
,
32 type SampledValueTemplate
,
33 StandardParametersKey
,
34 type StatusNotificationRequest
,
35 type StatusNotificationResponse
,
45 export class OCPPServiceUtils
{
46 protected constructor() {
47 // This is intentional
50 public static ajvErrorsToErrorType(errors
: ErrorObject
[]): ErrorType
{
51 for (const error
of errors
as DefinedError
[]) {
52 switch (error
.keyword
) {
54 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
57 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
60 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
63 return ErrorType
.FORMAT_VIOLATION
;
66 public static getMessageTypeString(messageType
: MessageType
): string {
67 switch (messageType
) {
68 case MessageType
.CALL_MESSAGE
:
70 case MessageType
.CALL_RESULT_MESSAGE
:
72 case MessageType
.CALL_ERROR_MESSAGE
:
79 public static isRequestCommandSupported(
80 chargingStation
: ChargingStation
,
81 command
: RequestCommand
,
83 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
85 isRequestCommand
=== true &&
86 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
90 isRequestCommand
=== true &&
91 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
93 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
95 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
99 public static isIncomingRequestCommandSupported(
100 chargingStation
: ChargingStation
,
101 command
: IncomingRequestCommand
,
103 const isIncomingRequestCommand
=
104 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
106 isIncomingRequestCommand
=== true &&
107 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
111 isIncomingRequestCommand
=== true &&
112 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
114 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
116 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
120 public static isMessageTriggerSupported(
121 chargingStation
: ChargingStation
,
122 messageTrigger
: MessageTrigger
,
124 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
125 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
127 } else if (isMessageTrigger
=== true && chargingStation
.stationInfo
?.messageTriggerSupport
) {
128 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
] ?? false;
131 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
136 public static isConnectorIdValid(
137 chargingStation
: ChargingStation
,
138 ocppCommand
: IncomingRequestCommand
,
141 if (connectorId
< 0) {
143 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
150 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
151 for (const key
in obj
) {
152 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
153 if (isDate(obj
![key
])) {
154 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
155 (obj
![key
] as string) = (obj
![key
] as Date).toISOString();
156 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
157 } else if (obj
![key
] !== null && typeof obj
![key
] === 'object') {
158 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
159 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
![key
] as T
);
164 public static buildStatusNotificationRequest(
165 chargingStation
: ChargingStation
,
167 status: ConnectorStatusEnum
,
169 ): StatusNotificationRequest
{
170 switch (chargingStation
.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
) {
171 case OCPPVersion
.VERSION_16
:
175 errorCode
: ChargePointErrorCode
.NO_ERROR
,
176 } as OCPP16StatusNotificationRequest
;
177 case OCPPVersion
.VERSION_20
:
178 case OCPPVersion
.VERSION_201
:
180 timestamp
: new Date(),
181 connectorStatus
: status,
184 } as OCPP20StatusNotificationRequest
;
186 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
190 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
191 if (!chargingStation
.heartbeatSetInterval
) {
192 chargingStation
.startHeartbeat();
193 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
194 chargingStation
.restartHeartbeat();
198 public static async sendAndSetConnectorStatus(
199 chargingStation
: ChargingStation
,
201 status: ConnectorStatusEnum
,
203 options
?: { send
: boolean },
205 options
= { send
: true, ...options
};
207 OCPPServiceUtils
.checkConnectorStatusTransition(chargingStation
, connectorId
, status);
208 await chargingStation
.ocppRequestService
.requestHandler
<
209 StatusNotificationRequest
,
210 StatusNotificationResponse
213 RequestCommand
.STATUS_NOTIFICATION
,
214 OCPPServiceUtils
.buildStatusNotificationRequest(
222 chargingStation
.getConnectorStatus(connectorId
)!.status = status;
225 public static async isIdTagAuthorized(
226 chargingStation
: ChargingStation
,
229 ): Promise
<boolean> {
230 let authorized
= false;
231 if (OCPPServiceUtils
.isIdTagLocalAuthorized(chargingStation
, idTag
)) {
232 const connectorStatus
: ConnectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
233 connectorStatus
.localAuthorizeIdTag
= idTag
;
234 connectorStatus
.idTagLocalAuthorized
= true;
237 authorized
= await OCPPServiceUtils
.isIdTagRemoteAuthorized(
246 protected static checkConnectorStatusTransition(
247 chargingStation
: ChargingStation
,
249 status: ConnectorStatusEnum
,
251 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
)!.status;
252 let transitionAllowed
= false;
253 switch (chargingStation
.stationInfo
.ocppVersion
) {
254 case OCPPVersion
.VERSION_16
:
256 (connectorId
=== 0 &&
257 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
258 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
261 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
262 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
265 transitionAllowed
= true;
268 case OCPPVersion
.VERSION_20
:
269 case OCPPVersion
.VERSION_201
:
271 (connectorId
=== 0 &&
272 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
273 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
276 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
277 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
280 transitionAllowed
= true;
285 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
286 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
289 if (transitionAllowed
=== false) {
291 `${chargingStation.logPrefix()} OCPP ${
292 chargingStation.stationInfo.ocppVersion
293 } connector id ${connectorId} status transition from '${
294 chargingStation.getConnectorStatus(connectorId)!.status
295 }' to '${status}' is not allowed`,
298 return transitionAllowed
;
301 protected static parseJsonSchemaFile
<T
extends JsonType
>(
302 relativePath
: string,
303 ocppVersion
: OCPPVersion
,
306 ): JSONSchemaType
<T
> {
307 const filePath
= join(dirname(fileURLToPath(import.meta
.url
)), relativePath
);
309 return JSON
.parse(readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
314 error
as NodeJS
.ErrnoException
,
315 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
316 { throwError
: false },
318 return {} as JSONSchemaType
<T
>;
322 protected static getSampledValueTemplate(
323 chargingStation
: ChargingStation
,
325 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
326 phase
?: MeterValuePhase
,
327 ): SampledValueTemplate
| undefined {
328 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
329 if (OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(measurand
) === false) {
331 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
336 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
339 StandardParametersKey
.MeterValuesSampledData
,
340 )?.value
?.includes(measurand
) === false
343 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
344 StandardParametersKey.MeterValuesSampledData
349 const sampledValueTemplates
: SampledValueTemplate
[] =
350 chargingStation
.getConnectorStatus(connectorId
)!.MeterValues
;
353 isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
357 OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(
358 sampledValueTemplates
[index
]?.measurand
??
359 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
363 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
367 sampledValueTemplates
[index
]?.phase
=== phase
&&
368 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
371 StandardParametersKey
.MeterValuesSampledData
,
372 )?.value
?.includes(measurand
) === true
374 return sampledValueTemplates
[index
];
377 !sampledValueTemplates
[index
].phase
&&
378 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
381 StandardParametersKey
.MeterValuesSampledData
,
382 )?.value
?.includes(measurand
) === true
384 return sampledValueTemplates
[index
];
386 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
387 (!sampledValueTemplates
[index
].measurand
||
388 sampledValueTemplates
[index
].measurand
=== measurand
)
390 return sampledValueTemplates
[index
];
393 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
394 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
395 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
396 throw new BaseError(errorMsg
);
399 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
403 protected static getLimitFromSampledValueTemplateCustomValue(
406 options
: { limitationEnabled
?: boolean; unitMultiplier
?: number } = {
407 limitationEnabled
: true,
413 limitationEnabled
: true,
418 const parsedInt
= parseInt(value
);
419 const numberValue
= isNaN(parsedInt
) ? Infinity : parsedInt
;
420 return options
?.limitationEnabled
421 ? Math.min(numberValue
* options
.unitMultiplier
!, limit
)
422 : numberValue
* options
.unitMultiplier
!;
425 private static isIdTagLocalAuthorized(chargingStation
: ChargingStation
, idTag
: string): boolean {
427 chargingStation
.getLocalAuthListEnabled() === true &&
428 chargingStation
.hasIdTags() === true &&
430 chargingStation
.idTagsCache
431 .getIdTags(getIdTagsFile(chargingStation
.stationInfo
)!)
432 ?.find((tag
) => tag
=== idTag
),
437 private static async isIdTagRemoteAuthorized(
438 chargingStation
: ChargingStation
,
441 ): Promise
<boolean> {
442 chargingStation
.getConnectorStatus(connectorId
)!.authorizeIdTag
= idTag
;
445 await chargingStation
.ocppRequestService
.requestHandler
<
448 >(chargingStation
, RequestCommand
.AUTHORIZE
, {
451 )?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
455 private static logPrefix
= (
456 ocppVersion
: OCPPVersion
,
461 isNotEmptyString(moduleName
) && isNotEmptyString(methodName
)
462 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
463 : ` OCPP ${ocppVersion} |`;
464 return logPrefix(logMsg
);