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';
7 import { OCPP16Constants
} from
'./1.6/OCPP16Constants';
8 import { OCPP20Constants
} from
'./2.0/OCPP20Constants';
9 import { OCPPConstants
} from
'./OCPPConstants';
10 import { type ChargingStation
, getConfigurationKey
} from
'../../charging-station';
11 import { BaseError
} from
'../../exception';
14 type ConnectorStatusEnum
,
17 IncomingRequestCommand
,
23 type OCPP16StatusNotificationRequest
,
24 type OCPP20StatusNotificationRequest
,
27 type SampledValueTemplate
,
28 StandardParametersKey
,
29 type StatusNotificationRequest
,
30 type StatusNotificationResponse
,
40 export class OCPPServiceUtils
{
41 protected constructor() {
42 // This is intentional
45 public static ajvErrorsToErrorType(errors
: ErrorObject
[]): ErrorType
{
46 for (const error
of errors
as DefinedError
[]) {
47 switch (error
.keyword
) {
49 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
52 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
55 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
58 return ErrorType
.FORMAT_VIOLATION
;
61 public static getMessageTypeString(messageType
: MessageType
): string {
62 switch (messageType
) {
63 case MessageType
.CALL_MESSAGE
:
65 case MessageType
.CALL_RESULT_MESSAGE
:
67 case MessageType
.CALL_ERROR_MESSAGE
:
74 public static isRequestCommandSupported(
75 chargingStation
: ChargingStation
,
76 command
: RequestCommand
,
78 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
80 isRequestCommand
=== true &&
81 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
85 isRequestCommand
=== true &&
86 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
88 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
90 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
94 public static isIncomingRequestCommandSupported(
95 chargingStation
: ChargingStation
,
96 command
: IncomingRequestCommand
,
98 const isIncomingRequestCommand
=
99 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
101 isIncomingRequestCommand
=== true &&
102 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
106 isIncomingRequestCommand
=== true &&
107 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
109 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
111 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
115 public static isMessageTriggerSupported(
116 chargingStation
: ChargingStation
,
117 messageTrigger
: MessageTrigger
,
119 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
120 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
122 } else if (isMessageTrigger
=== true && chargingStation
.stationInfo
?.messageTriggerSupport
) {
123 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
] ?? false;
126 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
131 public static isConnectorIdValid(
132 chargingStation
: ChargingStation
,
133 ocppCommand
: IncomingRequestCommand
,
136 if (connectorId
< 0) {
138 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
145 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
146 for (const key
in obj
) {
147 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
148 if (obj
![key
] instanceof Date) {
149 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
150 (obj
![key
] as string) = (obj
![key
] as Date).toISOString();
151 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
152 } else if (obj
![key
] !== null && typeof obj
![key
] === 'object') {
153 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
154 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
![key
] as T
);
159 public static buildStatusNotificationRequest(
160 chargingStation
: ChargingStation
,
162 status: ConnectorStatusEnum
,
164 ): StatusNotificationRequest
{
165 switch (chargingStation
.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
) {
166 case OCPPVersion
.VERSION_16
:
170 errorCode
: ChargePointErrorCode
.NO_ERROR
,
171 } as OCPP16StatusNotificationRequest
;
172 case OCPPVersion
.VERSION_20
:
173 case OCPPVersion
.VERSION_201
:
175 timestamp
: new Date(),
176 connectorStatus
: status,
179 } as OCPP20StatusNotificationRequest
;
181 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
185 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
186 if (!chargingStation
.heartbeatSetInterval
) {
187 chargingStation
.startHeartbeat();
188 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
189 chargingStation
.restartHeartbeat();
193 public static async sendAndSetConnectorStatus(
194 chargingStation
: ChargingStation
,
196 status: ConnectorStatusEnum
,
198 options
?: { send
: boolean },
200 options
= { send
: true, ...options
};
202 OCPPServiceUtils
.checkConnectorStatusTransition(chargingStation
, connectorId
, status);
203 await chargingStation
.ocppRequestService
.requestHandler
<
204 StatusNotificationRequest
,
205 StatusNotificationResponse
208 RequestCommand
.STATUS_NOTIFICATION
,
209 OCPPServiceUtils
.buildStatusNotificationRequest(
217 chargingStation
.getConnectorStatus(connectorId
)!.status = status;
220 protected static checkConnectorStatusTransition(
221 chargingStation
: ChargingStation
,
223 status: ConnectorStatusEnum
,
225 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
)!.status;
226 let transitionAllowed
= false;
227 switch (chargingStation
.stationInfo
.ocppVersion
) {
228 case OCPPVersion
.VERSION_16
:
230 (connectorId
=== 0 &&
231 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
232 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
235 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
236 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
239 transitionAllowed
= true;
242 case OCPPVersion
.VERSION_20
:
243 case OCPPVersion
.VERSION_201
:
245 (connectorId
=== 0 &&
246 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
247 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
250 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
251 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
254 transitionAllowed
= true;
259 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
260 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
263 if (transitionAllowed
=== false) {
265 `${chargingStation.logPrefix()} OCPP ${
266 chargingStation.stationInfo.ocppVersion
267 } connector id ${connectorId} status transition from '${
268 chargingStation.getConnectorStatus(connectorId)!.status
269 }' to '${status}' is not allowed`,
272 return transitionAllowed
;
275 protected static parseJsonSchemaFile
<T
extends JsonType
>(
276 relativePath
: string,
277 ocppVersion
: OCPPVersion
,
280 ): JSONSchemaType
<T
> {
281 const filePath
= join(dirname(fileURLToPath(import.meta
.url
)), relativePath
);
283 return JSON
.parse(readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
288 error
as NodeJS
.ErrnoException
,
289 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
290 { throwError
: false },
292 return {} as JSONSchemaType
<T
>;
296 protected static getSampledValueTemplate(
297 chargingStation
: ChargingStation
,
299 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
300 phase
?: MeterValuePhase
,
301 ): SampledValueTemplate
| undefined {
302 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
303 if (OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(measurand
) === false) {
305 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
310 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
313 StandardParametersKey
.MeterValuesSampledData
,
314 )?.value
?.includes(measurand
) === false
317 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
318 StandardParametersKey.MeterValuesSampledData
323 const sampledValueTemplates
: SampledValueTemplate
[] =
324 chargingStation
.getConnectorStatus(connectorId
)!.MeterValues
;
327 isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
331 OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(
332 sampledValueTemplates
[index
]?.measurand
??
333 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
337 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
341 sampledValueTemplates
[index
]?.phase
=== phase
&&
342 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
345 StandardParametersKey
.MeterValuesSampledData
,
346 )?.value
?.includes(measurand
) === true
348 return sampledValueTemplates
[index
];
351 !sampledValueTemplates
[index
].phase
&&
352 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
355 StandardParametersKey
.MeterValuesSampledData
,
356 )?.value
?.includes(measurand
) === true
358 return sampledValueTemplates
[index
];
360 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
361 (!sampledValueTemplates
[index
].measurand
||
362 sampledValueTemplates
[index
].measurand
=== measurand
)
364 return sampledValueTemplates
[index
];
367 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
368 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
369 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
370 throw new BaseError(errorMsg
);
373 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
377 protected static getLimitFromSampledValueTemplateCustomValue(
380 options
: { limitationEnabled
?: boolean; unitMultiplier
?: number } = {
381 limitationEnabled
: true,
387 limitationEnabled
: true,
392 const parsedInt
= parseInt(value
);
393 const numberValue
= isNaN(parsedInt
) ? Infinity : parsedInt
;
394 return options
?.limitationEnabled
395 ? Math.min(numberValue
* options
.unitMultiplier
!, limit
)
396 : numberValue
* options
.unitMultiplier
!;
399 private static logPrefix
= (
400 ocppVersion
: OCPPVersion
,
405 isNotEmptyString(moduleName
) && isNotEmptyString(methodName
)
406 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
407 : ` OCPP ${ocppVersion} |`;
408 return logPrefix(logMsg
);