1 import fs from
'node:fs';
3 import type { DefinedError
, ErrorObject
, JSONSchemaType
} from
'ajv';
5 import { OCPP16Constants
} from
'./1.6/OCPP16Constants';
6 import { OCPP20Constants
} from
'./2.0/OCPP20Constants';
7 import { type ChargingStation
, ChargingStationConfigurationUtils
} from
'../../charging-station';
8 import { BaseError
} from
'../../exception';
11 type ConnectorStatusEnum
,
14 IncomingRequestCommand
,
21 type OCPP16StatusNotificationRequest
,
22 type OCPP20StatusNotificationRequest
,
25 type SampledValueTemplate
,
26 StandardParametersKey
,
27 type StatusNotificationRequest
,
28 type StatusNotificationResponse
,
30 import { Constants
, FileUtils
, Utils
, logger
} from
'../../utils';
32 export class OCPPServiceUtils
{
33 protected constructor() {
34 // This is intentional
37 public static ajvErrorsToErrorType(errors
: ErrorObject
[]): ErrorType
{
38 for (const error
of errors
as DefinedError
[]) {
39 switch (error
.keyword
) {
41 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
44 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
47 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
50 return ErrorType
.FORMAT_VIOLATION
;
53 public static getMessageTypeString(messageType
: MessageType
): string {
54 switch (messageType
) {
55 case MessageType
.CALL_MESSAGE
:
57 case MessageType
.CALL_RESULT_MESSAGE
:
59 case MessageType
.CALL_ERROR_MESSAGE
:
66 public static isRequestCommandSupported(
67 chargingStation
: ChargingStation
,
68 command
: RequestCommand
70 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
72 isRequestCommand
=== true &&
73 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
77 isRequestCommand
=== true &&
78 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
80 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
82 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
86 public static isIncomingRequestCommandSupported(
87 chargingStation
: ChargingStation
,
88 command
: IncomingRequestCommand
90 const isIncomingRequestCommand
=
91 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
93 isIncomingRequestCommand
=== true &&
94 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
98 isIncomingRequestCommand
=== true &&
99 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
101 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
103 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
107 public static isMessageTriggerSupported(
108 chargingStation
: ChargingStation
,
109 messageTrigger
: MessageTrigger
111 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
112 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
114 } else if (isMessageTrigger
=== true && chargingStation
.stationInfo
?.messageTriggerSupport
) {
115 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
] ?? false;
118 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`
123 public static isConnectorIdValid(
124 chargingStation
: ChargingStation
,
125 ocppCommand
: IncomingRequestCommand
,
128 if (connectorId
< 0) {
130 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`
137 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
138 for (const key
in obj
) {
139 if (obj
[key
] instanceof Date) {
140 (obj
as JsonObject
)[key
] = (obj
[key
] as Date).toISOString();
141 } else if (obj
[key
] !== null && typeof obj
[key
] === 'object') {
142 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
[key
] as T
);
147 public static buildStatusNotificationRequest(
148 chargingStation
: ChargingStation
,
150 status: ConnectorStatusEnum
,
152 ): StatusNotificationRequest
{
153 switch (chargingStation
.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
) {
154 case OCPPVersion
.VERSION_16
:
158 errorCode
: ChargePointErrorCode
.NO_ERROR
,
159 } as OCPP16StatusNotificationRequest
;
160 case OCPPVersion
.VERSION_20
:
161 case OCPPVersion
.VERSION_201
:
163 timestamp
: new Date(),
164 connectorStatus
: status,
167 } as OCPP20StatusNotificationRequest
;
169 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
173 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
174 if (!chargingStation
.heartbeatSetInterval
) {
175 chargingStation
.startHeartbeat();
176 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
177 chargingStation
.restartHeartbeat();
181 public static async sendAndSetConnectorStatus(
182 chargingStation
: ChargingStation
,
184 status: ConnectorStatusEnum
,
187 OCPPServiceUtils
.checkConnectorStatusTransition(chargingStation
, connectorId
, status);
188 await chargingStation
.ocppRequestService
.requestHandler
<
189 StatusNotificationRequest
,
190 StatusNotificationResponse
193 RequestCommand
.STATUS_NOTIFICATION
,
194 OCPPServiceUtils
.buildStatusNotificationRequest(chargingStation
, connectorId
, status, evseId
)
196 chargingStation
.getConnectorStatus(connectorId
).status = status;
199 protected static checkConnectorStatusTransition(
200 chargingStation
: ChargingStation
,
202 status: ConnectorStatusEnum
204 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
).status;
205 let transitionAllowed
= false;
206 switch (chargingStation
.stationInfo
.ocppVersion
) {
207 case OCPPVersion
.VERSION_16
:
209 (connectorId
=== 0 &&
210 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
211 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
214 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
215 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
218 transitionAllowed
= true;
221 case OCPPVersion
.VERSION_20
:
222 case OCPPVersion
.VERSION_201
:
224 (connectorId
=== 0 &&
225 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
226 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
229 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
230 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
233 transitionAllowed
= true;
238 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
239 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`
242 if (transitionAllowed
=== false) {
244 `${chargingStation.logPrefix()} OCPP ${
245 chargingStation.stationInfo.ocppVersion
246 } connector id ${connectorId} status transition from '${
247 chargingStation.getConnectorStatus(connectorId).status
248 }' to '${status}' is not allowed`
251 return transitionAllowed
;
254 protected static parseJsonSchemaFile
<T
extends JsonType
>(
256 ocppVersion
: OCPPVersion
,
259 ): JSONSchemaType
<T
> {
261 return JSON
.parse(fs
.readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
263 FileUtils
.handleFileException(
266 error
as NodeJS
.ErrnoException
,
267 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
268 { throwError
: false }
273 protected static getSampledValueTemplate(
274 chargingStation
: ChargingStation
,
276 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
277 phase
?: MeterValuePhase
278 ): SampledValueTemplate
| undefined {
279 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
280 if (Constants
.SUPPORTED_MEASURANDS
.includes(measurand
) === false) {
282 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
287 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
288 ChargingStationConfigurationUtils
.getConfigurationKey(
290 StandardParametersKey
.MeterValuesSampledData
291 )?.value
?.includes(measurand
) === false
294 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
295 StandardParametersKey.MeterValuesSampledData
300 const sampledValueTemplates
: SampledValueTemplate
[] =
301 chargingStation
.getConnectorStatus(connectorId
)?.MeterValues
;
304 Utils
.isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
308 Constants
.SUPPORTED_MEASURANDS
.includes(
309 sampledValueTemplates
[index
]?.measurand
??
310 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
314 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
318 sampledValueTemplates
[index
]?.phase
=== phase
&&
319 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
320 ChargingStationConfigurationUtils
.getConfigurationKey(
322 StandardParametersKey
.MeterValuesSampledData
323 )?.value
?.includes(measurand
) === true
325 return sampledValueTemplates
[index
];
328 !sampledValueTemplates
[index
].phase
&&
329 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
330 ChargingStationConfigurationUtils
.getConfigurationKey(
332 StandardParametersKey
.MeterValuesSampledData
333 )?.value
?.includes(measurand
) === true
335 return sampledValueTemplates
[index
];
337 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
338 (!sampledValueTemplates
[index
].measurand
||
339 sampledValueTemplates
[index
].measurand
=== measurand
)
341 return sampledValueTemplates
[index
];
344 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
345 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
346 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
347 throw new BaseError(errorMsg
);
350 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
354 protected static getLimitFromSampledValueTemplateCustomValue(
357 options
: { limitationEnabled
?: boolean; unitMultiplier
?: number } = {
358 limitationEnabled
: true,
362 options
.limitationEnabled
= options
?.limitationEnabled
?? true;
363 options
.unitMultiplier
= options
?.unitMultiplier
?? 1;
364 const parsedInt
= parseInt(value
);
365 const numberValue
= isNaN(parsedInt
) ? Infinity : parsedInt
;
366 return options
?.limitationEnabled
367 ? Math.min(numberValue
* options
.unitMultiplier
, limit
)
368 : numberValue
* options
.unitMultiplier
;
371 private static logPrefix
= (
372 ocppVersion
: OCPPVersion
,
377 Utils
.isNotEmptyString(moduleName
) && Utils
.isNotEmptyString(methodName
)
378 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
379 : ` OCPP ${ocppVersion} |`;
380 return Utils
.logPrefix(logMsg
);