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';
6 import { isDate
} from
'date-fns';
8 import { OCPP16Constants
} from
'./1.6/OCPP16Constants';
9 import { OCPP20Constants
} from
'./2.0/OCPP20Constants';
10 import { OCPPConstants
} from
'./OCPPConstants';
11 import { type ChargingStation
, getConfigurationKey
} from
'../../charging-station';
12 import { BaseError
} from
'../../exception';
15 type ConnectorStatusEnum
,
18 IncomingRequestCommand
,
24 type OCPP16StatusNotificationRequest
,
25 type OCPP20StatusNotificationRequest
,
28 type SampledValueTemplate
,
29 StandardParametersKey
,
30 type StatusNotificationRequest
,
31 type StatusNotificationResponse
,
41 export class OCPPServiceUtils
{
42 protected constructor() {
43 // This is intentional
46 public static ajvErrorsToErrorType(errors
: ErrorObject
[]): ErrorType
{
47 for (const error
of errors
as DefinedError
[]) {
48 switch (error
.keyword
) {
50 return ErrorType
.TYPE_CONSTRAINT_VIOLATION
;
53 return ErrorType
.OCCURRENCE_CONSTRAINT_VIOLATION
;
56 return ErrorType
.PROPERTY_CONSTRAINT_VIOLATION
;
59 return ErrorType
.FORMAT_VIOLATION
;
62 public static getMessageTypeString(messageType
: MessageType
): string {
63 switch (messageType
) {
64 case MessageType
.CALL_MESSAGE
:
66 case MessageType
.CALL_RESULT_MESSAGE
:
68 case MessageType
.CALL_ERROR_MESSAGE
:
75 public static isRequestCommandSupported(
76 chargingStation
: ChargingStation
,
77 command
: RequestCommand
,
79 const isRequestCommand
= Object.values
<RequestCommand
>(RequestCommand
).includes(command
);
81 isRequestCommand
=== true &&
82 !chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
86 isRequestCommand
=== true &&
87 chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
89 return chargingStation
.stationInfo
?.commandsSupport
?.outgoingCommands
[command
] ?? false;
91 logger
.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
95 public static isIncomingRequestCommandSupported(
96 chargingStation
: ChargingStation
,
97 command
: IncomingRequestCommand
,
99 const isIncomingRequestCommand
=
100 Object.values
<IncomingRequestCommand
>(IncomingRequestCommand
).includes(command
);
102 isIncomingRequestCommand
=== true &&
103 !chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
107 isIncomingRequestCommand
=== true &&
108 chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
110 return chargingStation
.stationInfo
?.commandsSupport
?.incomingCommands
[command
] ?? false;
112 logger
.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
116 public static isMessageTriggerSupported(
117 chargingStation
: ChargingStation
,
118 messageTrigger
: MessageTrigger
,
120 const isMessageTrigger
= Object.values(MessageTrigger
).includes(messageTrigger
);
121 if (isMessageTrigger
=== true && !chargingStation
.stationInfo
?.messageTriggerSupport
) {
123 } else if (isMessageTrigger
=== true && chargingStation
.stationInfo
?.messageTriggerSupport
) {
124 return chargingStation
.stationInfo
?.messageTriggerSupport
[messageTrigger
] ?? false;
127 `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'`,
132 public static isConnectorIdValid(
133 chargingStation
: ChargingStation
,
134 ocppCommand
: IncomingRequestCommand
,
137 if (connectorId
< 0) {
139 `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}`,
146 public static convertDateToISOString
<T
extends JsonType
>(obj
: T
): void {
147 for (const key
in obj
) {
148 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
149 if (isDate(obj
![key
])) {
150 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
151 (obj
![key
] as string) = (obj
![key
] as Date).toISOString();
152 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
153 } else if (obj
![key
] !== null && typeof obj
![key
] === 'object') {
154 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
155 OCPPServiceUtils
.convertDateToISOString
<T
>(obj
![key
] as T
);
160 public static buildStatusNotificationRequest(
161 chargingStation
: ChargingStation
,
163 status: ConnectorStatusEnum
,
165 ): StatusNotificationRequest
{
166 switch (chargingStation
.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
) {
167 case OCPPVersion
.VERSION_16
:
171 errorCode
: ChargePointErrorCode
.NO_ERROR
,
172 } as OCPP16StatusNotificationRequest
;
173 case OCPPVersion
.VERSION_20
:
174 case OCPPVersion
.VERSION_201
:
176 timestamp
: new Date(),
177 connectorStatus
: status,
180 } as OCPP20StatusNotificationRequest
;
182 throw new BaseError('Cannot build status notification payload: OCPP version not supported');
186 public static startHeartbeatInterval(chargingStation
: ChargingStation
, interval
: number): void {
187 if (!chargingStation
.heartbeatSetInterval
) {
188 chargingStation
.startHeartbeat();
189 } else if (chargingStation
.getHeartbeatInterval() !== interval
) {
190 chargingStation
.restartHeartbeat();
194 public static async sendAndSetConnectorStatus(
195 chargingStation
: ChargingStation
,
197 status: ConnectorStatusEnum
,
199 options
?: { send
: boolean },
201 options
= { send
: true, ...options
};
203 OCPPServiceUtils
.checkConnectorStatusTransition(chargingStation
, connectorId
, status);
204 await chargingStation
.ocppRequestService
.requestHandler
<
205 StatusNotificationRequest
,
206 StatusNotificationResponse
209 RequestCommand
.STATUS_NOTIFICATION
,
210 OCPPServiceUtils
.buildStatusNotificationRequest(
218 chargingStation
.getConnectorStatus(connectorId
)!.status = status;
221 protected static checkConnectorStatusTransition(
222 chargingStation
: ChargingStation
,
224 status: ConnectorStatusEnum
,
226 const fromStatus
= chargingStation
.getConnectorStatus(connectorId
)!.status;
227 let transitionAllowed
= false;
228 switch (chargingStation
.stationInfo
.ocppVersion
) {
229 case OCPPVersion
.VERSION_16
:
231 (connectorId
=== 0 &&
232 OCPP16Constants
.ChargePointStatusChargingStationTransitions
.findIndex(
233 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
236 OCPP16Constants
.ChargePointStatusConnectorTransitions
.findIndex(
237 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
240 transitionAllowed
= true;
243 case OCPPVersion
.VERSION_20
:
244 case OCPPVersion
.VERSION_201
:
246 (connectorId
=== 0 &&
247 OCPP20Constants
.ChargingStationStatusTransitions
.findIndex(
248 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
251 OCPP20Constants
.ConnectorStatusTransitions
.findIndex(
252 (transition
) => transition
.from
=== fromStatus
&& transition
.to
=== status,
255 transitionAllowed
= true;
260 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
261 `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo.ocppVersion} not supported`,
264 if (transitionAllowed
=== false) {
266 `${chargingStation.logPrefix()} OCPP ${
267 chargingStation.stationInfo.ocppVersion
268 } connector id ${connectorId} status transition from '${
269 chargingStation.getConnectorStatus(connectorId)!.status
270 }' to '${status}' is not allowed`,
273 return transitionAllowed
;
276 protected static parseJsonSchemaFile
<T
extends JsonType
>(
277 relativePath
: string,
278 ocppVersion
: OCPPVersion
,
281 ): JSONSchemaType
<T
> {
282 const filePath
= join(dirname(fileURLToPath(import.meta
.url
)), relativePath
);
284 return JSON
.parse(readFileSync(filePath
, 'utf8')) as JSONSchemaType
<T
>;
289 error
as NodeJS
.ErrnoException
,
290 OCPPServiceUtils
.logPrefix(ocppVersion
, moduleName
, methodName
),
291 { throwError
: false },
293 return {} as JSONSchemaType
<T
>;
297 protected static getSampledValueTemplate(
298 chargingStation
: ChargingStation
,
300 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
301 phase
?: MeterValuePhase
,
302 ): SampledValueTemplate
| undefined {
303 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
304 if (OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(measurand
) === false) {
306 `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
311 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
314 StandardParametersKey
.MeterValuesSampledData
,
315 )?.value
?.includes(measurand
) === false
318 `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${
319 StandardParametersKey.MeterValuesSampledData
324 const sampledValueTemplates
: SampledValueTemplate
[] =
325 chargingStation
.getConnectorStatus(connectorId
)!.MeterValues
;
328 isNotEmptyArray(sampledValueTemplates
) === true && index
< sampledValueTemplates
.length
;
332 OCPPConstants
.OCPP_MEASURANDS_SUPPORTED
.includes(
333 sampledValueTemplates
[index
]?.measurand
??
334 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
338 `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
342 sampledValueTemplates
[index
]?.phase
=== phase
&&
343 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
346 StandardParametersKey
.MeterValuesSampledData
,
347 )?.value
?.includes(measurand
) === true
349 return sampledValueTemplates
[index
];
352 !sampledValueTemplates
[index
].phase
&&
353 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
356 StandardParametersKey
.MeterValuesSampledData
,
357 )?.value
?.includes(measurand
) === true
359 return sampledValueTemplates
[index
];
361 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
362 (!sampledValueTemplates
[index
].measurand
||
363 sampledValueTemplates
[index
].measurand
=== measurand
)
365 return sampledValueTemplates
[index
];
368 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
369 const errorMsg
= `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}`;
370 logger
.error(`${chargingStation.logPrefix()} ${errorMsg}`);
371 throw new BaseError(errorMsg
);
374 `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}`,
378 protected static getLimitFromSampledValueTemplateCustomValue(
381 options
: { limitationEnabled
?: boolean; unitMultiplier
?: number } = {
382 limitationEnabled
: true,
388 limitationEnabled
: true,
393 const parsedInt
= parseInt(value
);
394 const numberValue
= isNaN(parsedInt
) ? Infinity : parsedInt
;
395 return options
?.limitationEnabled
396 ? Math.min(numberValue
* options
.unitMultiplier
!, limit
)
397 : numberValue
* options
.unitMultiplier
!;
400 private static logPrefix
= (
401 ocppVersion
: OCPPVersion
,
406 isNotEmptyString(moduleName
) && isNotEmptyString(methodName
)
407 ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
408 : ` OCPP ${ocppVersion} |`;
409 return logPrefix(logMsg
);