1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import type { JSONSchemaType
} from
'ajv';
7 areIntervalsOverlapping
,
14 import { OCPP16Constants
} from
'./OCPP16Constants.js';
18 hasReservationExpired
,
19 } from
'../../../charging-station/index.js';
23 OCPP16AuthorizationStatus
,
24 OCPP16AvailabilityType
,
25 type OCPP16ChangeAvailabilityResponse
,
26 OCPP16ChargePointStatus
,
27 type OCPP16ChargingProfile
,
28 type OCPP16ChargingSchedule
,
29 type OCPP16ClearChargingProfileRequest
,
30 type OCPP16IncomingRequestCommand
,
31 type OCPP16MeterValue
,
32 OCPP16MeterValueContext
,
35 OCPP16StandardParametersKey
,
36 OCPP16StopTransactionReason
,
37 type OCPP16SupportedFeatureProfiles
,
39 } from
'../../../types/index.js';
40 import { isNotEmptyArray
, isNullOrUndefined
, logger
, roundTo
} from
'../../../utils/index.js';
41 import { OCPPServiceUtils
} from
'../OCPPServiceUtils.js';
43 export class OCPP16ServiceUtils
extends OCPPServiceUtils
{
44 public static checkFeatureProfile(
45 chargingStation
: ChargingStation
,
46 featureProfile
: OCPP16SupportedFeatureProfiles
,
47 command
: OCPP16RequestCommand
| OCPP16IncomingRequestCommand
,
49 if (!hasFeatureProfile(chargingStation
, featureProfile
)) {
51 `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
52 OCPP16StandardParametersKey.SupportedFeatureProfiles
60 public static buildTransactionBeginMeterValue(
61 chargingStation
: ChargingStation
,
65 const meterValue
: OCPP16MeterValue
= {
66 timestamp
: new Date(),
69 // Energy.Active.Import.Register measurand (default)
70 const sampledValueTemplate
= OCPP16ServiceUtils
.getSampledValueTemplate(
75 sampledValueTemplate
?.unit
=== OCPP16MeterValueUnit
.KILO_WATT_HOUR
? 1000 : 1;
76 meterValue
.sampledValue
.push(
77 OCPP16ServiceUtils
.buildSampledValue(
78 sampledValueTemplate
!,
79 roundTo((meterStart
?? 0) / unitDivider
, 4),
80 OCPP16MeterValueContext
.TRANSACTION_BEGIN
,
86 public static buildTransactionDataMeterValues(
87 transactionBeginMeterValue
: OCPP16MeterValue
,
88 transactionEndMeterValue
: OCPP16MeterValue
,
89 ): OCPP16MeterValue
[] {
90 const meterValues
: OCPP16MeterValue
[] = [];
91 meterValues
.push(transactionBeginMeterValue
);
92 meterValues
.push(transactionEndMeterValue
);
96 public static remoteStopTransaction
= async (
97 chargingStation
: ChargingStation
,
99 ): Promise
<GenericResponse
> => {
100 await OCPP16ServiceUtils
.sendAndSetConnectorStatus(
103 OCPP16ChargePointStatus
.Finishing
,
105 const stopResponse
= await chargingStation
.stopTransactionOnConnector(
107 OCPP16StopTransactionReason
.REMOTE
,
109 if (stopResponse
.idTagInfo
?.status === OCPP16AuthorizationStatus
.ACCEPTED
) {
110 return OCPP16Constants
.OCPP_RESPONSE_ACCEPTED
;
112 return OCPP16Constants
.OCPP_RESPONSE_REJECTED
;
115 public static changeAvailability
= async (
116 chargingStation
: ChargingStation
,
117 connectorIds
: number[],
118 chargePointStatus
: OCPP16ChargePointStatus
,
119 availabilityType
: OCPP16AvailabilityType
,
120 ): Promise
<OCPP16ChangeAvailabilityResponse
> => {
121 const responses
: OCPP16ChangeAvailabilityResponse
[] = [];
122 for (const connectorId
of connectorIds
) {
123 let response
: OCPP16ChangeAvailabilityResponse
=
124 OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
;
125 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!;
126 if (connectorStatus
?.transactionStarted
=== true) {
127 response
= OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
;
129 connectorStatus
.availability
= availabilityType
;
130 if (response
=== OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
) {
131 await OCPP16ServiceUtils
.sendAndSetConnectorStatus(
137 responses
.push(response
);
139 if (responses
.includes(OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
)) {
140 return OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
;
142 return OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
;
145 public static setChargingProfile(
146 chargingStation
: ChargingStation
,
148 cp
: OCPP16ChargingProfile
,
150 if (isNullOrUndefined(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
152 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`,
154 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
= [];
157 Array.isArray(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
) === false
160 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type deferred initialization`,
162 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
= [];
164 let cpReplaced
= false;
165 if (isNotEmptyArray(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
167 .getConnectorStatus(connectorId
)
168 ?.chargingProfiles
?.forEach((chargingProfile
: OCPP16ChargingProfile
, index
: number) => {
170 chargingProfile
.chargingProfileId
=== cp
.chargingProfileId
||
171 (chargingProfile
.stackLevel
=== cp
.stackLevel
&&
172 chargingProfile
.chargingProfilePurpose
=== cp
.chargingProfilePurpose
)
174 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
![index
] = cp
;
179 !cpReplaced
&& chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?.push(cp
);
182 public static clearChargingProfiles
= (
183 chargingStation
: ChargingStation
,
184 commandPayload
: OCPP16ClearChargingProfileRequest
,
185 chargingProfiles
: OCPP16ChargingProfile
[] | undefined,
187 const { id
, chargingProfilePurpose
, stackLevel
} = commandPayload
;
188 let clearedCP
= false;
189 if (isNotEmptyArray(chargingProfiles
)) {
190 chargingProfiles
?.forEach((chargingProfile
: OCPP16ChargingProfile
, index
: number) => {
191 let clearCurrentCP
= false;
192 if (chargingProfile
.chargingProfileId
=== id
) {
193 clearCurrentCP
= true;
195 if (!chargingProfilePurpose
&& chargingProfile
.stackLevel
=== stackLevel
) {
196 clearCurrentCP
= true;
198 if (!stackLevel
&& chargingProfile
.chargingProfilePurpose
=== chargingProfilePurpose
) {
199 clearCurrentCP
= true;
202 chargingProfile
.stackLevel
=== stackLevel
&&
203 chargingProfile
.chargingProfilePurpose
=== chargingProfilePurpose
205 clearCurrentCP
= true;
207 if (clearCurrentCP
) {
208 chargingProfiles
.splice(index
, 1);
210 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
220 public static composeChargingSchedules
= (
221 chargingScheduleHigher
: OCPP16ChargingSchedule
| undefined,
222 chargingScheduleLower
: OCPP16ChargingSchedule
| undefined,
223 compositeInterval
: Interval
,
224 ): OCPP16ChargingSchedule
| undefined => {
225 if (!chargingScheduleHigher
&& !chargingScheduleLower
) {
228 if (chargingScheduleHigher
&& !chargingScheduleLower
) {
229 return OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleHigher
, compositeInterval
);
231 if (!chargingScheduleHigher
&& chargingScheduleLower
) {
232 return OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleLower
, compositeInterval
);
234 const compositeChargingScheduleHigher
: OCPP16ChargingSchedule
| undefined =
235 OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleHigher
!, compositeInterval
);
236 const compositeChargingScheduleLower
: OCPP16ChargingSchedule
| undefined =
237 OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleLower
!, compositeInterval
);
238 const compositeChargingScheduleHigherInterval
: Interval
= {
239 start
: compositeChargingScheduleHigher
!.startSchedule
!,
241 compositeChargingScheduleHigher
!.startSchedule
!,
242 compositeChargingScheduleHigher
!.duration
!,
245 const compositeChargingScheduleLowerInterval
: Interval
= {
246 start
: compositeChargingScheduleLower
!.startSchedule
!,
248 compositeChargingScheduleLower
!.startSchedule
!,
249 compositeChargingScheduleLower
!.duration
!,
252 const higherFirst
= isBefore(
253 compositeChargingScheduleHigherInterval
.start
,
254 compositeChargingScheduleLowerInterval
.start
,
257 !areIntervalsOverlapping(
258 compositeChargingScheduleHigherInterval
,
259 compositeChargingScheduleLowerInterval
,
263 ...compositeChargingScheduleLower
,
264 ...compositeChargingScheduleHigher
!,
265 startSchedule
: higherFirst
266 ? (compositeChargingScheduleHigherInterval
.start
as Date)
267 : (compositeChargingScheduleLowerInterval
.start
as Date),
268 duration
: higherFirst
269 ? differenceInSeconds(
270 compositeChargingScheduleLowerInterval
.end
,
271 compositeChargingScheduleHigherInterval
.start
,
273 : differenceInSeconds(
274 compositeChargingScheduleHigherInterval
.end
,
275 compositeChargingScheduleLowerInterval
.start
,
277 chargingSchedulePeriod
: [
278 ...compositeChargingScheduleHigher
!.chargingSchedulePeriod
.map((schedulePeriod
) => {
281 startPeriod
: higherFirst
283 : schedulePeriod
.startPeriod
+
285 compositeChargingScheduleHigherInterval
.start
,
286 compositeChargingScheduleLowerInterval
.start
,
290 ...compositeChargingScheduleLower
!.chargingSchedulePeriod
.map((schedulePeriod
) => {
293 startPeriod
: higherFirst
294 ? schedulePeriod
.startPeriod
+
296 compositeChargingScheduleLowerInterval
.start
,
297 compositeChargingScheduleHigherInterval
.start
,
302 ].sort((a
, b
) => a
.startPeriod
- b
.startPeriod
),
306 ...compositeChargingScheduleLower
,
307 ...compositeChargingScheduleHigher
!,
308 startSchedule
: higherFirst
309 ? (compositeChargingScheduleHigherInterval
.start
as Date)
310 : (compositeChargingScheduleLowerInterval
.start
as Date),
311 duration
: higherFirst
312 ? differenceInSeconds(
313 compositeChargingScheduleLowerInterval
.end
,
314 compositeChargingScheduleHigherInterval
.start
,
316 : differenceInSeconds(
317 compositeChargingScheduleHigherInterval
.end
,
318 compositeChargingScheduleLowerInterval
.start
,
320 chargingSchedulePeriod
: [
321 ...compositeChargingScheduleHigher
!.chargingSchedulePeriod
.map((schedulePeriod
) => {
324 startPeriod
: higherFirst
326 : schedulePeriod
.startPeriod
+
328 compositeChargingScheduleHigherInterval
.start
,
329 compositeChargingScheduleLowerInterval
.start
,
333 ...compositeChargingScheduleLower
!.chargingSchedulePeriod
334 .filter((schedulePeriod
, index
) => {
339 compositeChargingScheduleLowerInterval
.start
,
340 schedulePeriod
.startPeriod
,
343 start
: compositeChargingScheduleLowerInterval
.start
,
344 end
: compositeChargingScheduleHigherInterval
.end
,
352 index
< compositeChargingScheduleLower
!.chargingSchedulePeriod
.length
- 1 &&
355 compositeChargingScheduleLowerInterval
.start
,
356 schedulePeriod
.startPeriod
,
359 start
: compositeChargingScheduleLowerInterval
.start
,
360 end
: compositeChargingScheduleHigherInterval
.end
,
365 compositeChargingScheduleLowerInterval
.start
,
366 compositeChargingScheduleLower
!.chargingSchedulePeriod
[index
+ 1].startPeriod
,
369 start
: compositeChargingScheduleLowerInterval
.start
,
370 end
: compositeChargingScheduleHigherInterval
.end
,
380 compositeChargingScheduleLowerInterval
.start
,
381 schedulePeriod
.startPeriod
,
384 start
: compositeChargingScheduleHigherInterval
.start
,
385 end
: compositeChargingScheduleLowerInterval
.end
,
393 .map((schedulePeriod
, index
) => {
394 if (index
=== 0 && schedulePeriod
.startPeriod
!== 0) {
395 schedulePeriod
.startPeriod
= 0;
399 startPeriod
: higherFirst
400 ? schedulePeriod
.startPeriod
+
402 compositeChargingScheduleLowerInterval
.start
,
403 compositeChargingScheduleHigherInterval
.start
,
408 ].sort((a
, b
) => a
.startPeriod
- b
.startPeriod
),
412 public static hasReservation
= (
413 chargingStation
: ChargingStation
,
417 const connectorReservation
= chargingStation
.getReservationBy('connectorId', connectorId
);
418 const chargingStationReservation
= chargingStation
.getReservationBy('connectorId', 0);
420 (chargingStation
.getConnectorStatus(connectorId
)?.status ===
421 OCPP16ChargePointStatus
.Reserved
&&
422 connectorReservation
&&
423 !hasReservationExpired(connectorReservation
) &&
424 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
425 connectorReservation
?.idTag
=== idTag
) ||
426 (chargingStation
.getConnectorStatus(0)?.status === OCPP16ChargePointStatus
.Reserved
&&
427 chargingStationReservation
&&
428 !hasReservationExpired(chargingStationReservation
) &&
429 chargingStationReservation
?.idTag
=== idTag
)
432 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
433 connectorReservation
?? chargingStationReservation
,
440 public static parseJsonSchemaFile
<T
extends JsonType
>(
441 relativePath
: string,
444 ): JSONSchemaType
<T
> {
445 return super.parseJsonSchemaFile
<T
>(
447 OCPPVersion
.VERSION_16
,
453 private static composeChargingSchedule
= (
454 chargingSchedule
: OCPP16ChargingSchedule
,
455 compositeInterval
: Interval
,
456 ): OCPP16ChargingSchedule
| undefined => {
457 const chargingScheduleInterval
: Interval
= {
458 start
: chargingSchedule
.startSchedule
!,
459 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!),
461 if (areIntervalsOverlapping(chargingScheduleInterval
, compositeInterval
)) {
462 chargingSchedule
.chargingSchedulePeriod
.sort((a
, b
) => a
.startPeriod
- b
.startPeriod
);
463 if (isBefore(chargingScheduleInterval
.start
, compositeInterval
.start
)) {
466 startSchedule
: compositeInterval
.start
as Date,
467 duration
: differenceInSeconds(
468 chargingScheduleInterval
.end
,
469 compositeInterval
.start
as Date,
471 chargingSchedulePeriod
: chargingSchedule
.chargingSchedulePeriod
472 .filter((schedulePeriod
, index
) => {
475 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
)!,
482 index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
484 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
),
489 chargingScheduleInterval
.start
,
490 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
,
499 .map((schedulePeriod
, index
) => {
500 if (index
=== 0 && schedulePeriod
.startPeriod
!== 0) {
501 schedulePeriod
.startPeriod
= 0;
503 return schedulePeriod
;
507 if (isAfter(chargingScheduleInterval
.end
, compositeInterval
.end
)) {
510 duration
: differenceInSeconds(
511 compositeInterval
.end
as Date,
512 chargingScheduleInterval
.start
,
514 chargingSchedulePeriod
: chargingSchedule
.chargingSchedulePeriod
.filter((schedulePeriod
) =>
516 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
)!,
522 return chargingSchedule
;