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'
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 { 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
) === false) {
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 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
79 sampledValueTemplate
!,
80 roundTo((meterStart
?? 0) / unitDivider
, 4),
81 OCPP16MeterValueContext
.TRANSACTION_BEGIN
87 public static buildTransactionDataMeterValues (
88 transactionBeginMeterValue
: OCPP16MeterValue
,
89 transactionEndMeterValue
: OCPP16MeterValue
90 ): OCPP16MeterValue
[] {
91 const meterValues
: OCPP16MeterValue
[] = []
92 meterValues
.push(transactionBeginMeterValue
)
93 meterValues
.push(transactionEndMeterValue
)
97 public static remoteStopTransaction
= async (
98 chargingStation
: ChargingStation
,
100 ): Promise
<GenericResponse
> => {
101 await OCPP16ServiceUtils
.sendAndSetConnectorStatus(
104 OCPP16ChargePointStatus
.Finishing
106 const stopResponse
= await chargingStation
.stopTransactionOnConnector(
108 OCPP16StopTransactionReason
.REMOTE
110 if (stopResponse
.idTagInfo
?.status === OCPP16AuthorizationStatus
.ACCEPTED
) {
111 return OCPP16Constants
.OCPP_RESPONSE_ACCEPTED
113 return OCPP16Constants
.OCPP_RESPONSE_REJECTED
116 public static changeAvailability
= async (
117 chargingStation
: ChargingStation
,
118 connectorIds
: number[],
119 chargePointStatus
: OCPP16ChargePointStatus
,
120 availabilityType
: OCPP16AvailabilityType
121 ): Promise
<OCPP16ChangeAvailabilityResponse
> => {
122 const responses
: OCPP16ChangeAvailabilityResponse
[] = []
123 for (const connectorId
of connectorIds
) {
124 let response
: OCPP16ChangeAvailabilityResponse
=
125 OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
126 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
127 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!
128 if (connectorStatus
?.transactionStarted
=== true) {
129 response
= OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
131 connectorStatus
.availability
= availabilityType
132 if (response
=== OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
) {
133 await OCPP16ServiceUtils
.sendAndSetConnectorStatus(
139 responses
.push(response
)
141 if (responses
.includes(OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
)) {
142 return OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
144 return OCPP16Constants
.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
147 public static setChargingProfile (
148 chargingStation
: ChargingStation
,
150 cp
: OCPP16ChargingProfile
152 if (isNullOrUndefined(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
154 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`
156 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
157 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
= []
159 if (!Array.isArray(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
161 `${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`
163 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
164 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
= []
166 let cpReplaced
= false
167 if (isNotEmptyArray(chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
169 .getConnectorStatus(connectorId
)
170 ?.chargingProfiles
?.forEach((chargingProfile
: OCPP16ChargingProfile
, index
: number) => {
172 chargingProfile
.chargingProfileId
=== cp
.chargingProfileId
||
173 (chargingProfile
.stackLevel
=== cp
.stackLevel
&&
174 chargingProfile
.chargingProfilePurpose
=== cp
.chargingProfilePurpose
)
176 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
177 chargingStation
.getConnectorStatus(connectorId
)!.chargingProfiles
![index
] = cp
182 !cpReplaced
&& chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?.push(cp
)
185 public static clearChargingProfiles
= (
186 chargingStation
: ChargingStation
,
187 commandPayload
: OCPP16ClearChargingProfileRequest
,
188 chargingProfiles
: OCPP16ChargingProfile
[] | undefined
190 const { id
, chargingProfilePurpose
, stackLevel
} = commandPayload
191 let clearedCP
= false
192 if (isNotEmptyArray(chargingProfiles
)) {
193 chargingProfiles
?.forEach((chargingProfile
: OCPP16ChargingProfile
, index
: number) => {
194 let clearCurrentCP
= false
195 if (chargingProfile
.chargingProfileId
=== id
) {
196 clearCurrentCP
= true
198 if (chargingProfilePurpose
== null && chargingProfile
.stackLevel
=== stackLevel
) {
199 clearCurrentCP
= true
202 stackLevel
== null &&
203 chargingProfile
.chargingProfilePurpose
=== chargingProfilePurpose
205 clearCurrentCP
= true
208 chargingProfile
.stackLevel
=== stackLevel
&&
209 chargingProfile
.chargingProfilePurpose
=== chargingProfilePurpose
211 clearCurrentCP
= true
213 if (clearCurrentCP
) {
214 chargingProfiles
.splice(index
, 1)
216 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
226 public static composeChargingSchedules
= (
227 chargingScheduleHigher
: OCPP16ChargingSchedule
| undefined,
228 chargingScheduleLower
: OCPP16ChargingSchedule
| undefined,
229 compositeInterval
: Interval
230 ): OCPP16ChargingSchedule
| undefined => {
231 if (chargingScheduleHigher
== null && chargingScheduleLower
== null) {
234 if (chargingScheduleHigher
!= null && chargingScheduleLower
== null) {
235 return OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleHigher
, compositeInterval
)
237 if (chargingScheduleHigher
== null && chargingScheduleLower
!= null) {
238 return OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleLower
, compositeInterval
)
240 const compositeChargingScheduleHigher
: OCPP16ChargingSchedule
| undefined =
241 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
242 OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleHigher
!, compositeInterval
)
243 const compositeChargingScheduleLower
: OCPP16ChargingSchedule
| undefined =
244 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
245 OCPP16ServiceUtils
.composeChargingSchedule(chargingScheduleLower
!, compositeInterval
)
246 const compositeChargingScheduleHigherInterval
: Interval
= {
247 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
248 start
: compositeChargingScheduleHigher
!.startSchedule
!,
250 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
251 compositeChargingScheduleHigher
!.startSchedule
!,
252 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
253 compositeChargingScheduleHigher
!.duration
!
256 const compositeChargingScheduleLowerInterval
: Interval
= {
257 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
258 start
: compositeChargingScheduleLower
!.startSchedule
!,
260 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
261 compositeChargingScheduleLower
!.startSchedule
!,
262 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
263 compositeChargingScheduleLower
!.duration
!
266 const higherFirst
= isBefore(
267 compositeChargingScheduleHigherInterval
.start
,
268 compositeChargingScheduleLowerInterval
.start
271 !areIntervalsOverlapping(
272 compositeChargingScheduleHigherInterval
,
273 compositeChargingScheduleLowerInterval
277 ...compositeChargingScheduleLower
,
278 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
279 ...compositeChargingScheduleHigher
!,
280 startSchedule
: higherFirst
281 ? (compositeChargingScheduleHigherInterval
.start
as Date)
282 : (compositeChargingScheduleLowerInterval
.start
as Date),
283 duration
: higherFirst
284 ? differenceInSeconds(
285 compositeChargingScheduleLowerInterval
.end
,
286 compositeChargingScheduleHigherInterval
.start
288 : differenceInSeconds(
289 compositeChargingScheduleHigherInterval
.end
,
290 compositeChargingScheduleLowerInterval
.start
292 chargingSchedulePeriod
: [
293 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
294 ...compositeChargingScheduleHigher
!.chargingSchedulePeriod
.map((schedulePeriod
) => {
297 startPeriod
: higherFirst
299 : schedulePeriod
.startPeriod
+
301 compositeChargingScheduleHigherInterval
.start
,
302 compositeChargingScheduleLowerInterval
.start
306 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
307 ...compositeChargingScheduleLower
!.chargingSchedulePeriod
.map((schedulePeriod
) => {
310 startPeriod
: higherFirst
311 ? schedulePeriod
.startPeriod
+
313 compositeChargingScheduleLowerInterval
.start
,
314 compositeChargingScheduleHigherInterval
.start
319 ].sort((a
, b
) => a
.startPeriod
- b
.startPeriod
)
323 ...compositeChargingScheduleLower
,
324 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
325 ...compositeChargingScheduleHigher
!,
326 startSchedule
: higherFirst
327 ? (compositeChargingScheduleHigherInterval
.start
as Date)
328 : (compositeChargingScheduleLowerInterval
.start
as Date),
329 duration
: higherFirst
330 ? differenceInSeconds(
331 compositeChargingScheduleLowerInterval
.end
,
332 compositeChargingScheduleHigherInterval
.start
334 : differenceInSeconds(
335 compositeChargingScheduleHigherInterval
.end
,
336 compositeChargingScheduleLowerInterval
.start
338 chargingSchedulePeriod
: [
339 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
340 ...compositeChargingScheduleHigher
!.chargingSchedulePeriod
.map((schedulePeriod
) => {
343 startPeriod
: higherFirst
345 : schedulePeriod
.startPeriod
+
347 compositeChargingScheduleHigherInterval
.start
,
348 compositeChargingScheduleLowerInterval
.start
352 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
353 ...compositeChargingScheduleLower
!.chargingSchedulePeriod
354 .filter((schedulePeriod
, index
) => {
359 compositeChargingScheduleLowerInterval
.start
,
360 schedulePeriod
.startPeriod
363 start
: compositeChargingScheduleLowerInterval
.start
,
364 end
: compositeChargingScheduleHigherInterval
.end
372 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
373 index
< compositeChargingScheduleLower
!.chargingSchedulePeriod
.length
- 1 &&
376 compositeChargingScheduleLowerInterval
.start
,
377 schedulePeriod
.startPeriod
380 start
: compositeChargingScheduleLowerInterval
.start
,
381 end
: compositeChargingScheduleHigherInterval
.end
386 compositeChargingScheduleLowerInterval
.start
,
387 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
388 compositeChargingScheduleLower
!.chargingSchedulePeriod
[index
+ 1].startPeriod
391 start
: compositeChargingScheduleLowerInterval
.start
,
392 end
: compositeChargingScheduleHigherInterval
.end
402 compositeChargingScheduleLowerInterval
.start
,
403 schedulePeriod
.startPeriod
406 start
: compositeChargingScheduleHigherInterval
.start
,
407 end
: compositeChargingScheduleLowerInterval
.end
415 .map((schedulePeriod
, index
) => {
416 if (index
=== 0 && schedulePeriod
.startPeriod
!== 0) {
417 schedulePeriod
.startPeriod
= 0
421 startPeriod
: higherFirst
422 ? schedulePeriod
.startPeriod
+
424 compositeChargingScheduleLowerInterval
.start
,
425 compositeChargingScheduleHigherInterval
.start
430 ].sort((a
, b
) => a
.startPeriod
- b
.startPeriod
)
434 public static hasReservation
= (
435 chargingStation
: ChargingStation
,
439 const connectorReservation
= chargingStation
.getReservationBy('connectorId', connectorId
)
440 const chargingStationReservation
= chargingStation
.getReservationBy('connectorId', 0)
442 (chargingStation
.getConnectorStatus(connectorId
)?.status ===
443 OCPP16ChargePointStatus
.Reserved
&&
444 connectorReservation
!= null &&
445 !hasReservationExpired(connectorReservation
) &&
446 connectorReservation
?.idTag
=== idTag
) ||
447 (chargingStation
.getConnectorStatus(0)?.status === OCPP16ChargePointStatus
.Reserved
&&
448 chargingStationReservation
!= null &&
449 !hasReservationExpired(chargingStationReservation
) &&
450 chargingStationReservation
?.idTag
=== idTag
)
453 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
454 connectorReservation
?? chargingStationReservation
461 public static parseJsonSchemaFile
<T
extends JsonType
>(
462 relativePath
: string,
465 ): JSONSchemaType
<T
> {
466 return super.parseJsonSchemaFile
<T
>(
468 OCPPVersion
.VERSION_16
,
474 private static readonly composeChargingSchedule
= (
475 chargingSchedule
: OCPP16ChargingSchedule
,
476 compositeInterval
: Interval
477 ): OCPP16ChargingSchedule
| undefined => {
478 const chargingScheduleInterval
: Interval
= {
479 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
480 start
: chargingSchedule
.startSchedule
!,
481 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
482 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!)
484 if (areIntervalsOverlapping(chargingScheduleInterval
, compositeInterval
)) {
485 chargingSchedule
.chargingSchedulePeriod
.sort((a
, b
) => a
.startPeriod
- b
.startPeriod
)
486 if (isBefore(chargingScheduleInterval
.start
, compositeInterval
.start
)) {
489 startSchedule
: compositeInterval
.start
as Date,
490 duration
: differenceInSeconds(
491 chargingScheduleInterval
.end
,
492 compositeInterval
.start
as Date
494 chargingSchedulePeriod
: chargingSchedule
.chargingSchedulePeriod
495 .filter((schedulePeriod
, index
) => {
498 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
499 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
)!,
506 index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
508 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
),
513 chargingScheduleInterval
.start
,
514 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
523 .map((schedulePeriod
, index
) => {
524 if (index
=== 0 && schedulePeriod
.startPeriod
!== 0) {
525 schedulePeriod
.startPeriod
= 0
527 return schedulePeriod
531 if (isAfter(chargingScheduleInterval
.end
, compositeInterval
.end
)) {
534 duration
: differenceInSeconds(
535 compositeInterval
.end
as Date,
536 chargingScheduleInterval
.start
538 chargingSchedulePeriod
: chargingSchedule
.chargingSchedulePeriod
.filter((schedulePeriod
) =>
540 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
541 addSeconds(chargingScheduleInterval
.start
, schedulePeriod
.startPeriod
)!,
547 return chargingSchedule