1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
3 import type { JSONSchemaType
} from
'ajv'
6 areIntervalsOverlapping
,
18 } from
'../../../charging-station/index.js'
20 type ConfigurationKey
,
23 OCPP16AuthorizationStatus
,
24 type OCPP16AvailabilityType
,
25 type OCPP16ChangeAvailabilityResponse
,
26 OCPP16ChargePointStatus
,
27 type OCPP16ChargingProfile
,
28 type OCPP16ChargingSchedule
,
29 type OCPP16ClearChargingProfileRequest
,
30 type OCPP16IncomingRequestCommand
,
31 type OCPP16MeterValue
,
32 OCPP16MeterValueContext
,
34 type OCPP16RequestCommand
,
35 OCPP16StandardParametersKey
,
36 OCPP16StopTransactionReason
,
37 type OCPP16SupportedFeatureProfiles
,
39 } from
'../../../types/index.js'
40 import { convertToDate
, isNotEmptyArray
, logger
, roundTo
} from
'../../../utils/index.js'
41 import { OCPPServiceUtils
} from
'../OCPPServiceUtils.js'
42 import { OCPP16Constants
} from
'./OCPP16Constants.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 cp
.chargingSchedule
.startSchedule
= convertToDate(cp
.chargingSchedule
.startSchedule
)
168 cp
.validFrom
= convertToDate(cp
.validFrom
)
169 cp
.validTo
= convertToDate(cp
.validTo
)
170 let cpReplaced
= false
171 if (isNotEmptyArray(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
172 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
173 for (const [index
, chargingProfile
] of chargingStation
174 .getConnectorStatus(connectorId
)!
175 .chargingProfiles
!.entries()) {
177 chargingProfile
.chargingProfileId
=== cp
.chargingProfileId
||
178 (chargingProfile
.stackLevel
=== cp
.stackLevel
&&
179 chargingProfile
.chargingProfilePurpose
=== cp
.chargingProfilePurpose
)
181 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
182 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
![index
] = cp
187 !cpReplaced
&& chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?.push(cp
)
190 public static clearChargingProfiles
= (
191 chargingStation
: ChargingStation
,
192 commandPayload
: OCPP16ClearChargingProfileRequest
,
193 chargingProfiles
: OCPP16ChargingProfile
[] | undefined
195 const { id
, chargingProfilePurpose
, stackLevel
} = commandPayload
196 let clearedCP
= false
197 if (isNotEmptyArray(chargingProfiles
)) {
198 chargingProfiles
.forEach((chargingProfile
: OCPP16ChargingProfile
, index
: number) => {
199 let clearCurrentCP
= false
200 if (chargingProfile
.chargingProfileId
=== id
) {
201 clearCurrentCP
= true
203 if (chargingProfilePurpose
== null && chargingProfile
.stackLevel
=== stackLevel
) {
204 clearCurrentCP
= true
207 stackLevel
== null &&
208 chargingProfile
.chargingProfilePurpose
=== chargingProfilePurpose
210 clearCurrentCP
= true
213 chargingProfile
.stackLevel
=== stackLevel
&&
214 chargingProfile
.chargingProfilePurpose
=== chargingProfilePurpose
216 clearCurrentCP
= true
218 if (clearCurrentCP
) {
219 chargingProfiles
.splice(index
, 1)
221 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
231 public static composeChargingSchedules
= (
232 chargingScheduleHigher
: OCPP16ChargingSchedule
| undefined,
233 chargingScheduleLower
: OCPP16ChargingSchedule
| undefined,
234 compositeInterval
: Interval
235 ): OCPP16ChargingSchedule
| undefined => {
236 if (chargingScheduleHigher
== null && chargingScheduleLower
== null) {
239 if (chargingScheduleHigher
!= null && chargingScheduleLower
== null) {
240 return OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleHigher
, compositeInterval
)
242 if (chargingScheduleHigher
== null && chargingScheduleLower
!= null) {
243 return OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleLower
, compositeInterval
)
245 const compositeChargingScheduleHigher
: OCPP16ChargingSchedule
| undefined =
246 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
247 OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleHigher
!, compositeInterval
)
248 const compositeChargingScheduleLower
: OCPP16ChargingSchedule
| undefined =
249 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
250 OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleLower
!, compositeInterval
)
251 const compositeChargingScheduleHigherInterval
: Interval
= {
252 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
253 start
: compositeChargingScheduleHigher
!.startSchedule
!,
255 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
256 compositeChargingScheduleHigher
!.startSchedule
!,
257 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
258 compositeChargingScheduleHigher
!.duration
!
261 const compositeChargingScheduleLowerInterval
: Interval
= {
262 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
263 start
: compositeChargingScheduleLower
!.startSchedule
!,
265 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
266 compositeChargingScheduleLower
!.startSchedule
!,
267 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
268 compositeChargingScheduleLower
!.duration
!
271 const higherFirst
= isBefore(
272 compositeChargingScheduleHigherInterval
.start
,
273 compositeChargingScheduleLowerInterval
.start
276 !areIntervalsOverlapping(
277 compositeChargingScheduleHigherInterval
,
278 compositeChargingScheduleLowerInterval
282 ...compositeChargingScheduleLower
,
283 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
284 ...compositeChargingScheduleHigher
!,
285 startSchedule
: higherFirst
286 ? (compositeChargingScheduleHigherInterval
.start
as Date)
287 : (compositeChargingScheduleLowerInterval
.start
as Date),
288 duration
: higherFirst
289 ? differenceInSeconds(
290 compositeChargingScheduleLowerInterval
.end
,
291 compositeChargingScheduleHigherInterval
.start
293 : differenceInSeconds(
294 compositeChargingScheduleHigherInterval
.end
,
295 compositeChargingScheduleLowerInterval
.start
297 chargingSchedulePeriod
: [
298 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
299 ...compositeChargingScheduleHigher
!.chargingSchedulePeriod
.map(schedulePeriod
=> {
302 startPeriod
: higherFirst
304 : schedulePeriod
.startPeriod
+
306 compositeChargingScheduleHigherInterval
.start
,
307 compositeChargingScheduleLowerInterval
.start
311 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
312 ...compositeChargingScheduleLower
!.chargingSchedulePeriod
.map(schedulePeriod
=> {
315 startPeriod
: higherFirst
316 ? schedulePeriod
.startPeriod
+
318 compositeChargingScheduleLowerInterval
.start
,
319 compositeChargingScheduleHigherInterval
.start
324 ].sort((a
, b
) => a
.startPeriod
- b
.startPeriod
)
328 ...compositeChargingScheduleLower
,
329 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
330 ...compositeChargingScheduleHigher
!,
331 startSchedule
: higherFirst
332 ? (compositeChargingScheduleHigherInterval
.start
as Date)
333 : (compositeChargingScheduleLowerInterval
.start
as Date),
334 duration
: higherFirst
335 ? differenceInSeconds(
336 compositeChargingScheduleLowerInterval
.end
,
337 compositeChargingScheduleHigherInterval
.start
339 : differenceInSeconds(
340 compositeChargingScheduleHigherInterval
.end
,
341 compositeChargingScheduleLowerInterval
.start
343 chargingSchedulePeriod
: [
344 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
345 ...compositeChargingScheduleHigher
!.chargingSchedulePeriod
.map(schedulePeriod
=> {
348 startPeriod
: higherFirst
350 : schedulePeriod
.startPeriod
+
352 compositeChargingScheduleHigherInterval
.start
,
353 compositeChargingScheduleLowerInterval
.start
357 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
358 ...compositeChargingScheduleLower
!.chargingSchedulePeriod
359 .filter((schedulePeriod
, index
) => {
364 compositeChargingScheduleLowerInterval
.start
,
365 schedulePeriod
.startPeriod
368 start
: compositeChargingScheduleLowerInterval
.start
,
369 end
: compositeChargingScheduleHigherInterval
.end
377 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
378 index
< compositeChargingScheduleLower
!.chargingSchedulePeriod
.length
- 1 &&
381 compositeChargingScheduleLowerInterval
.start
,
382 schedulePeriod
.startPeriod
385 start
: compositeChargingScheduleLowerInterval
.start
,
386 end
: compositeChargingScheduleHigherInterval
.end
391 compositeChargingScheduleLowerInterval
.start
,
392 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
393 compositeChargingScheduleLower
!.chargingSchedulePeriod
[index
+ 1].startPeriod
396 start
: compositeChargingScheduleLowerInterval
.start
,
397 end
: compositeChargingScheduleHigherInterval
.end
407 compositeChargingScheduleLowerInterval
.start
,
408 schedulePeriod
.startPeriod
411 start
: compositeChargingScheduleHigherInterval
.start
,
412 end
: compositeChargingScheduleLowerInterval
.end
420 .map((schedulePeriod
, index
) => {
421 if (index
=== 0 && schedulePeriod
.startPeriod
!== 0) {
422 schedulePeriod
.startPeriod
= 0
426 startPeriod
: higherFirst
427 ? schedulePeriod
.startPeriod
+
429 compositeChargingScheduleLowerInterval
.start
,
430 compositeChargingScheduleHigherInterval
.start
435 ].sort((a
, b
) => a
.startPeriod
- b
.startPeriod
)
439 public static isConfigurationKeyVisible (key
: ConfigurationKey
): boolean {
440 if (key
.visible
== null) {
446 public static hasReservation
= (
447 chargingStation
: ChargingStation
,
451 const connectorReservation
= chargingStation
.getReservationBy('connectorId', connectorId
)
452 const chargingStationReservation
= chargingStation
.getReservationBy('connectorId', 0)
454 (chargingStation
.getConnectorStatus(connectorId
)?.status ===
455 OCPP16ChargePointStatus
.Reserved
&&
456 connectorReservation
!= null &&
457 !hasReservationExpired(connectorReservation
) &&
458 connectorReservation
.idTag
=== idTag
) ||
459 (chargingStation
.getConnectorStatus(0)?.status === OCPP16ChargePointStatus
.Reserved
&&
460 chargingStationReservation
!= null &&
461 !hasReservationExpired(chargingStationReservation
) &&
462 chargingStationReservation
.idTag
=== idTag
)
465 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
466 connectorReservation
?? chargingStationReservation
473 public static parseJsonSchemaFile
<T
extends JsonType
>(
474 relativePath
: string,
477 ): JSONSchemaType
<T
> {
478 return super.parseJsonSchemaFile
<T
>(
480 OCPPVersion
.VERSION_16
,
486 private static readonly composeChargingSchedule
= (
487 chargingSchedule
: OCPP16ChargingSchedule
,
488 compositeInterval
: Interval
489 ): OCPP16ChargingSchedule
| undefined => {
490 const chargingScheduleInterval
: Interval
= {
491 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
492 start
: chargingSchedule
.startSchedule
!,
493 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
494 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!)
496 if (areIntervalsOverlapping(chargingScheduleInterval
, compositeInterval
)) {
497 chargingSchedule
.chargingSchedulePeriod
.sort((a
, b
) => a
.startPeriod
- b
.startPeriod
)
498 if (isBefore(chargingScheduleInterval
.start
, compositeInterval
.start
)) {
501 startSchedule
: compositeInterval
.start
as Date,
502 duration
: differenceInSeconds(
503 chargingScheduleInterval
.end
,
504 compositeInterval
.start
as Date
506 chargingSchedulePeriod
: chargingSchedule
.chargingSchedulePeriod
507 .filter((schedulePeriod
, index
) => {
510 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
),
517 index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
519 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
),
524 chargingScheduleInterval
.start
,
525 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
534 .map((schedulePeriod
, index
) => {
535 if (index
=== 0 && schedulePeriod
.startPeriod
!== 0) {
536 schedulePeriod
.startPeriod
= 0
538 return schedulePeriod
542 if (isAfter(chargingScheduleInterval
.end
, compositeInterval
.end
)) {
545 duration
: differenceInSeconds(
546 compositeInterval
.end
as Date,
547 chargingScheduleInterval
.start
549 chargingSchedulePeriod
: chargingSchedule
.chargingSchedulePeriod
.filter(schedulePeriod
=>
551 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
),
557 return chargingSchedule