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 const getMessageTypeString
= (messageType
: MessageType
): string => {
49 switch (messageType
) {
50 case MessageType
.CALL_MESSAGE
:
52 case MessageType
.CALL_RESULT_MESSAGE
:
54 case MessageType
.CALL_ERROR_MESSAGE
:
61 export const buildStatusNotificationRequest
= (
62 chargingStation
: ChargingStation
,
64 status: ConnectorStatusEnum
,
66 ): StatusNotificationRequest
=> {
67 switch (chargingStation
.stationInfo
?.ocppVersion
) {
68 case OCPPVersion
.VERSION_16
:
72 errorCode
: ChargePointErrorCode
.NO_ERROR
,
73 } as OCPP16StatusNotificationRequest
;
74 case OCPPVersion
.VERSION_20
:
75 case OCPPVersion
.VERSION_201
:
77 timestamp
: new Date(),
78 connectorStatus
: status,
81 } as OCPP20StatusNotificationRequest
;
83 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
87 export const isIdTagAuthorized
= async (
88 chargingStation
: ChargingStation
,
91 ): Promise
<boolean> => {
93 !chargingStation
.getLocalAuthListEnabled() &&
94 !chargingStation
.stationInfo
?.remoteAuthorization
97 `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`,
101 chargingStation
.getLocalAuthListEnabled() === true &&
102 isIdTagLocalAuthorized(chargingStation
, idTag
)
104 const connectorStatus
: ConnectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
105 connectorStatus
.localAuthorizeIdTag
= idTag
;
106 connectorStatus
.idTagLocalAuthorized
= true;
108 } else if (chargingStation
.stationInfo
?.remoteAuthorization
) {
109 return await isIdTagRemoteAuthorized(chargingStation
, connectorId
, idTag
);
114 const isIdTagLocalAuthorized
= (chargingStation
: ChargingStation
, idTag
: string): boolean => {
116 chargingStation
.hasIdTags() === true &&
118 chargingStation
.idTagsCache
119 .getIdTags(getIdTagsFile(chargingStation
.stationInfo
)!)
120 ?.find((tag
) => tag
=== idTag
),
125 const isIdTagRemoteAuthorized
= async (
126 chargingStation
: ChargingStation
,
129 ): Promise
<boolean> => {
130 chargingStation
.getConnectorStatus(connectorId
)!.authorizeIdTag
= idTag
;
133 await chargingStation
.ocppRequestService
.requestHandler
<AuthorizeRequest
, AuthorizeResponse
>(
135 RequestCommand
.AUTHORIZE
,
140 )?.idTagInfo
?.status === AuthorizationStatus
.ACCEPTED
144 export const sendAndSetConnectorStatus
= async (
145 chargingStation
: ChargingStation
,
147 status: ConnectorStatusEnum
,
149 options
?: { send
: boolean },
150 ): Promise
<void> => {
151 options
= { send
: true, ...options
};
153 checkConnectorStatusTransition(chargingStation
, connectorId
, status);
154 await chargingStation
.ocppRequestService
.requestHandler
<
155 StatusNotificationRequest
,
156 StatusNotificationResponse
159 RequestCommand
.STATUS_NOTIFICATION
,
160 buildStatusNotificationRequest(chargingStation
, connectorId
, status, evseId
),
163 chargingStation
.getConnectorStatus(connectorId
)!.status = status;
164 chargingStation
.emit(ChargingStationEvents
.connectorStatusChanged
, {
166 ...chargingStation
.getConnectorStatus(connectorId
),
170 const checkConnectorStatusTransition
= (
171 chargingStation
: ChargingStation
,
173 status: ConnectorStatusEnum
,
175 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
)!.status;
176 let transitionAllowed
= false;
177 switch (chargingStation
.stationInfo
?.ocppVersion
) {
178 case OCPPVersion
.VERSION_16
:
180 (connectorId
=== 0 &&
181 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
182 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
185 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
186 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
189 transitionAllowed
= true;
192 case OCPPVersion
.VERSION_20
:
193 case OCPPVersion
.VERSION_201
:
195 (connectorId
=== 0 &&
196 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
197 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
200 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
201 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
204 transitionAllowed
= true;
209 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
210 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
213 if (transitionAllowed
=== false) {
215 `${chargingStation.logPrefix()} OCPP ${chargingStation.stationInfo
216 ?.ocppVersion} connector id ${connectorId} status transition from '${
217 chargingStation.getConnectorStatus(connectorId)!.status
218 }' to '${status}' is not allowed`,
221 return transitionAllowed
;
224 export class OCPPServiceUtils
{
225 public static getMessageTypeString
= getMessageTypeString
;
226 public static sendAndSetConnectorStatus
= sendAndSetConnectorStatus
;
227 public static isIdTagAuthorized
= isIdTagAuthorized
;
229 protected constructor() {
230 // This is intentional
233 public static ajvErrorsToErrorType(errors
: ErrorObject
[] | null | undefined): ErrorType
{
234 if (isNotEmptyArray(errors
) === true) {
235 for (const error
of errors
as DefinedError
[]) {
236 switch (error
.keyword
) {
238 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
241 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
244 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
248 return ErrorType
.FORMAT_VIOLATION
;
251 public static isRequestCommandSupported(
252 chargingStation
: ChargingStation
,
253 command
: RequestCommand
,
255 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
257 isRequestCommand
=== true &&
258 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
262 isRequestCommand
=== true &&
263 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
?.[command
]
265 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
];
267 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
271 public static isIncomingRequestCommandSupported(
272 chargingStation
: ChargingStation
,
273 command
: IncomingRequestCommand
,
275 const isIncomingRequestCommand
=
276 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
278 isIncomingRequestCommand
=== true &&
279 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
283 isIncomingRequestCommand
=== true &&
284 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
?.[command
]
286 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
];
288 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
292 public static isMessageTriggerSupported(
293 chargingStation
: ChargingStation
,
294 messageTrigger
: MessageTrigger
,
296 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
297 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
300 isMessageTrigger
=== true &&
301 chargingStation
.stationInfo
?.messageTriggerSupport
?.[messageTrigger
]
303 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
];
306 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
311 public static isConnectorIdValid(
312 chargingStation
: ChargingStation
,
313 ocppCommand
: IncomingRequestCommand
,
316 if (connectorId
< 0) {
318 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
325 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
326 for (const key
in obj
) {
327 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
328 if (isDate(obj
![key
])) {
329 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
330 (obj
![key
] as string) = (obj
![key
] as Date).toISOString();
331 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
332 } else if (obj
![key
] !== null && typeof obj
![key
] === 'object') {
333 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
334 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
![key
] as T
);
339 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
340 if (chargingStation
.heartbeatSetInterval
=== undefined) {
341 chargingStation
.startHeartbeat();
342 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
343 chargingStation
.restartHeartbeat();
347 protected static parseJsonSchemaFile
<T
extends JsonType
>(
348 relativePath
: string,
349 ocppVersion
: OCPPVersion
,
352 ): JSONSchemaType
<T
> {
353 const filePath
= join(dirname(fileURLToPath(import.meta
.url
)), relativePath
);
355 return JSON
.parse(readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
360 error
as NodeJS
.ErrnoException
,
361 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
362 { throwError
: false },
364 return {} as JSONSchemaType
<T
>;
368 protected static getSampledValueTemplate(
369 chargingStation
: ChargingStation
,
371 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
372 phase
?: MeterValuePhase
,
373 ): SampledValueTemplate
| undefined {
374 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
375 if (OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(measurand
) === false) {
377 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
382 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
385 StandardParametersKey
.MeterValuesSampledData
,
386 )?.value
?.includes(measurand
) === false
389 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
390 StandardParametersKey.MeterValuesSampledData
395 const sampledValueTemplates
: SampledValueTemplate
[] =
396 chargingStation
.getConnectorStatus(connectorId
)!.MeterValues
;
399 isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
403 OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(
404 sampledValueTemplates
[index
]?.measurand
??
405 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
409 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
413 sampledValueTemplates
[index
]?.phase
=== phase
&&
414 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
417 StandardParametersKey
.MeterValuesSampledData
,
418 )?.value
?.includes(measurand
) === true
420 return sampledValueTemplates
[index
];
423 !sampledValueTemplates
[index
]?.phase
&&
424 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
427 StandardParametersKey
.MeterValuesSampledData
,
428 )?.value
?.includes(measurand
) === true
430 return sampledValueTemplates
[index
];
432 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
433 (!sampledValueTemplates
[index
]?.measurand
||
434 sampledValueTemplates
[index
]?.measurand
=== measurand
)
436 return sampledValueTemplates
[index
];
439 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
440 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
441 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
442 throw new BaseError(errorMsg
);
445 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
449 protected static getLimitFromSampledValueTemplateCustomValue(
450 value
: string | undefined,
453 options
?: { limitationEnabled
?: boolean; fallbackValue
?: number; unitMultiplier
?: number },
457 limitationEnabled
: false,
463 const parsedValue
= parseInt(value
?? '');
464 if (options
?.limitationEnabled
) {
466 min((!isNaN(parsedValue
) ? parsedValue
: Infinity) * options
.unitMultiplier
!, maxLimit
),
470 return (!isNaN(parsedValue
) ? parsedValue
: options
.fallbackValue
!) * options
.unitMultiplier
!;
473 private static logPrefix
= (
474 ocppVersion
: OCPPVersion
,
479 isNotEmptyString(moduleName
) && isNotEmptyString(methodName
)
480 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
481 : ` OCPP ${ocppVersion} |`;
482 return logPrefix(logMsg
);