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
,
18 ChargingStationEvents
,
20 type ConnectorStatusEnum
,
23 IncomingRequestCommand
,
29 type OCPP16StatusNotificationRequest
,
30 type OCPP20StatusNotificationRequest
,
33 type SampledValueTemplate
,
34 StandardParametersKey
,
35 type StatusNotificationRequest
,
36 type StatusNotificationResponse
,
47 export class OCPPServiceUtils
{
48 protected constructor() {
49 // This is intentional
52 public static ajvErrorsToErrorType(errors
: ErrorObject
[] | null | undefined): ErrorType
{
53 if (isNotEmptyArray(errors
) === true) {
54 for (const error
of errors
as DefinedError
[]) {
55 switch (error
.keyword
) {
57 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
60 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
63 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
67 return ErrorType
.FORMAT_VIOLATION
;
70 public static getMessageTypeString(messageType
: MessageType
): string {
71 switch (messageType
) {
72 case MessageType
.CALL_MESSAGE
:
74 case MessageType
.CALL_RESULT_MESSAGE
:
76 case MessageType
.CALL_ERROR_MESSAGE
:
83 public static isRequestCommandSupported(
84 chargingStation
: ChargingStation
,
85 command
: RequestCommand
,
87 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
89 isRequestCommand
=== true &&
90 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
94 isRequestCommand
=== true &&
95 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
?.[command
]
97 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
];
99 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
103 public static isIncomingRequestCommandSupported(
104 chargingStation
: ChargingStation
,
105 command
: IncomingRequestCommand
,
107 const isIncomingRequestCommand
=
108 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
110 isIncomingRequestCommand
=== true &&
111 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
115 isIncomingRequestCommand
=== true &&
116 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
?.[command
]
118 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
];
120 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
124 public static isMessageTriggerSupported(
125 chargingStation
: ChargingStation
,
126 messageTrigger
: MessageTrigger
,
128 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
129 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
132 isMessageTrigger
=== true &&
133 chargingStation
.stationInfo
?.messageTriggerSupport
?.[messageTrigger
]
135 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
];
138 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
143 public static isConnectorIdValid(
144 chargingStation
: ChargingStation
,
145 ocppCommand
: IncomingRequestCommand
,
148 if (connectorId
< 0) {
150 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
157 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
158 for (const key
in obj
) {
159 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
160 if (isDate(obj
![key
])) {
161 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
162 (obj
![key
] as string) = (obj
![key
] as Date).toISOString();
163 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
164 } else if (obj
![key
] !== null && typeof obj
![key
] === 'object') {
165 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
166 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
![key
] as T
);
171 public static buildStatusNotificationRequest(
172 chargingStation
: ChargingStation
,
174 status: ConnectorStatusEnum
,
176 ): StatusNotificationRequest
{
177 switch (chargingStation
.stationInfo
?.ocppVersion
) {
178 case OCPPVersion
.VERSION_16
:
182 errorCode
: ChargePointErrorCode
.NO_ERROR
,
183 } as OCPP16StatusNotificationRequest
;
184 case OCPPVersion
.VERSION_20
:
185 case OCPPVersion
.VERSION_201
:
187 timestamp
: new Date(),
188 connectorStatus
: status,
191 } as OCPP20StatusNotificationRequest
;
193 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
197 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
198 if (!chargingStation
.heartbeatSetInterval
) {
199 chargingStation
.startHeartbeat();
200 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
201 chargingStation
.restartHeartbeat();
205 public static async sendAndSetConnectorStatus(
206 chargingStation
: ChargingStation
,
208 status: ConnectorStatusEnum
,
210 options
?: { send
: boolean },
212 options
= { send
: true, ...options
};
214 OCPPServiceUtils
.checkConnectorStatusTransition(chargingStation
, connectorId
, status);
215 await chargingStation
.ocppRequestService
.requestHandler
<
216 StatusNotificationRequest
,
217 StatusNotificationResponse
220 RequestCommand
.STATUS_NOTIFICATION
,
221 OCPPServiceUtils
.buildStatusNotificationRequest(
229 chargingStation
.getConnectorStatus(connectorId
)!.status = status;
230 chargingStation
.emit(ChargingStationEvents
.connectorStatusChanged
, {
232 ...chargingStation
.getConnectorStatus(connectorId
),
236 public static async isIdTagAuthorized(
237 chargingStation
: ChargingStation
,
240 ): Promise
<boolean> {
242 !chargingStation
.getLocalAuthListEnabled() &&
243 !chargingStation
.stationInfo
?.remoteAuthorization
246 `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`,
250 chargingStation
.getLocalAuthListEnabled() === true &&
251 OCPPServiceUtils
.isIdTagLocalAuthorized(chargingStation
, idTag
)
253 const connectorStatus
: ConnectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
254 connectorStatus
.localAuthorizeIdTag
= idTag
;
255 connectorStatus
.idTagLocalAuthorized
= true;
257 } else if (chargingStation
.stationInfo
?.remoteAuthorization
) {
258 return await OCPPServiceUtils
.isIdTagRemoteAuthorized(chargingStation
, connectorId
, idTag
);
263 protected static checkConnectorStatusTransition(
264 chargingStation
: ChargingStation
,
266 status: ConnectorStatusEnum
,
268 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
)!.status;
269 let transitionAllowed
= false;
270 switch (chargingStation
.stationInfo
?.ocppVersion
) {
271 case OCPPVersion
.VERSION_16
:
273 (connectorId
=== 0 &&
274 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
275 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
278 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
279 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
282 transitionAllowed
= true;
285 case OCPPVersion
.VERSION_20
:
286 case OCPPVersion
.VERSION_201
:
288 (connectorId
=== 0 &&
289 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
290 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
293 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
294 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
297 transitionAllowed
= true;
302 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
303 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
306 if (transitionAllowed
=== false) {
308 `${chargingStation.logPrefix()} OCPP ${chargingStation.stationInfo
309 ?.ocppVersion} connector id ${connectorId} status transition from '${
310 chargingStation.getConnectorStatus(connectorId)!.status
311 }' to '${status}' is not allowed`,
314 return transitionAllowed
;
317 protected static parseJsonSchemaFile
<T
extends JsonType
>(
318 relativePath
: string,
319 ocppVersion
: OCPPVersion
,
322 ): JSONSchemaType
<T
> {
323 const filePath
= join(dirname(fileURLToPath(import.meta
.url
)), relativePath
);
325 return JSON
.parse(readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
330 error
as NodeJS
.ErrnoException
,
331 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
332 { throwError
: false },
334 return {} as JSONSchemaType
<T
>;
338 protected static getSampledValueTemplate(
339 chargingStation
: ChargingStation
,
341 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
342 phase
?: MeterValuePhase
,
343 ): SampledValueTemplate
| undefined {
344 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
345 if (OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(measurand
) === false) {
347 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
352 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
355 StandardParametersKey
.MeterValuesSampledData
,
356 )?.value
?.includes(measurand
) === false
359 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
360 StandardParametersKey.MeterValuesSampledData
365 const sampledValueTemplates
: SampledValueTemplate
[] =
366 chargingStation
.getConnectorStatus(connectorId
)!.MeterValues
;
369 isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
373 OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(
374 sampledValueTemplates
[index
]?.measurand
??
375 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
379 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
383 sampledValueTemplates
[index
]?.phase
=== phase
&&
384 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
387 StandardParametersKey
.MeterValuesSampledData
,
388 )?.value
?.includes(measurand
) === true
390 return sampledValueTemplates
[index
];
393 !sampledValueTemplates
[index
]?.phase
&&
394 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
397 StandardParametersKey
.MeterValuesSampledData
,
398 )?.value
?.includes(measurand
) === true
400 return sampledValueTemplates
[index
];
402 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
403 (!sampledValueTemplates
[index
]?.measurand
||
404 sampledValueTemplates
[index
]?.measurand
=== measurand
)
406 return sampledValueTemplates
[index
];
409 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
410 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
411 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
412 throw new BaseError(errorMsg
);
415 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
419 protected static getLimitFromSampledValueTemplateCustomValue(
422 options
?: { limitationEnabled
?: boolean; unitMultiplier
?: number },
426 limitationEnabled
: true,
431 const parsedInt
= parseInt(value
);
432 const numberValue
= isNaN(parsedInt
) ? Infinity : parsedInt
;
433 return options
?.limitationEnabled
434 ? min(numberValue
* options
.unitMultiplier
!, limit
)
435 : numberValue
* options
.unitMultiplier
!;
438 private static isIdTagLocalAuthorized(chargingStation
: ChargingStation
, idTag
: string): boolean {
440 chargingStation
.hasIdTags() === true &&
442 chargingStation
.idTagsCache
443 .getIdTags(getIdTagsFile(chargingStation
.stationInfo
)!)
444 ?.find((tag
) => tag
=== idTag
),
449 private static async isIdTagRemoteAuthorized(
450 chargingStation
: ChargingStation
,
453 ): Promise
<boolean> {
454 chargingStation
.getConnectorStatus(connectorId
)!.authorizeIdTag
= idTag
;
457 await chargingStation
.ocppRequestService
.requestHandler
<
460 >(chargingStation
, RequestCommand
.AUTHORIZE
, {
463 )?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
467 private static logPrefix
= (
468 ocppVersion
: OCPPVersion
,
473 isNotEmptyString(moduleName
) && isNotEmptyString(methodName
)
474 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
475 : ` OCPP ${ocppVersion} |`;
476 return logPrefix(logMsg
);