1 import fs from
'node:fs';
2 import path 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 { type ChargingStation
, ChargingStationConfigurationUtils
} from
'../../charging-station';
10 import { BaseError
} from
'../../exception';
13 type ConnectorStatusEnum
,
16 IncomingRequestCommand
,
23 type OCPP16StatusNotificationRequest
,
24 type OCPP20StatusNotificationRequest
,
27 type SampledValueTemplate
,
28 StandardParametersKey
,
29 type StatusNotificationRequest
,
30 type StatusNotificationResponse
,
32 import { Constants
, ErrorUtils
, Utils
, logger
} from
'../../utils';
34 export class OCPPServiceUtils
{
35 protected constructor() {
36 // This is intentional
39 public static ajvErrorsToErrorType(errors
: ErrorObject
[]): ErrorType
{
40 for (const error
of errors
as DefinedError
[]) {
41 switch (error
.keyword
) {
43 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
46 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
49 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
52 return ErrorType
.FORMAT_VIOLATION
;
55 public static getMessageTypeString(messageType
: MessageType
): string {
56 switch (messageType
) {
57 case MessageType
.CALL_MESSAGE
:
59 case MessageType
.CALL_RESULT_MESSAGE
:
61 case MessageType
.CALL_ERROR_MESSAGE
:
68 public static isRequestCommandSupported(
69 chargingStation
: ChargingStation
,
70 command
: RequestCommand
72 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
74 isRequestCommand
=== true &&
75 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
79 isRequestCommand
=== true &&
80 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
82 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
84 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
88 public static isIncomingRequestCommandSupported(
89 chargingStation
: ChargingStation
,
90 command
: IncomingRequestCommand
92 const isIncomingRequestCommand
=
93 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
95 isIncomingRequestCommand
=== true &&
96 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
100 isIncomingRequestCommand
=== true &&
101 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
103 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
105 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
109 public static isMessageTriggerSupported(
110 chargingStation
: ChargingStation
,
111 messageTrigger
: MessageTrigger
113 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
114 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
116 } else if (isMessageTrigger
=== true && chargingStation
.stationInfo
?.messageTriggerSupport
) {
117 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
] ?? false;
120 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`
125 public static isConnectorIdValid(
126 chargingStation
: ChargingStation
,
127 ocppCommand
: IncomingRequestCommand
,
130 if (connectorId
< 0) {
132 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`
139 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
140 for (const key
in obj
) {
141 if (obj
[key
] instanceof Date) {
142 (obj
as JsonObject
)[key
] = (obj
[key
] as Date).toISOString();
143 } else if (obj
[key
] !== null && typeof obj
[key
] === 'object') {
144 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
[key
] as T
);
149 public static buildStatusNotificationRequest(
150 chargingStation
: ChargingStation
,
152 status: ConnectorStatusEnum
,
154 ): StatusNotificationRequest
{
155 switch (chargingStation
.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
) {
156 case OCPPVersion
.VERSION_16
:
160 errorCode
: ChargePointErrorCode
.NO_ERROR
,
161 } as OCPP16StatusNotificationRequest
;
162 case OCPPVersion
.VERSION_20
:
163 case OCPPVersion
.VERSION_201
:
165 timestamp
: new Date(),
166 connectorStatus
: status,
169 } as OCPP20StatusNotificationRequest
;
171 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
175 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
176 if (!chargingStation
.heartbeatSetInterval
) {
177 chargingStation
.startHeartbeat();
178 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
179 chargingStation
.restartHeartbeat();
183 public static async sendAndSetConnectorStatus(
184 chargingStation
: ChargingStation
,
186 status: ConnectorStatusEnum
,
189 OCPPServiceUtils
.checkConnectorStatusTransition(chargingStation
, connectorId
, status);
190 await chargingStation
.ocppRequestService
.requestHandler
<
191 StatusNotificationRequest
,
192 StatusNotificationResponse
195 RequestCommand
.STATUS_NOTIFICATION
,
196 OCPPServiceUtils
.buildStatusNotificationRequest(chargingStation
, connectorId
, status, evseId
)
198 chargingStation
.getConnectorStatus(connectorId
).status = status;
201 protected static checkConnectorStatusTransition(
202 chargingStation
: ChargingStation
,
204 status: ConnectorStatusEnum
206 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
).status;
207 let transitionAllowed
= false;
208 switch (chargingStation
.stationInfo
.ocppVersion
) {
209 case OCPPVersion
.VERSION_16
:
211 (connectorId
=== 0 &&
212 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
213 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
216 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
217 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
220 transitionAllowed
= true;
223 case OCPPVersion
.VERSION_20
:
224 case OCPPVersion
.VERSION_201
:
226 (connectorId
=== 0 &&
227 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
228 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
231 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
232 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
235 transitionAllowed
= true;
240 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
241 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`
244 if (transitionAllowed
=== false) {
246 `${chargingStation.logPrefix()} OCPP ${
247 chargingStation.stationInfo.ocppVersion
248 } connector id ${connectorId} status transition from '${
249 chargingStation.getConnectorStatus(connectorId).status
250 }' to '${status}' is not allowed`
253 return transitionAllowed
;
256 protected static parseJsonSchemaFile
<T
extends JsonType
>(
257 relativePath
: string,
258 ocppVersion
: OCPPVersion
,
261 ): JSONSchemaType
<T
> {
262 const filePath
= path
.join(path
.dirname(fileURLToPath(import.meta
.url
)), relativePath
);
264 return JSON
.parse(fs
.readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
266 ErrorUtils
.handleFileException(
269 error
as NodeJS
.ErrnoException
,
270 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
271 { throwError
: false }
276 protected static getSampledValueTemplate(
277 chargingStation
: ChargingStation
,
279 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
280 phase
?: MeterValuePhase
281 ): SampledValueTemplate
| undefined {
282 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
283 if (Constants
.SUPPORTED_MEASURANDS
.includes(measurand
) === false) {
285 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
290 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
291 ChargingStationConfigurationUtils
.getConfigurationKey(
293 StandardParametersKey
.MeterValuesSampledData
294 )?.value
?.includes(measurand
) === false
297 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
298 StandardParametersKey.MeterValuesSampledData
303 const sampledValueTemplates
: SampledValueTemplate
[] =
304 chargingStation
.getConnectorStatus(connectorId
)?.MeterValues
;
307 Utils
.isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
311 Constants
.SUPPORTED_MEASURANDS
.includes(
312 sampledValueTemplates
[index
]?.measurand
??
313 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
317 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
321 sampledValueTemplates
[index
]?.phase
=== phase
&&
322 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
323 ChargingStationConfigurationUtils
.getConfigurationKey(
325 StandardParametersKey
.MeterValuesSampledData
326 )?.value
?.includes(measurand
) === true
328 return sampledValueTemplates
[index
];
331 !sampledValueTemplates
[index
].phase
&&
332 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
333 ChargingStationConfigurationUtils
.getConfigurationKey(
335 StandardParametersKey
.MeterValuesSampledData
336 )?.value
?.includes(measurand
) === true
338 return sampledValueTemplates
[index
];
340 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
341 (!sampledValueTemplates
[index
].measurand
||
342 sampledValueTemplates
[index
].measurand
=== measurand
)
344 return sampledValueTemplates
[index
];
347 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
348 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
349 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
350 throw new BaseError(errorMsg
);
353 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
357 protected static getLimitFromSampledValueTemplateCustomValue(
360 options
: { limitationEnabled
?: boolean; unitMultiplier
?: number } = {
361 limitationEnabled
: true,
365 options
.limitationEnabled
= options
?.limitationEnabled
?? true;
366 options
.unitMultiplier
= options
?.unitMultiplier
?? 1;
367 const parsedInt
= parseInt(value
);
368 const numberValue
= isNaN(parsedInt
) ? Infinity : parsedInt
;
369 return options
?.limitationEnabled
370 ? Math.min(numberValue
* options
.unitMultiplier
, limit
)
371 : numberValue
* options
.unitMultiplier
;
374 private static logPrefix
= (
375 ocppVersion
: OCPPVersion
,
380 Utils
.isNotEmptyString(moduleName
) && Utils
.isNotEmptyString(methodName
)
381 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
382 : ` OCPP ${ocppVersion} |`;
383 return Utils
.logPrefix(logMsg
);