1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import type { JSONSchemaType
} from
'ajv'
7 areIntervalsOverlapping
,
14 import { OCPP16Constants
} from
'./OCPP16Constants.js'
19 } from
'../../../charging-station/index.js'
21 type ConfigurationKey
,
24 OCPP16AuthorizationStatus
,
25 type OCPP16AvailabilityType
,
26 type OCPP16ChangeAvailabilityResponse
,
27 OCPP16ChargePointStatus
,
28 type OCPP16ChargingProfile
,
29 type OCPP16ChargingSchedule
,
30 type OCPP16ClearChargingProfileRequest
,
31 type OCPP16IncomingRequestCommand
,
32 type OCPP16MeterValue
,
33 OCPP16MeterValueContext
,
35 type OCPP16RequestCommand
,
36 OCPP16StandardParametersKey
,
37 OCPP16StopTransactionReason
,
38 type OCPP16SupportedFeatureProfiles
,
40 } from
'../../../types/index.js'
41 import { convertToDate
, isNotEmptyArray
, logger
, roundTo
} from
'../../../utils/index.js'
42 import { OCPPServiceUtils
} from
'../OCPPServiceUtils.js'
44 export class OCPP16ServiceUtils
extends OCPPServiceUtils
{
45 public static checkFeatureProfile (
46 chargingStation
: ChargingStation
,
47 featureProfile
: OCPP16SupportedFeatureProfiles
,
48 command
: OCPP16RequestCommand
| OCPP16IncomingRequestCommand
50 if (hasFeatureProfile(chargingStation
, featureProfile
) === false) {
52 `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
53 OCPP16StandardParametersKey.SupportedFeatureProfiles
61 public static buildTransactionBeginMeterValue (
62 chargingStation
: ChargingStation
,
64 meterStart
: number | undefined
66 const meterValue
: OCPP16MeterValue
= {
67 timestamp
: new Date(),
70 // Energy.Active.Import.Register measurand (default)
71 const sampledValueTemplate
= OCPP16ServiceUtils
.getSampledValueTemplate(
76 sampledValueTemplate
?.unit
=== OCPP16MeterValueUnit
.KILO_WATT_HOUR
? 1000 : 1
77 meterValue
.sampledValue
.push(
78 OCPP16ServiceUtils
.buildSampledValue(
79 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
80 sampledValueTemplate
!,
81 roundTo((meterStart
?? 0) / unitDivider
, 4),
82 OCPP16MeterValueContext
.TRANSACTION_BEGIN
88 public static buildTransactionDataMeterValues (
89 transactionBeginMeterValue
: OCPP16MeterValue
,
90 transactionEndMeterValue
: OCPP16MeterValue
91 ): OCPP16MeterValue
[] {
92 const meterValues
: OCPP16MeterValue
[] = []
93 meterValues
.push(transactionBeginMeterValue
)
94 meterValues
.push(transactionEndMeterValue
)
98 public static remoteStopTransaction
= async (
99 chargingStation
: ChargingStation
,
101 ): Promise
<GenericResponse
> => {
102 await OCPP16ServiceUtils
.sendAndSetConnectorStatus(
105 OCPP16ChargePointStatus
.Finishing
107 const stopResponse
= await chargingStation
.stopTransactionOnConnector(
109 OCPP16StopTransactionReason
.REMOTE
111 if (stopResponse
.idTagInfo
?.status === OCPP16AuthorizationStatus
.ACCEPTED
) {
112 return OCPP16Constants
.OCPP_RESPONSE_ACCEPTED
114 return OCPP16Constants
.OCPP_RESPONSE_REJECTED
117 public static changeAvailability
= async (
118 chargingStation
: ChargingStation
,
119 connectorIds
: number[],
120 chargePointStatus
: OCPP16ChargePointStatus
,
121 availabilityType
: OCPP16AvailabilityType
122 ): Promise
<OCPP16ChangeAvailabilityResponse
> => {
123 const responses
: OCPP16ChangeAvailabilityResponse
[] = []
124 for (const connectorId
of connectorIds
) {
125 let response
: OCPP16ChangeAvailabilityResponse
=
126 OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
127 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
128 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!
129 if (connectorStatus
.transactionStarted
=== true) {
130 response
= OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
132 connectorStatus
.availability
= availabilityType
133 if (response
=== OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
) {
134 await OCPP16ServiceUtils
.sendAndSetConnectorStatus(
140 responses
.push(response
)
142 if (responses
.includes(OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
)) {
143 return OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
145 return OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
148 public static setChargingProfile (
149 chargingStation
: ChargingStation
,
151 cp
: OCPP16ChargingProfile
153 if (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
== null) {
155 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`
157 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
158 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
= []
160 if (!Array.isArray(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
162 `${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`
164 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
165 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
= []
167 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
168 cp
.chargingSchedule
.startSchedule
= convertToDate(cp
.chargingSchedule
.startSchedule
)!
169 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
170 cp
.validFrom
= convertToDate(cp
.validFrom
)!
171 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
172 cp
.validTo
= convertToDate(cp
.validTo
)!
173 let cpReplaced
= false
174 if (isNotEmptyArray(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
175 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
176 for (const [index
, chargingProfile
] of chargingStation
177 .getConnectorStatus(connectorId
)!
178 .chargingProfiles
!.entries()) {
180 chargingProfile
.chargingProfileId
=== cp
.chargingProfileId
||
181 (chargingProfile
.stackLevel
=== cp
.stackLevel
&&
182 chargingProfile
.chargingProfilePurpose
=== cp
.chargingProfilePurpose
)
184 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
185 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
![index
] = cp
190 !cpReplaced
&& chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?.push(cp
)
193 public static clearChargingProfiles
= (
194 chargingStation
: ChargingStation
,
195 commandPayload
: OCPP16ClearChargingProfileRequest
,
196 chargingProfiles
: OCPP16ChargingProfile
[] | undefined
198 const { id
, chargingProfilePurpose
, stackLevel
} = commandPayload
199 let clearedCP
= false
200 if (isNotEmptyArray(chargingProfiles
)) {
201 chargingProfiles
.forEach((chargingProfile
: OCPP16ChargingProfile
, index
: number) => {
202 let clearCurrentCP
= false
203 if (chargingProfile
.chargingProfileId
=== id
) {
204 clearCurrentCP
= true
206 if (chargingProfilePurpose
== null && chargingProfile
.stackLevel
=== stackLevel
) {
207 clearCurrentCP
= true
210 stackLevel
== null &&
211 chargingProfile
.chargingProfilePurpose
=== chargingProfilePurpose
213 clearCurrentCP
= true
216 chargingProfile
.stackLevel
=== stackLevel
&&
217 chargingProfile
.chargingProfilePurpose
=== chargingProfilePurpose
219 clearCurrentCP
= true
221 if (clearCurrentCP
) {
222 chargingProfiles
.splice(index
, 1)
224 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
234 public static composeChargingSchedules
= (
235 chargingScheduleHigher
: OCPP16ChargingSchedule
| undefined,
236 chargingScheduleLower
: OCPP16ChargingSchedule
| undefined,
237 compositeInterval
: Interval
238 ): OCPP16ChargingSchedule
| undefined => {
239 if (chargingScheduleHigher
== null && chargingScheduleLower
== null) {
242 if (chargingScheduleHigher
!= null && chargingScheduleLower
== null) {
243 return OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleHigher
, compositeInterval
)
245 if (chargingScheduleHigher
== null && chargingScheduleLower
!= null) {
246 return OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleLower
, compositeInterval
)
248 const compositeChargingScheduleHigher
: OCPP16ChargingSchedule
| undefined =
249 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
250 OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleHigher
!, compositeInterval
)
251 const compositeChargingScheduleLower
: OCPP16ChargingSchedule
| undefined =
252 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
253 OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleLower
!, compositeInterval
)
254 const compositeChargingScheduleHigherInterval
: Interval
= {
255 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
256 start
: compositeChargingScheduleHigher
!.startSchedule
!,
258 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
259 compositeChargingScheduleHigher
!.startSchedule
!,
260 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
261 compositeChargingScheduleHigher
!.duration
!
264 const compositeChargingScheduleLowerInterval
: Interval
= {
265 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
266 start
: compositeChargingScheduleLower
!.startSchedule
!,
268 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
269 compositeChargingScheduleLower
!.startSchedule
!,
270 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
271 compositeChargingScheduleLower
!.duration
!
274 const higherFirst
= isBefore(
275 compositeChargingScheduleHigherInterval
.start
,
276 compositeChargingScheduleLowerInterval
.start
279 !areIntervalsOverlapping(
280 compositeChargingScheduleHigherInterval
,
281 compositeChargingScheduleLowerInterval
285 ...compositeChargingScheduleLower
,
286 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
287 ...compositeChargingScheduleHigher
!,
288 startSchedule
: higherFirst
289 ? (compositeChargingScheduleHigherInterval
.start
as Date)
290 : (compositeChargingScheduleLowerInterval
.start
as Date),
291 duration
: higherFirst
292 ? differenceInSeconds(
293 compositeChargingScheduleLowerInterval
.end
,
294 compositeChargingScheduleHigherInterval
.start
296 : differenceInSeconds(
297 compositeChargingScheduleHigherInterval
.end
,
298 compositeChargingScheduleLowerInterval
.start
300 chargingSchedulePeriod
: [
301 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
302 ...compositeChargingScheduleHigher
!.chargingSchedulePeriod
.map(schedulePeriod
=> {
305 startPeriod
: higherFirst
307 : schedulePeriod
.startPeriod
+
309 compositeChargingScheduleHigherInterval
.start
,
310 compositeChargingScheduleLowerInterval
.start
314 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
315 ...compositeChargingScheduleLower
!.chargingSchedulePeriod
.map(schedulePeriod
=> {
318 startPeriod
: higherFirst
319 ? schedulePeriod
.startPeriod
+
321 compositeChargingScheduleLowerInterval
.start
,
322 compositeChargingScheduleHigherInterval
.start
327 ].sort((a
, b
) => a
.startPeriod
- b
.startPeriod
)
331 ...compositeChargingScheduleLower
,
332 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
333 ...compositeChargingScheduleHigher
!,
334 startSchedule
: higherFirst
335 ? (compositeChargingScheduleHigherInterval
.start
as Date)
336 : (compositeChargingScheduleLowerInterval
.start
as Date),
337 duration
: higherFirst
338 ? differenceInSeconds(
339 compositeChargingScheduleLowerInterval
.end
,
340 compositeChargingScheduleHigherInterval
.start
342 : differenceInSeconds(
343 compositeChargingScheduleHigherInterval
.end
,
344 compositeChargingScheduleLowerInterval
.start
346 chargingSchedulePeriod
: [
347 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
348 ...compositeChargingScheduleHigher
!.chargingSchedulePeriod
.map(schedulePeriod
=> {
351 startPeriod
: higherFirst
353 : schedulePeriod
.startPeriod
+
355 compositeChargingScheduleHigherInterval
.start
,
356 compositeChargingScheduleLowerInterval
.start
360 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
361 ...compositeChargingScheduleLower
!.chargingSchedulePeriod
362 .filter((schedulePeriod
, index
) => {
367 compositeChargingScheduleLowerInterval
.start
,
368 schedulePeriod
.startPeriod
371 start
: compositeChargingScheduleLowerInterval
.start
,
372 end
: compositeChargingScheduleHigherInterval
.end
380 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
381 index
< compositeChargingScheduleLower
!.chargingSchedulePeriod
.length
- 1 &&
384 compositeChargingScheduleLowerInterval
.start
,
385 schedulePeriod
.startPeriod
388 start
: compositeChargingScheduleLowerInterval
.start
,
389 end
: compositeChargingScheduleHigherInterval
.end
394 compositeChargingScheduleLowerInterval
.start
,
395 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
396 compositeChargingScheduleLower
!.chargingSchedulePeriod
[index
+ 1].startPeriod
399 start
: compositeChargingScheduleLowerInterval
.start
,
400 end
: compositeChargingScheduleHigherInterval
.end
410 compositeChargingScheduleLowerInterval
.start
,
411 schedulePeriod
.startPeriod
414 start
: compositeChargingScheduleHigherInterval
.start
,
415 end
: compositeChargingScheduleLowerInterval
.end
423 .map((schedulePeriod
, index
) => {
424 if (index
=== 0 && schedulePeriod
.startPeriod
!== 0) {
425 schedulePeriod
.startPeriod
= 0
429 startPeriod
: higherFirst
430 ? schedulePeriod
.startPeriod
+
432 compositeChargingScheduleLowerInterval
.start
,
433 compositeChargingScheduleHigherInterval
.start
438 ].sort((a
, b
) => a
.startPeriod
- b
.startPeriod
)
442 public static isConfigurationKeyVisible (key
: ConfigurationKey
): boolean {
443 if (key
.visible
== null) {
449 public static hasReservation
= (
450 chargingStation
: ChargingStation
,
454 const connectorReservation
= chargingStation
.getReservationBy('connectorId', connectorId
)
455 const chargingStationReservation
= chargingStation
.getReservationBy('connectorId', 0)
457 (chargingStation
.getConnectorStatus(connectorId
)?.status ===
458 OCPP16ChargePointStatus
.Reserved
&&
459 connectorReservation
!= null &&
460 !hasReservationExpired(connectorReservation
) &&
461 connectorReservation
.idTag
=== idTag
) ||
462 (chargingStation
.getConnectorStatus(0)?.status === OCPP16ChargePointStatus
.Reserved
&&
463 chargingStationReservation
!= null &&
464 !hasReservationExpired(chargingStationReservation
) &&
465 chargingStationReservation
.idTag
=== idTag
)
468 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
469 connectorReservation
?? chargingStationReservation
476 public static parseJsonSchemaFile
<T
extends JsonType
>(
477 relativePath
: string,
480 ): JSONSchemaType
<T
> {
481 return super.parseJsonSchemaFile
<T
>(
483 OCPPVersion
.VERSION_16
,
489 private static readonly composeChargingSchedule
= (
490 chargingSchedule
: OCPP16ChargingSchedule
,
491 compositeInterval
: Interval
492 ): OCPP16ChargingSchedule
| undefined => {
493 const chargingScheduleInterval
: Interval
= {
494 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
495 start
: chargingSchedule
.startSchedule
!,
496 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
497 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!)
499 if (areIntervalsOverlapping(chargingScheduleInterval
, compositeInterval
)) {
500 chargingSchedule
.chargingSchedulePeriod
.sort((a
, b
) => a
.startPeriod
- b
.startPeriod
)
501 if (isBefore(chargingScheduleInterval
.start
, compositeInterval
.start
)) {
504 startSchedule
: compositeInterval
.start
as Date,
505 duration
: differenceInSeconds(
506 chargingScheduleInterval
.end
,
507 compositeInterval
.start
as Date
509 chargingSchedulePeriod
: chargingSchedule
.chargingSchedulePeriod
510 .filter((schedulePeriod
, index
) => {
513 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
514 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
)!,
521 index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
523 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
),
528 chargingScheduleInterval
.start
,
529 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
538 .map((schedulePeriod
, index
) => {
539 if (index
=== 0 && schedulePeriod
.startPeriod
!== 0) {
540 schedulePeriod
.startPeriod
= 0
542 return schedulePeriod
546 if (isAfter(chargingScheduleInterval
.end
, compositeInterval
.end
)) {
549 duration
: differenceInSeconds(
550 compositeInterval
.end
as Date,
551 chargingScheduleInterval
.start
553 chargingSchedulePeriod
: chargingSchedule
.chargingSchedulePeriod
.filter(schedulePeriod
=>
555 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
556 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
)!,
562 return chargingSchedule