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
,
48 export class OCPPServiceUtils
{
49 protected constructor() {
50 // This is intentional
53 public static ajvErrorsToErrorType(errors
: ErrorObject
[] | null | undefined): ErrorType
{
54 if (isNotEmptyArray(errors
) === true) {
55 for (const error
of errors
as DefinedError
[]) {
56 switch (error
.keyword
) {
58 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
61 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
64 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
68 return ErrorType
.FORMAT_VIOLATION
;
71 public static getMessageTypeString(messageType
: MessageType
): string {
72 switch (messageType
) {
73 case MessageType
.CALL_MESSAGE
:
75 case MessageType
.CALL_RESULT_MESSAGE
:
77 case MessageType
.CALL_ERROR_MESSAGE
:
84 public static isRequestCommandSupported(
85 chargingStation
: ChargingStation
,
86 command
: RequestCommand
,
88 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
90 isRequestCommand
=== true &&
91 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
95 isRequestCommand
=== true &&
96 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
?.[command
]
98 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
];
100 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
104 public static isIncomingRequestCommandSupported(
105 chargingStation
: ChargingStation
,
106 command
: IncomingRequestCommand
,
108 const isIncomingRequestCommand
=
109 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
111 isIncomingRequestCommand
=== true &&
112 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
116 isIncomingRequestCommand
=== true &&
117 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
?.[command
]
119 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
];
121 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
125 public static isMessageTriggerSupported(
126 chargingStation
: ChargingStation
,
127 messageTrigger
: MessageTrigger
,
129 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
130 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
133 isMessageTrigger
=== true &&
134 chargingStation
.stationInfo
?.messageTriggerSupport
?.[messageTrigger
]
136 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
];
139 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
144 public static isConnectorIdValid(
145 chargingStation
: ChargingStation
,
146 ocppCommand
: IncomingRequestCommand
,
149 if (connectorId
< 0) {
151 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
158 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
159 for (const key
in obj
) {
160 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
161 if (isDate(obj
![key
])) {
162 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
163 (obj
![key
] as string) = (obj
![key
] as Date).toISOString();
164 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
165 } else if (obj
![key
] !== null && typeof obj
![key
] === 'object') {
166 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
167 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
![key
] as T
);
172 public static buildStatusNotificationRequest(
173 chargingStation
: ChargingStation
,
175 status: ConnectorStatusEnum
,
177 ): StatusNotificationRequest
{
178 switch (chargingStation
.stationInfo
?.ocppVersion
) {
179 case OCPPVersion
.VERSION_16
:
183 errorCode
: ChargePointErrorCode
.NO_ERROR
,
184 } as OCPP16StatusNotificationRequest
;
185 case OCPPVersion
.VERSION_20
:
186 case OCPPVersion
.VERSION_201
:
188 timestamp
: new Date(),
189 connectorStatus
: status,
192 } as OCPP20StatusNotificationRequest
;
194 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
198 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
199 if (!chargingStation
.heartbeatSetInterval
) {
200 chargingStation
.startHeartbeat();
201 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
202 chargingStation
.restartHeartbeat();
206 public static async sendAndSetConnectorStatus(
207 chargingStation
: ChargingStation
,
209 status: ConnectorStatusEnum
,
211 options
?: { send
: boolean },
213 options
= { send
: true, ...options
};
215 OCPPServiceUtils
.checkConnectorStatusTransition(chargingStation
, connectorId
, status);
216 await chargingStation
.ocppRequestService
.requestHandler
<
217 StatusNotificationRequest
,
218 StatusNotificationResponse
221 RequestCommand
.STATUS_NOTIFICATION
,
222 OCPPServiceUtils
.buildStatusNotificationRequest(
230 chargingStation
.getConnectorStatus(connectorId
)!.status = status;
231 chargingStation
.emit(ChargingStationEvents
.connectorStatusChanged
, {
233 ...chargingStation
.getConnectorStatus(connectorId
),
237 public static async isIdTagAuthorized(
238 chargingStation
: ChargingStation
,
241 ): Promise
<boolean> {
243 !chargingStation
.getLocalAuthListEnabled() &&
244 !chargingStation
.stationInfo
?.remoteAuthorization
247 `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`,
251 chargingStation
.getLocalAuthListEnabled() === true &&
252 OCPPServiceUtils
.isIdTagLocalAuthorized(chargingStation
, idTag
)
254 const connectorStatus
: ConnectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
255 connectorStatus
.localAuthorizeIdTag
= idTag
;
256 connectorStatus
.idTagLocalAuthorized
= true;
258 } else if (chargingStation
.stationInfo
?.remoteAuthorization
) {
259 return await OCPPServiceUtils
.isIdTagRemoteAuthorized(chargingStation
, connectorId
, idTag
);
264 protected static checkConnectorStatusTransition(
265 chargingStation
: ChargingStation
,
267 status: ConnectorStatusEnum
,
269 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
)!.status;
270 let transitionAllowed
= false;
271 switch (chargingStation
.stationInfo
?.ocppVersion
) {
272 case OCPPVersion
.VERSION_16
:
274 (connectorId
=== 0 &&
275 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
276 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
279 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
280 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
283 transitionAllowed
= true;
286 case OCPPVersion
.VERSION_20
:
287 case OCPPVersion
.VERSION_201
:
289 (connectorId
=== 0 &&
290 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
291 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
294 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
295 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
298 transitionAllowed
= true;
303 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
304 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
307 if (transitionAllowed
=== false) {
309 `${chargingStation.logPrefix()} OCPP ${chargingStation.stationInfo
310 ?.ocppVersion} connector id ${connectorId} status transition from '${
311 chargingStation.getConnectorStatus(connectorId)!.status
312 }' to '${status}' is not allowed`,
315 return transitionAllowed
;
318 protected static parseJsonSchemaFile
<T
extends JsonType
>(
319 relativePath
: string,
320 ocppVersion
: OCPPVersion
,
323 ): JSONSchemaType
<T
> {
324 const filePath
= join(dirname(fileURLToPath(import.meta
.url
)), relativePath
);
326 return JSON
.parse(readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
331 error
as NodeJS
.ErrnoException
,
332 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
333 { throwError
: false },
335 return {} as JSONSchemaType
<T
>;
339 protected static getSampledValueTemplate(
340 chargingStation
: ChargingStation
,
342 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
343 phase
?: MeterValuePhase
,
344 ): SampledValueTemplate
| undefined {
345 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
346 if (OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(measurand
) === false) {
348 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
353 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
356 StandardParametersKey
.MeterValuesSampledData
,
357 )?.value
?.includes(measurand
) === false
360 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
361 StandardParametersKey.MeterValuesSampledData
366 const sampledValueTemplates
: SampledValueTemplate
[] =
367 chargingStation
.getConnectorStatus(connectorId
)!.MeterValues
;
370 isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
374 OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(
375 sampledValueTemplates
[index
]?.measurand
??
376 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
380 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
384 sampledValueTemplates
[index
]?.phase
=== phase
&&
385 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
388 StandardParametersKey
.MeterValuesSampledData
,
389 )?.value
?.includes(measurand
) === true
391 return sampledValueTemplates
[index
];
394 !sampledValueTemplates
[index
]?.phase
&&
395 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
398 StandardParametersKey
.MeterValuesSampledData
,
399 )?.value
?.includes(measurand
) === true
401 return sampledValueTemplates
[index
];
403 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
404 (!sampledValueTemplates
[index
]?.measurand
||
405 sampledValueTemplates
[index
]?.measurand
=== measurand
)
407 return sampledValueTemplates
[index
];
410 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
411 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
412 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
413 throw new BaseError(errorMsg
);
416 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
420 protected static getLimitFromSampledValueTemplateCustomValue(
421 value
: string | undefined,
424 options
?: { limitationEnabled
?: boolean; fallbackValue
?: number; unitMultiplier
?: number },
428 limitationEnabled
: true,
434 const parsedValue
= parseInt(value
?? '');
435 if (options
?.limitationEnabled
) {
437 min((!isNaN(parsedValue
) ? parsedValue
: Infinity) * options
.unitMultiplier
!, maxLimit
),
441 return (!isNaN(parsedValue
) ? parsedValue
: options
.fallbackValue
!) * options
.unitMultiplier
!;
444 private static isIdTagLocalAuthorized(chargingStation
: ChargingStation
, idTag
: string): boolean {
446 chargingStation
.hasIdTags() === true &&
448 chargingStation
.idTagsCache
449 .getIdTags(getIdTagsFile(chargingStation
.stationInfo
)!)
450 ?.find((tag
) => tag
=== idTag
),
455 private static async isIdTagRemoteAuthorized(
456 chargingStation
: ChargingStation
,
459 ): Promise
<boolean> {
460 chargingStation
.getConnectorStatus(connectorId
)!.authorizeIdTag
= idTag
;
463 await chargingStation
.ocppRequestService
.requestHandler
<
466 >(chargingStation
, RequestCommand
.AUTHORIZE
, {
469 )?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
473 private static logPrefix
= (
474 ocppVersion
: OCPPVersion
,
479 isNotEmptyString(moduleName
) && isNotEmptyString(methodName
)
480 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
481 : ` OCPP ${ocppVersion} |`;
482 return logPrefix(logMsg
);