1 import fs from
'node:fs';
3 import type { DefinedError
, ErrorObject
, JSONSchemaType
} from
'ajv';
5 import { OCPP16Constants
, OCPP20Constants
} from
'./internal';
6 import { type ChargingStation
, ChargingStationConfigurationUtils
} from
'../../charging-station';
7 import { BaseError
} from
'../../exception';
10 type ConnectorStatusEnum
,
13 IncomingRequestCommand
,
20 type OCPP16StatusNotificationRequest
,
21 type OCPP20StatusNotificationRequest
,
24 type SampledValueTemplate
,
25 StandardParametersKey
,
26 type StatusNotificationRequest
,
27 type StatusNotificationResponse
,
29 import { Constants
, FileUtils
, Utils
, logger
} from
'../../utils';
31 export class OCPPServiceUtils
{
32 protected constructor() {
33 // This is intentional
36 public static ajvErrorsToErrorType(errors
: ErrorObject
[]): ErrorType
{
37 for (const error
of errors
as DefinedError
[]) {
38 switch (error
.keyword
) {
40 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
43 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
46 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
49 return ErrorType
.FORMAT_VIOLATION
;
52 public static getMessageTypeString(messageType
: MessageType
): string {
53 switch (messageType
) {
54 case MessageType
.CALL_MESSAGE
:
56 case MessageType
.CALL_RESULT_MESSAGE
:
58 case MessageType
.CALL_ERROR_MESSAGE
:
65 public static isRequestCommandSupported(
66 chargingStation
: ChargingStation
,
67 command
: RequestCommand
69 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
71 isRequestCommand
=== true &&
72 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
76 isRequestCommand
=== true &&
77 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
79 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
81 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
85 public static isIncomingRequestCommandSupported(
86 chargingStation
: ChargingStation
,
87 command
: IncomingRequestCommand
89 const isIncomingRequestCommand
=
90 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
92 isIncomingRequestCommand
=== true &&
93 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
97 isIncomingRequestCommand
=== true &&
98 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
100 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
102 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
106 public static isMessageTriggerSupported(
107 chargingStation
: ChargingStation
,
108 messageTrigger
: MessageTrigger
110 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
111 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
113 } else if (isMessageTrigger
=== true && chargingStation
.stationInfo
?.messageTriggerSupport
) {
114 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
] ?? false;
117 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`
122 public static isConnectorIdValid(
123 chargingStation
: ChargingStation
,
124 ocppCommand
: IncomingRequestCommand
,
127 if (connectorId
< 0) {
129 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`
136 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
137 for (const key
in obj
) {
138 if (obj
[key
] instanceof Date) {
139 (obj
as JsonObject
)[key
] = (obj
[key
] as Date).toISOString();
140 } else if (obj
[key
] !== null && typeof obj
[key
] === 'object') {
141 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
[key
] as T
);
146 public static buildStatusNotificationRequest(
147 chargingStation
: ChargingStation
,
149 status: ConnectorStatusEnum
,
151 ): StatusNotificationRequest
{
152 switch (chargingStation
.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
) {
153 case OCPPVersion
.VERSION_16
:
157 errorCode
: ChargePointErrorCode
.NO_ERROR
,
158 } as OCPP16StatusNotificationRequest
;
159 case OCPPVersion
.VERSION_20
:
160 case OCPPVersion
.VERSION_201
:
162 timestamp
: new Date(),
163 connectorStatus
: status,
166 } as OCPP20StatusNotificationRequest
;
168 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
172 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
173 if (!chargingStation
.heartbeatSetInterval
) {
174 chargingStation
.startHeartbeat();
175 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
176 chargingStation
.restartHeartbeat();
180 public static async sendAndSetConnectorStatus(
181 chargingStation
: ChargingStation
,
183 status: ConnectorStatusEnum
,
186 OCPPServiceUtils
.checkConnectorStatusTransition(chargingStation
, connectorId
, status);
187 await chargingStation
.ocppRequestService
.requestHandler
<
188 StatusNotificationRequest
,
189 StatusNotificationResponse
192 RequestCommand
.STATUS_NOTIFICATION
,
193 OCPPServiceUtils
.buildStatusNotificationRequest(chargingStation
, connectorId
, status, evseId
)
195 chargingStation
.getConnectorStatus(connectorId
).status = status;
198 protected static checkConnectorStatusTransition(
199 chargingStation
: ChargingStation
,
201 status: ConnectorStatusEnum
203 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
).status;
204 let transitionAllowed
= false;
205 switch (chargingStation
.stationInfo
.ocppVersion
) {
206 case OCPPVersion
.VERSION_16
:
208 (connectorId
=== 0 &&
209 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
210 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
213 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
214 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
217 transitionAllowed
= true;
220 case OCPPVersion
.VERSION_20
:
221 case OCPPVersion
.VERSION_201
:
223 (connectorId
=== 0 &&
224 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
225 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
228 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
229 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
232 transitionAllowed
= true;
237 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
238 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`
241 if (transitionAllowed
=== false) {
243 `${chargingStation.logPrefix()} OCPP ${
244 chargingStation.stationInfo.ocppVersion
245 } connector id ${connectorId} status transition from '${
246 chargingStation.getConnectorStatus(connectorId).status
247 }' to '${status}' is not allowed`
250 return transitionAllowed
;
253 protected static parseJsonSchemaFile
<T
extends JsonType
>(
255 ocppVersion
: OCPPVersion
,
258 ): JSONSchemaType
<T
> {
260 return JSON
.parse(fs
.readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
262 FileUtils
.handleFileException(
265 error
as NodeJS
.ErrnoException
,
266 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
267 { throwError
: false }
272 protected static getSampledValueTemplate(
273 chargingStation
: ChargingStation
,
275 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
276 phase
?: MeterValuePhase
277 ): SampledValueTemplate
| undefined {
278 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
279 if (Constants
.SUPPORTED_MEASURANDS
.includes(measurand
) === false) {
281 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
286 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
287 ChargingStationConfigurationUtils
.getConfigurationKey(
289 StandardParametersKey
.MeterValuesSampledData
290 )?.value
?.includes(measurand
) === false
293 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
294 StandardParametersKey.MeterValuesSampledData
299 const sampledValueTemplates
: SampledValueTemplate
[] =
300 chargingStation
.getConnectorStatus(connectorId
)?.MeterValues
;
303 Utils
.isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
307 Constants
.SUPPORTED_MEASURANDS
.includes(
308 sampledValueTemplates
[index
]?.measurand
??
309 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
313 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
317 sampledValueTemplates
[index
]?.phase
=== phase
&&
318 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
319 ChargingStationConfigurationUtils
.getConfigurationKey(
321 StandardParametersKey
.MeterValuesSampledData
322 )?.value
?.includes(measurand
) === true
324 return sampledValueTemplates
[index
];
327 !sampledValueTemplates
[index
].phase
&&
328 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
329 ChargingStationConfigurationUtils
.getConfigurationKey(
331 StandardParametersKey
.MeterValuesSampledData
332 )?.value
?.includes(measurand
) === true
334 return sampledValueTemplates
[index
];
336 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
337 (!sampledValueTemplates
[index
].measurand
||
338 sampledValueTemplates
[index
].measurand
=== measurand
)
340 return sampledValueTemplates
[index
];
343 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
344 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
345 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
346 throw new BaseError(errorMsg
);
349 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
353 protected static getLimitFromSampledValueTemplateCustomValue(
356 options
: { limitationEnabled
?: boolean; unitMultiplier
?: number } = {
357 limitationEnabled
: true,
361 options
.limitationEnabled
= options
?.limitationEnabled
?? true;
362 options
.unitMultiplier
= options
?.unitMultiplier
?? 1;
363 const parsedInt
= parseInt(value
);
364 const numberValue
= isNaN(parsedInt
) ? Infinity : parsedInt
;
365 return options
?.limitationEnabled
366 ? Math.min(numberValue
* options
.unitMultiplier
, limit
)
367 : numberValue
* options
.unitMultiplier
;
370 private static logPrefix
= (
371 ocppVersion
: OCPPVersion
,
376 Utils
.isNotEmptyString(moduleName
) && Utils
.isNotEmptyString(methodName
)
377 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
378 : ` OCPP ${ocppVersion} |`;
379 return Utils
.logPrefix(logMsg
);