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
,
46 export class OCPPServiceUtils
{
47 protected constructor() {
48 // This is intentional
51 public static ajvErrorsToErrorType(errors
: ErrorObject
[] | null | undefined): ErrorType
{
52 if (isNotEmptyArray(errors
) === true) {
53 for (const error
of errors
as DefinedError
[]) {
54 switch (error
.keyword
) {
56 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
59 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
62 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
66 return ErrorType
.FORMAT_VIOLATION
;
69 public static getMessageTypeString(messageType
: MessageType
): string {
70 switch (messageType
) {
71 case MessageType
.CALL_MESSAGE
:
73 case MessageType
.CALL_RESULT_MESSAGE
:
75 case MessageType
.CALL_ERROR_MESSAGE
:
82 public static isRequestCommandSupported(
83 chargingStation
: ChargingStation
,
84 command
: RequestCommand
,
86 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
88 isRequestCommand
=== true &&
89 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
93 isRequestCommand
=== true &&
94 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
?.[command
]
96 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
];
98 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
102 public static isIncomingRequestCommandSupported(
103 chargingStation
: ChargingStation
,
104 command
: IncomingRequestCommand
,
106 const isIncomingRequestCommand
=
107 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
109 isIncomingRequestCommand
=== true &&
110 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
114 isIncomingRequestCommand
=== true &&
115 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
?.[command
]
117 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
];
119 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
123 public static isMessageTriggerSupported(
124 chargingStation
: ChargingStation
,
125 messageTrigger
: MessageTrigger
,
127 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
128 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
131 isMessageTrigger
=== true &&
132 chargingStation
.stationInfo
?.messageTriggerSupport
?.[messageTrigger
]
134 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
];
137 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
142 public static isConnectorIdValid(
143 chargingStation
: ChargingStation
,
144 ocppCommand
: IncomingRequestCommand
,
147 if (connectorId
< 0) {
149 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
156 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
157 for (const key
in obj
) {
158 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
159 if (isDate(obj
![key
])) {
160 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
161 (obj
![key
] as string) = (obj
![key
] as Date).toISOString();
162 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
163 } else if (obj
![key
] !== null && typeof obj
![key
] === 'object') {
164 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
165 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
![key
] as T
);
170 public static buildStatusNotificationRequest(
171 chargingStation
: ChargingStation
,
173 status: ConnectorStatusEnum
,
175 ): StatusNotificationRequest
{
176 switch (chargingStation
.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
) {
177 case OCPPVersion
.VERSION_16
:
181 errorCode
: ChargePointErrorCode
.NO_ERROR
,
182 } as OCPP16StatusNotificationRequest
;
183 case OCPPVersion
.VERSION_20
:
184 case OCPPVersion
.VERSION_201
:
186 timestamp
: new Date(),
187 connectorStatus
: status,
190 } as OCPP20StatusNotificationRequest
;
192 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
196 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
197 if (!chargingStation
.heartbeatSetInterval
) {
198 chargingStation
.startHeartbeat();
199 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
200 chargingStation
.restartHeartbeat();
204 public static async sendAndSetConnectorStatus(
205 chargingStation
: ChargingStation
,
207 status: ConnectorStatusEnum
,
209 options
?: { send
: boolean },
211 options
= { send
: true, ...options
};
213 OCPPServiceUtils
.checkConnectorStatusTransition(chargingStation
, connectorId
, status);
214 await chargingStation
.ocppRequestService
.requestHandler
<
215 StatusNotificationRequest
,
216 StatusNotificationResponse
219 RequestCommand
.STATUS_NOTIFICATION
,
220 OCPPServiceUtils
.buildStatusNotificationRequest(
228 chargingStation
.getConnectorStatus(connectorId
)!.status = status;
231 public static async isIdTagAuthorized(
232 chargingStation
: ChargingStation
,
235 ): Promise
<boolean> {
236 if (!chargingStation
.getLocalAuthListEnabled() && !chargingStation
.getRemoteAuthorization()) {
238 `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`,
242 chargingStation
.getLocalAuthListEnabled() === true &&
243 OCPPServiceUtils
.isIdTagLocalAuthorized(chargingStation
, idTag
)
245 const connectorStatus
: ConnectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
246 connectorStatus
.localAuthorizeIdTag
= idTag
;
247 connectorStatus
.idTagLocalAuthorized
= true;
249 } else if (chargingStation
.getRemoteAuthorization()) {
250 return await OCPPServiceUtils
.isIdTagRemoteAuthorized(chargingStation
, connectorId
, idTag
);
255 protected static checkConnectorStatusTransition(
256 chargingStation
: ChargingStation
,
258 status: ConnectorStatusEnum
,
260 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
)!.status;
261 let transitionAllowed
= false;
262 switch (chargingStation
.stationInfo
.ocppVersion
) {
263 case OCPPVersion
.VERSION_16
:
265 (connectorId
=== 0 &&
266 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
267 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
270 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
271 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
274 transitionAllowed
= true;
277 case OCPPVersion
.VERSION_20
:
278 case OCPPVersion
.VERSION_201
:
280 (connectorId
=== 0 &&
281 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
282 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
285 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
286 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
289 transitionAllowed
= true;
294 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
295 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
298 if (transitionAllowed
=== false) {
300 `${chargingStation.logPrefix()} OCPP ${
301 chargingStation.stationInfo.ocppVersion
302 } connector id ${connectorId} status transition from '${
303 chargingStation.getConnectorStatus(connectorId)!.status
304 }' to '${status}' is not allowed`,
307 return transitionAllowed
;
310 protected static parseJsonSchemaFile
<T
extends JsonType
>(
311 relativePath
: string,
312 ocppVersion
: OCPPVersion
,
315 ): JSONSchemaType
<T
> {
316 const filePath
= join(dirname(fileURLToPath(import.meta
.url
)), relativePath
);
318 return JSON
.parse(readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
323 error
as NodeJS
.ErrnoException
,
324 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
325 { throwError
: false },
327 return {} as JSONSchemaType
<T
>;
331 protected static getSampledValueTemplate(
332 chargingStation
: ChargingStation
,
334 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
335 phase
?: MeterValuePhase
,
336 ): SampledValueTemplate
| undefined {
337 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
338 if (OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(measurand
) === false) {
340 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
345 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
348 StandardParametersKey
.MeterValuesSampledData
,
349 )?.value
?.includes(measurand
) === false
352 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
353 StandardParametersKey.MeterValuesSampledData
358 const sampledValueTemplates
: SampledValueTemplate
[] =
359 chargingStation
.getConnectorStatus(connectorId
)!.MeterValues
;
362 isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
366 OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(
367 sampledValueTemplates
[index
]?.measurand
??
368 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
372 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
376 sampledValueTemplates
[index
]?.phase
=== phase
&&
377 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
380 StandardParametersKey
.MeterValuesSampledData
,
381 )?.value
?.includes(measurand
) === true
383 return sampledValueTemplates
[index
];
386 !sampledValueTemplates
[index
]?.phase
&&
387 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
390 StandardParametersKey
.MeterValuesSampledData
,
391 )?.value
?.includes(measurand
) === true
393 return sampledValueTemplates
[index
];
395 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
396 (!sampledValueTemplates
[index
]?.measurand
||
397 sampledValueTemplates
[index
]?.measurand
=== measurand
)
399 return sampledValueTemplates
[index
];
402 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
403 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
404 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
405 throw new BaseError(errorMsg
);
408 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
412 protected static getLimitFromSampledValueTemplateCustomValue(
415 options
?: { limitationEnabled
?: boolean; unitMultiplier
?: number },
419 limitationEnabled
: true,
424 const parsedInt
= parseInt(value
);
425 const numberValue
= isNaN(parsedInt
) ? Infinity : parsedInt
;
426 return options
?.limitationEnabled
427 ? min(numberValue
* options
.unitMultiplier
!, limit
)
428 : numberValue
* options
.unitMultiplier
!;
431 private static isIdTagLocalAuthorized(chargingStation
: ChargingStation
, idTag
: string): boolean {
433 chargingStation
.hasIdTags() === true &&
435 chargingStation
.idTagsCache
436 .getIdTags(getIdTagsFile(chargingStation
.stationInfo
)!)
437 ?.find((tag
) => tag
=== idTag
),
442 private static async isIdTagRemoteAuthorized(
443 chargingStation
: ChargingStation
,
446 ): Promise
<boolean> {
447 chargingStation
.getConnectorStatus(connectorId
)!.authorizeIdTag
= idTag
;
450 await chargingStation
.ocppRequestService
.requestHandler
<
453 >(chargingStation
, RequestCommand
.AUTHORIZE
, {
456 )?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
460 private static logPrefix
= (
461 ocppVersion
: OCPPVersion
,
466 isNotEmptyString(moduleName
) && isNotEmptyString(methodName
)
467 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
468 : ` OCPP ${ocppVersion} |`;
469 return logPrefix(logMsg
);