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 { OCPPConstants
} from
'./OCPPConstants';
10 import { type ChargingStation
, ChargingStationConfigurationUtils
} from
'../../charging-station';
11 import { BaseError
} from
'../../exception';
14 type ConnectorStatusEnum
,
17 IncomingRequestCommand
,
24 type OCPP16StatusNotificationRequest
,
25 type OCPP20StatusNotificationRequest
,
28 type SampledValueTemplate
,
29 StandardParametersKey
,
30 type StatusNotificationRequest
,
31 type StatusNotificationResponse
,
33 import { Utils
, handleFileException
, logger
} from
'../../utils';
35 export class OCPPServiceUtils
{
36 protected constructor() {
37 // This is intentional
40 public static ajvErrorsToErrorType(errors
: ErrorObject
[]): ErrorType
{
41 for (const error
of errors
as DefinedError
[]) {
42 switch (error
.keyword
) {
44 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
47 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
50 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
53 return ErrorType
.FORMAT_VIOLATION
;
56 public static getMessageTypeString(messageType
: MessageType
): string {
57 switch (messageType
) {
58 case MessageType
.CALL_MESSAGE
:
60 case MessageType
.CALL_RESULT_MESSAGE
:
62 case MessageType
.CALL_ERROR_MESSAGE
:
69 public static isRequestCommandSupported(
70 chargingStation
: ChargingStation
,
71 command
: RequestCommand
73 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
75 isRequestCommand
=== true &&
76 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
80 isRequestCommand
=== true &&
81 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
83 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
85 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
89 public static isIncomingRequestCommandSupported(
90 chargingStation
: ChargingStation
,
91 command
: IncomingRequestCommand
93 const isIncomingRequestCommand
=
94 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
96 isIncomingRequestCommand
=== true &&
97 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
101 isIncomingRequestCommand
=== true &&
102 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
104 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
106 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
110 public static isMessageTriggerSupported(
111 chargingStation
: ChargingStation
,
112 messageTrigger
: MessageTrigger
114 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
115 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
117 } else if (isMessageTrigger
=== true && chargingStation
.stationInfo
?.messageTriggerSupport
) {
118 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
] ?? false;
121 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`
126 public static isConnectorIdValid(
127 chargingStation
: ChargingStation
,
128 ocppCommand
: IncomingRequestCommand
,
131 if (connectorId
< 0) {
133 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`
140 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
141 for (const key
in obj
) {
142 if (obj
[key
] instanceof Date) {
143 (obj
as JsonObject
)[key
] = (obj
[key
] as Date).toISOString();
144 } else if (obj
[key
] !== null && typeof obj
[key
] === 'object') {
145 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
[key
] as T
);
150 public static buildStatusNotificationRequest(
151 chargingStation
: ChargingStation
,
153 status: ConnectorStatusEnum
,
155 ): StatusNotificationRequest
{
156 switch (chargingStation
.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
) {
157 case OCPPVersion
.VERSION_16
:
161 errorCode
: ChargePointErrorCode
.NO_ERROR
,
162 } as OCPP16StatusNotificationRequest
;
163 case OCPPVersion
.VERSION_20
:
164 case OCPPVersion
.VERSION_201
:
166 timestamp
: new Date(),
167 connectorStatus
: status,
170 } as OCPP20StatusNotificationRequest
;
172 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
176 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
177 if (!chargingStation
.heartbeatSetInterval
) {
178 chargingStation
.startHeartbeat();
179 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
180 chargingStation
.restartHeartbeat();
184 public static async sendAndSetConnectorStatus(
185 chargingStation
: ChargingStation
,
187 status: ConnectorStatusEnum
,
189 options
: { send
: boolean } = { send
: true }
191 options
= { send
: true, ...options
};
193 OCPPServiceUtils
.checkConnectorStatusTransition(chargingStation
, connectorId
, status);
194 await chargingStation
.ocppRequestService
.requestHandler
<
195 StatusNotificationRequest
,
196 StatusNotificationResponse
199 RequestCommand
.STATUS_NOTIFICATION
,
200 OCPPServiceUtils
.buildStatusNotificationRequest(
208 chargingStation
.getConnectorStatus(connectorId
).status = status;
211 protected static checkConnectorStatusTransition(
212 chargingStation
: ChargingStation
,
214 status: ConnectorStatusEnum
216 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
).status;
217 let transitionAllowed
= false;
218 switch (chargingStation
.stationInfo
.ocppVersion
) {
219 case OCPPVersion
.VERSION_16
:
221 (connectorId
=== 0 &&
222 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
223 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
226 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
227 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
230 transitionAllowed
= true;
233 case OCPPVersion
.VERSION_20
:
234 case OCPPVersion
.VERSION_201
:
236 (connectorId
=== 0 &&
237 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
238 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
241 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
242 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status
245 transitionAllowed
= true;
250 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
251 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`
254 if (transitionAllowed
=== false) {
256 `${chargingStation.logPrefix()} OCPP ${
257 chargingStation.stationInfo.ocppVersion
258 } connector id ${connectorId} status transition from '${
259 chargingStation.getConnectorStatus(connectorId).status
260 }' to '${status}' is not allowed`
263 return transitionAllowed
;
266 protected static parseJsonSchemaFile
<T
extends JsonType
>(
267 relativePath
: string,
268 ocppVersion
: OCPPVersion
,
271 ): JSONSchemaType
<T
> {
272 const filePath
= path
.join(path
.dirname(fileURLToPath(import.meta
.url
)), relativePath
);
274 return JSON
.parse(fs
.readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
279 error
as NodeJS
.ErrnoException
,
280 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
281 { throwError
: false }
286 protected static getSampledValueTemplate(
287 chargingStation
: ChargingStation
,
289 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
290 phase
?: MeterValuePhase
291 ): SampledValueTemplate
| undefined {
292 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
293 if (OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(measurand
) === false) {
295 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
300 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
301 ChargingStationConfigurationUtils
.getConfigurationKey(
303 StandardParametersKey
.MeterValuesSampledData
304 )?.value
?.includes(measurand
) === false
307 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
308 StandardParametersKey.MeterValuesSampledData
313 const sampledValueTemplates
: SampledValueTemplate
[] =
314 chargingStation
.getConnectorStatus(connectorId
)?.MeterValues
;
317 Utils
.isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
321 OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(
322 sampledValueTemplates
[index
]?.measurand
??
323 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
327 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
331 sampledValueTemplates
[index
]?.phase
=== phase
&&
332 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
333 ChargingStationConfigurationUtils
.getConfigurationKey(
335 StandardParametersKey
.MeterValuesSampledData
336 )?.value
?.includes(measurand
) === true
338 return sampledValueTemplates
[index
];
341 !sampledValueTemplates
[index
].phase
&&
342 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
343 ChargingStationConfigurationUtils
.getConfigurationKey(
345 StandardParametersKey
.MeterValuesSampledData
346 )?.value
?.includes(measurand
) === true
348 return sampledValueTemplates
[index
];
350 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
351 (!sampledValueTemplates
[index
].measurand
||
352 sampledValueTemplates
[index
].measurand
=== measurand
)
354 return sampledValueTemplates
[index
];
357 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
358 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
359 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
360 throw new BaseError(errorMsg
);
363 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`
367 protected static getLimitFromSampledValueTemplateCustomValue(
370 options
: { limitationEnabled
?: boolean; unitMultiplier
?: number } = {
371 limitationEnabled
: true,
377 limitationEnabled
: true,
382 const parsedInt
= parseInt(value
);
383 const numberValue
= isNaN(parsedInt
) ? Infinity : parsedInt
;
384 return options
?.limitationEnabled
385 ? Math.min(numberValue
* options
.unitMultiplier
, limit
)
386 : numberValue
* options
.unitMultiplier
;
389 private static logPrefix
= (
390 ocppVersion
: OCPPVersion
,
395 Utils
.isNotEmptyString(moduleName
) && Utils
.isNotEmptyString(methodName
)
396 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
397 : ` OCPP ${ocppVersion} |`;
398 return Utils
.logPrefix(logMsg
);