1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import type { JSONSchemaType
} from
'ajv'
7 areIntervalsOverlapping
,
18 hasReservationExpired
,
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'
43 import { OCPP16Constants
} from
'./OCPP16Constants.js'
45 export class OCPP16ServiceUtils
extends OCPPServiceUtils
{
46 public static changeAvailability
= async (
47 chargingStation
: ChargingStation
,
48 connectorIds
: number[],
49 chargePointStatus
: OCPP16ChargePointStatus
,
50 availabilityType
: OCPP16AvailabilityType
51 ): Promise
<OCPP16ChangeAvailabilityResponse
> => {
52 const responses
: OCPP16ChangeAvailabilityResponse
[] = []
53 for (const connectorId
of connectorIds
) {
54 let response
: OCPP16ChangeAvailabilityResponse
=
55 OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
56 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
57 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!
58 if (connectorStatus
.transactionStarted
=== true) {
59 response
= OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
61 connectorStatus
.availability
= availabilityType
62 if (response
=== OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
) {
63 await OCPP16ServiceUtils
.sendAndSetConnectorStatus(
69 responses
.push(response
)
71 if (responses
.includes(OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
)) {
72 return OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
74 return OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
77 public static clearChargingProfiles
= (
78 chargingStation
: ChargingStation
,
79 commandPayload
: OCPP16ClearChargingProfileRequest
,
80 chargingProfiles
: OCPP16ChargingProfile
[] | undefined
82 const { chargingProfilePurpose
, id
, stackLevel
} = commandPayload
84 if (isNotEmptyArray(chargingProfiles
)) {
85 chargingProfiles
.forEach((chargingProfile
: OCPP16ChargingProfile
, index
: number) => {
86 let clearCurrentCP
= false
87 if (chargingProfile
.chargingProfileId
=== id
) {
90 if (chargingProfilePurpose
== null && chargingProfile
.stackLevel
=== stackLevel
) {
95 chargingProfile
.chargingProfilePurpose
=== chargingProfilePurpose
100 chargingProfile
.stackLevel
=== stackLevel
&&
101 chargingProfile
.chargingProfilePurpose
=== chargingProfilePurpose
103 clearCurrentCP
= true
105 if (clearCurrentCP
) {
106 chargingProfiles
.splice(index
, 1)
108 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
118 private static readonly composeChargingSchedule
= (
119 chargingSchedule
: OCPP16ChargingSchedule
,
120 compositeInterval
: Interval
121 ): OCPP16ChargingSchedule
| undefined => {
122 const chargingScheduleInterval
: Interval
= {
123 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
124 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!),
125 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
126 start
: chargingSchedule
.startSchedule
!,
128 if (areIntervalsOverlapping(chargingScheduleInterval
, compositeInterval
)) {
129 chargingSchedule
.chargingSchedulePeriod
.sort((a
, b
) => a
.startPeriod
- b
.startPeriod
)
130 if (isBefore(chargingScheduleInterval
.start
, compositeInterval
.start
)) {
133 chargingSchedulePeriod
: chargingSchedule
.chargingSchedulePeriod
134 .filter((schedulePeriod
, index
) => {
137 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
),
144 index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
146 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
),
151 chargingScheduleInterval
.start
,
152 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
161 .map((schedulePeriod
, index
) => {
162 if (index
=== 0 && schedulePeriod
.startPeriod
!== 0) {
163 schedulePeriod
.startPeriod
= 0
165 return schedulePeriod
167 duration
: differenceInSeconds(
168 chargingScheduleInterval
.end
,
169 compositeInterval
.start
as Date
171 startSchedule
: compositeInterval
.start
as Date,
174 if (isAfter(chargingScheduleInterval
.end
, compositeInterval
.end
)) {
177 chargingSchedulePeriod
: chargingSchedule
.chargingSchedulePeriod
.filter(schedulePeriod
=>
179 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
),
183 duration
: differenceInSeconds(
184 compositeInterval
.end
as Date,
185 chargingScheduleInterval
.start
189 return chargingSchedule
193 public static composeChargingSchedules
= (
194 chargingScheduleHigher
: OCPP16ChargingSchedule
| undefined,
195 chargingScheduleLower
: OCPP16ChargingSchedule
| undefined,
196 compositeInterval
: Interval
197 ): OCPP16ChargingSchedule
| undefined => {
198 if (chargingScheduleHigher
== null && chargingScheduleLower
== null) {
201 if (chargingScheduleHigher
!= null && chargingScheduleLower
== null) {
202 return OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleHigher
, compositeInterval
)
204 if (chargingScheduleHigher
== null && chargingScheduleLower
!= null) {
205 return OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleLower
, compositeInterval
)
207 const compositeChargingScheduleHigher
: OCPP16ChargingSchedule
| undefined =
208 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
209 OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleHigher
!, compositeInterval
)
210 const compositeChargingScheduleLower
: OCPP16ChargingSchedule
| undefined =
211 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
212 OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleLower
!, compositeInterval
)
213 const compositeChargingScheduleHigherInterval
: Interval
= {
215 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
216 compositeChargingScheduleHigher
!.startSchedule
!,
217 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
218 compositeChargingScheduleHigher
!.duration
!
220 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
221 start
: compositeChargingScheduleHigher
!.startSchedule
!,
223 const compositeChargingScheduleLowerInterval
: Interval
= {
225 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
226 compositeChargingScheduleLower
!.startSchedule
!,
227 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
228 compositeChargingScheduleLower
!.duration
!
230 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
231 start
: compositeChargingScheduleLower
!.startSchedule
!,
233 const higherFirst
= isBefore(
234 compositeChargingScheduleHigherInterval
.start
,
235 compositeChargingScheduleLowerInterval
.start
238 !areIntervalsOverlapping(
239 compositeChargingScheduleHigherInterval
,
240 compositeChargingScheduleLowerInterval
244 ...compositeChargingScheduleLower
,
245 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
246 ...compositeChargingScheduleHigher
!,
247 chargingSchedulePeriod
: [
248 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
249 ...compositeChargingScheduleHigher
!.chargingSchedulePeriod
.map(schedulePeriod
=> {
252 startPeriod
: higherFirst
254 : schedulePeriod
.startPeriod
+
256 compositeChargingScheduleHigherInterval
.start
,
257 compositeChargingScheduleLowerInterval
.start
261 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262 ...compositeChargingScheduleLower
!.chargingSchedulePeriod
.map(schedulePeriod
=> {
265 startPeriod
: higherFirst
266 ? schedulePeriod
.startPeriod
+
268 compositeChargingScheduleLowerInterval
.start
,
269 compositeChargingScheduleHigherInterval
.start
274 ].sort((a
, b
) => a
.startPeriod
- b
.startPeriod
),
275 duration
: higherFirst
276 ? differenceInSeconds(
277 compositeChargingScheduleLowerInterval
.end
,
278 compositeChargingScheduleHigherInterval
.start
280 : differenceInSeconds(
281 compositeChargingScheduleHigherInterval
.end
,
282 compositeChargingScheduleLowerInterval
.start
284 startSchedule
: higherFirst
285 ? (compositeChargingScheduleHigherInterval
.start
as Date)
286 : (compositeChargingScheduleLowerInterval
.start
as Date),
290 ...compositeChargingScheduleLower
,
291 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
292 ...compositeChargingScheduleHigher
!,
293 chargingSchedulePeriod
: [
294 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
295 ...compositeChargingScheduleHigher
!.chargingSchedulePeriod
.map(schedulePeriod
=> {
298 startPeriod
: higherFirst
300 : schedulePeriod
.startPeriod
+
302 compositeChargingScheduleHigherInterval
.start
,
303 compositeChargingScheduleLowerInterval
.start
307 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
308 ...compositeChargingScheduleLower
!.chargingSchedulePeriod
309 .filter((schedulePeriod
, index
) => {
314 compositeChargingScheduleLowerInterval
.start
,
315 schedulePeriod
.startPeriod
318 end
: compositeChargingScheduleHigherInterval
.end
,
319 start
: compositeChargingScheduleLowerInterval
.start
,
327 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
328 index
< compositeChargingScheduleLower
!.chargingSchedulePeriod
.length
- 1 &&
331 compositeChargingScheduleLowerInterval
.start
,
332 schedulePeriod
.startPeriod
335 end
: compositeChargingScheduleHigherInterval
.end
,
336 start
: compositeChargingScheduleLowerInterval
.start
,
341 compositeChargingScheduleLowerInterval
.start
,
342 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
343 compositeChargingScheduleLower
!.chargingSchedulePeriod
[index
+ 1].startPeriod
346 end
: compositeChargingScheduleHigherInterval
.end
,
347 start
: compositeChargingScheduleLowerInterval
.start
,
357 compositeChargingScheduleLowerInterval
.start
,
358 schedulePeriod
.startPeriod
361 end
: compositeChargingScheduleLowerInterval
.end
,
362 start
: compositeChargingScheduleHigherInterval
.start
,
370 .map((schedulePeriod
, index
) => {
371 if (index
=== 0 && schedulePeriod
.startPeriod
!== 0) {
372 schedulePeriod
.startPeriod
= 0
376 startPeriod
: higherFirst
377 ? schedulePeriod
.startPeriod
+
379 compositeChargingScheduleLowerInterval
.start
,
380 compositeChargingScheduleHigherInterval
.start
385 ].sort((a
, b
) => a
.startPeriod
- b
.startPeriod
),
386 duration
: higherFirst
387 ? differenceInSeconds(
388 compositeChargingScheduleLowerInterval
.end
,
389 compositeChargingScheduleHigherInterval
.start
391 : differenceInSeconds(
392 compositeChargingScheduleHigherInterval
.end
,
393 compositeChargingScheduleLowerInterval
.start
395 startSchedule
: higherFirst
396 ? (compositeChargingScheduleHigherInterval
.start
as Date)
397 : (compositeChargingScheduleLowerInterval
.start
as Date),
401 public static hasReservation
= (
402 chargingStation
: ChargingStation
,
406 const connectorReservation
= chargingStation
.getReservationBy('connectorId', connectorId
)
407 const chargingStationReservation
= chargingStation
.getReservationBy('connectorId', 0)
409 (chargingStation
.getConnectorStatus(connectorId
)?.status ===
410 OCPP16ChargePointStatus
.Reserved
&&
411 connectorReservation
!= null &&
412 !hasReservationExpired(connectorReservation
) &&
413 connectorReservation
.idTag
=== idTag
) ||
414 (chargingStation
.getConnectorStatus(0)?.status === OCPP16ChargePointStatus
.Reserved
&&
415 chargingStationReservation
!= null &&
416 !hasReservationExpired(chargingStationReservation
) &&
417 chargingStationReservation
.idTag
=== idTag
)
420 `${chargingStation.logPrefix()} Connector id ${connectorId.toString()} has a valid reservation for idTag ${idTag}: %j`,
421 connectorReservation
?? chargingStationReservation
428 public static remoteStopTransaction
= async (
429 chargingStation
: ChargingStation
,
431 ): Promise
<GenericResponse
> => {
432 await OCPP16ServiceUtils
.sendAndSetConnectorStatus(
435 OCPP16ChargePointStatus
.Finishing
437 const stopResponse
= await chargingStation
.stopTransactionOnConnector(
439 OCPP16StopTransactionReason
.REMOTE
441 if (stopResponse
.idTagInfo
?.status === OCPP16AuthorizationStatus
.ACCEPTED
) {
442 return OCPP16Constants
.OCPP_RESPONSE_ACCEPTED
444 return OCPP16Constants
.OCPP_RESPONSE_REJECTED
447 public static buildTransactionBeginMeterValue (
448 chargingStation
: ChargingStation
,
450 meterStart
: number | undefined
451 ): OCPP16MeterValue
{
452 const meterValue
: OCPP16MeterValue
= {
454 timestamp
: new Date(),
456 // Energy.Active.Import.Register measurand (default)
457 const sampledValueTemplate
= OCPP16ServiceUtils
.getSampledValueTemplate(
462 sampledValueTemplate
?.unit
=== OCPP16MeterValueUnit
.KILO_WATT_HOUR
? 1000 : 1
463 meterValue
.sampledValue
.push(
464 OCPP16ServiceUtils
.buildSampledValue(
465 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
466 sampledValueTemplate
!,
467 roundTo((meterStart
?? 0) / unitDivider
, 4),
468 OCPP16MeterValueContext
.TRANSACTION_BEGIN
474 public static buildTransactionDataMeterValues (
475 transactionBeginMeterValue
: OCPP16MeterValue
,
476 transactionEndMeterValue
: OCPP16MeterValue
477 ): OCPP16MeterValue
[] {
478 const meterValues
: OCPP16MeterValue
[] = []
479 meterValues
.push(transactionBeginMeterValue
)
480 meterValues
.push(transactionEndMeterValue
)
484 public static checkFeatureProfile (
485 chargingStation
: ChargingStation
,
486 featureProfile
: OCPP16SupportedFeatureProfiles
,
487 command
: OCPP16IncomingRequestCommand
| OCPP16RequestCommand
489 if (hasFeatureProfile(chargingStation
, featureProfile
) === false) {
491 `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
492 OCPP16StandardParametersKey.SupportedFeatureProfiles
500 public static isConfigurationKeyVisible (key
: ConfigurationKey
): boolean {
501 if (key
.visible
== null) {
507 public static override parseJsonSchemaFile
<T
extends JsonType
>(
508 relativePath
: string,
511 ): JSONSchemaType
<T
> {
512 return super.parseJsonSchemaFile
<T
>(
514 OCPPVersion
.VERSION_16
,
520 public static setChargingProfile (
521 chargingStation
: ChargingStation
,
523 cp
: OCPP16ChargingProfile
525 if (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
== null) {
527 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId.toString()} with an uninitialized charging profiles array attribute, applying deferred initialization`
529 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
530 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
= []
532 if (!Array.isArray(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
534 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId.toString()} with an improper attribute type for the charging profiles array, applying proper type deferred initialization`
536 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
537 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
= []
539 cp
.chargingSchedule
.startSchedule
= convertToDate(cp
.chargingSchedule
.startSchedule
)
540 cp
.validFrom
= convertToDate(cp
.validFrom
)
541 cp
.validTo
= convertToDate(cp
.validTo
)
542 let cpReplaced
= false
543 if (isNotEmptyArray(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
544 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
545 for (const [index
, chargingProfile
] of chargingStation
546 .getConnectorStatus(connectorId
)!
547 .chargingProfiles
!.entries()) {
549 chargingProfile
.chargingProfileId
=== cp
.chargingProfileId
||
550 (chargingProfile
.stackLevel
=== cp
.stackLevel
&&
551 chargingProfile
.chargingProfilePurpose
=== cp
.chargingProfilePurpose
)
553 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
554 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
![index
] = cp
559 !cpReplaced
&& chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?.push(cp
)