Commit | Line | Data |
---|---|---|
edd13439 | 1 | // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. |
c8eeb62b | 2 | |
130783a7 | 3 | import type { JSONSchemaType } from 'ajv'; |
ef9e3b33 JB |
4 | import { |
5 | addSeconds, | |
6 | areIntervalsOverlapping, | |
7 | differenceInSeconds, | |
8 | isAfter, | |
9 | isBefore, | |
10 | isWithinInterval, | |
11 | } from 'date-fns'; | |
130783a7 | 12 | |
366f75f6 | 13 | import { OCPP16Constants } from './OCPP16Constants'; |
90aceaf6 JB |
14 | import { |
15 | type ChargingStation, | |
16 | hasFeatureProfile, | |
17 | hasReservationExpired, | |
18 | } from '../../../charging-station'; | |
268a74bb | 19 | import { OCPPError } from '../../../exception'; |
e7aeea18 | 20 | import { |
73d87be1 | 21 | type ClearChargingProfileRequest, |
268a74bb JB |
22 | CurrentType, |
23 | ErrorType, | |
d19b10a8 | 24 | type GenericResponse, |
268a74bb JB |
25 | type JsonType, |
26 | type MeasurandPerPhaseSampledValueTemplates, | |
27 | type MeasurandValues, | |
e7aeea18 JB |
28 | MeterValueContext, |
29 | MeterValueLocation, | |
30 | MeterValueUnit, | |
d19b10a8 | 31 | OCPP16AuthorizationStatus, |
366f75f6 JB |
32 | OCPP16AvailabilityType, |
33 | type OCPP16ChangeAvailabilityResponse, | |
34 | OCPP16ChargePointStatus, | |
268a74bb | 35 | type OCPP16ChargingProfile, |
ef9e3b33 | 36 | type OCPP16ChargingSchedule, |
268a74bb | 37 | type OCPP16IncomingRequestCommand, |
27782dbc | 38 | type OCPP16MeterValue, |
e7aeea18 JB |
39 | OCPP16MeterValueMeasurand, |
40 | OCPP16MeterValuePhase, | |
370ae4ee | 41 | OCPP16RequestCommand, |
268a74bb JB |
42 | type OCPP16SampledValue, |
43 | OCPP16StandardParametersKey, | |
d19b10a8 | 44 | OCPP16StopTransactionReason, |
268a74bb JB |
45 | type OCPP16SupportedFeatureProfiles, |
46 | OCPPVersion, | |
47 | type SampledValueTemplate, | |
48 | Voltage, | |
49 | } from '../../../types'; | |
9bf0ef23 JB |
50 | import { |
51 | ACElectricUtils, | |
52 | Constants, | |
53 | DCElectricUtils, | |
54 | convertToFloat, | |
55 | convertToInt, | |
56 | getRandomFloatFluctuatedRounded, | |
57 | getRandomFloatRounded, | |
58 | getRandomInteger, | |
59 | isNotEmptyArray, | |
9bf0ef23 JB |
60 | isNullOrUndefined, |
61 | isUndefined, | |
62 | logger, | |
63 | roundTo, | |
64 | } from '../../../utils'; | |
4c3c0d59 | 65 | import { OCPPServiceUtils } from '../OCPPServiceUtils'; |
6ed92bc1 | 66 | |
7bc31f9c | 67 | export class OCPP16ServiceUtils extends OCPPServiceUtils { |
370ae4ee JB |
68 | public static checkFeatureProfile( |
69 | chargingStation: ChargingStation, | |
70 | featureProfile: OCPP16SupportedFeatureProfiles, | |
5edd8ba0 | 71 | command: OCPP16RequestCommand | OCPP16IncomingRequestCommand, |
370ae4ee | 72 | ): boolean { |
d8093be1 | 73 | if (!hasFeatureProfile(chargingStation, featureProfile)) { |
370ae4ee JB |
74 | logger.warn( |
75 | `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${ | |
76 | OCPP16StandardParametersKey.SupportedFeatureProfiles | |
5edd8ba0 | 77 | } in configuration`, |
370ae4ee JB |
78 | ); |
79 | return false; | |
80 | } | |
81 | return true; | |
82 | } | |
83 | ||
78085c42 JB |
84 | public static buildMeterValue( |
85 | chargingStation: ChargingStation, | |
86 | connectorId: number, | |
87 | transactionId: number, | |
88 | interval: number, | |
5edd8ba0 | 89 | debug = false, |
78085c42 JB |
90 | ): OCPP16MeterValue { |
91 | const meterValue: OCPP16MeterValue = { | |
c38f0ced | 92 | timestamp: new Date(), |
78085c42 JB |
93 | sampledValue: [], |
94 | }; | |
95 | const connector = chargingStation.getConnectorStatus(connectorId); | |
96 | // SoC measurand | |
ed3d2808 | 97 | const socSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 98 | chargingStation, |
78085c42 | 99 | connectorId, |
5edd8ba0 | 100 | OCPP16MeterValueMeasurand.STATE_OF_CHARGE, |
78085c42 JB |
101 | ); |
102 | if (socSampledValueTemplate) { | |
860ef183 JB |
103 | const socMaximumValue = 100; |
104 | const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0; | |
78085c42 | 105 | const socSampledValueTemplateValue = socSampledValueTemplate.value |
9bf0ef23 | 106 | ? getRandomFloatFluctuatedRounded( |
78085c42 | 107 | parseInt(socSampledValueTemplate.value), |
5edd8ba0 | 108 | socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 | 109 | ) |
9bf0ef23 | 110 | : getRandomInteger(socMaximumValue, socMinimumValue); |
78085c42 | 111 | meterValue.sampledValue.push( |
5edd8ba0 | 112 | OCPP16ServiceUtils.buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue), |
78085c42 JB |
113 | ); |
114 | const sampledValuesIndex = meterValue.sampledValue.length - 1; | |
860ef183 | 115 | if ( |
9bf0ef23 JB |
116 | convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue || |
117 | convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue || | |
860ef183 JB |
118 | debug |
119 | ) { | |
78085c42 JB |
120 | logger.error( |
121 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
122 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
123 | OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
5edd8ba0 | 124 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${socMinimumValue}/${ |
78085c42 | 125 | meterValue.sampledValue[sampledValuesIndex].value |
5edd8ba0 | 126 | }/${socMaximumValue}}`, |
78085c42 JB |
127 | ); |
128 | } | |
129 | } | |
130 | // Voltage measurand | |
ed3d2808 | 131 | const voltageSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 132 | chargingStation, |
78085c42 | 133 | connectorId, |
5edd8ba0 | 134 | OCPP16MeterValueMeasurand.VOLTAGE, |
78085c42 JB |
135 | ); |
136 | if (voltageSampledValueTemplate) { | |
137 | const voltageSampledValueTemplateValue = voltageSampledValueTemplate.value | |
138 | ? parseInt(voltageSampledValueTemplate.value) | |
139 | : chargingStation.getVoltageOut(); | |
140 | const fluctuationPercent = | |
141 | voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT; | |
9bf0ef23 | 142 | const voltageMeasurandValue = getRandomFloatFluctuatedRounded( |
78085c42 | 143 | voltageSampledValueTemplateValue, |
5edd8ba0 | 144 | fluctuationPercent, |
78085c42 JB |
145 | ); |
146 | if ( | |
147 | chargingStation.getNumberOfPhases() !== 3 || | |
148 | (chargingStation.getNumberOfPhases() === 3 && chargingStation.getMainVoltageMeterValues()) | |
149 | ) { | |
150 | meterValue.sampledValue.push( | |
5edd8ba0 | 151 | OCPP16ServiceUtils.buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue), |
78085c42 JB |
152 | ); |
153 | } | |
154 | for ( | |
155 | let phase = 1; | |
156 | chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); | |
157 | phase++ | |
158 | ) { | |
159 | const phaseLineToNeutralValue = `L${phase}-N`; | |
160 | const voltagePhaseLineToNeutralSampledValueTemplate = | |
ed3d2808 | 161 | OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 162 | chargingStation, |
78085c42 JB |
163 | connectorId, |
164 | OCPP16MeterValueMeasurand.VOLTAGE, | |
5edd8ba0 | 165 | phaseLineToNeutralValue as OCPP16MeterValuePhase, |
78085c42 | 166 | ); |
e1d9a0f4 | 167 | let voltagePhaseLineToNeutralMeasurandValue: number | undefined; |
78085c42 JB |
168 | if (voltagePhaseLineToNeutralSampledValueTemplate) { |
169 | const voltagePhaseLineToNeutralSampledValueTemplateValue = | |
170 | voltagePhaseLineToNeutralSampledValueTemplate.value | |
171 | ? parseInt(voltagePhaseLineToNeutralSampledValueTemplate.value) | |
172 | : chargingStation.getVoltageOut(); | |
173 | const fluctuationPhaseToNeutralPercent = | |
174 | voltagePhaseLineToNeutralSampledValueTemplate.fluctuationPercent ?? | |
175 | Constants.DEFAULT_FLUCTUATION_PERCENT; | |
9bf0ef23 | 176 | voltagePhaseLineToNeutralMeasurandValue = getRandomFloatFluctuatedRounded( |
78085c42 | 177 | voltagePhaseLineToNeutralSampledValueTemplateValue, |
5edd8ba0 | 178 | fluctuationPhaseToNeutralPercent, |
78085c42 JB |
179 | ); |
180 | } | |
181 | meterValue.sampledValue.push( | |
182 | OCPP16ServiceUtils.buildSampledValue( | |
183 | voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate, | |
184 | voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue, | |
72092cfc | 185 | undefined, |
5edd8ba0 JB |
186 | phaseLineToNeutralValue as OCPP16MeterValuePhase, |
187 | ), | |
78085c42 JB |
188 | ); |
189 | if (chargingStation.getPhaseLineToLineVoltageMeterValues()) { | |
190 | const phaseLineToLineValue = `L${phase}-L${ | |
191 | (phase + 1) % chargingStation.getNumberOfPhases() !== 0 | |
192 | ? (phase + 1) % chargingStation.getNumberOfPhases() | |
193 | : chargingStation.getNumberOfPhases() | |
194 | }`; | |
195 | const voltagePhaseLineToLineSampledValueTemplate = | |
ed3d2808 | 196 | OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 197 | chargingStation, |
78085c42 JB |
198 | connectorId, |
199 | OCPP16MeterValueMeasurand.VOLTAGE, | |
5edd8ba0 | 200 | phaseLineToLineValue as OCPP16MeterValuePhase, |
78085c42 | 201 | ); |
e1d9a0f4 | 202 | let voltagePhaseLineToLineMeasurandValue: number | undefined; |
78085c42 JB |
203 | if (voltagePhaseLineToLineSampledValueTemplate) { |
204 | const voltagePhaseLineToLineSampledValueTemplateValue = | |
205 | voltagePhaseLineToLineSampledValueTemplate.value | |
206 | ? parseInt(voltagePhaseLineToLineSampledValueTemplate.value) | |
207 | : Voltage.VOLTAGE_400; | |
208 | const fluctuationPhaseLineToLinePercent = | |
209 | voltagePhaseLineToLineSampledValueTemplate.fluctuationPercent ?? | |
210 | Constants.DEFAULT_FLUCTUATION_PERCENT; | |
9bf0ef23 | 211 | voltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded( |
78085c42 | 212 | voltagePhaseLineToLineSampledValueTemplateValue, |
5edd8ba0 | 213 | fluctuationPhaseLineToLinePercent, |
78085c42 JB |
214 | ); |
215 | } | |
9bf0ef23 | 216 | const defaultVoltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded( |
78085c42 | 217 | Voltage.VOLTAGE_400, |
5edd8ba0 | 218 | fluctuationPercent, |
78085c42 JB |
219 | ); |
220 | meterValue.sampledValue.push( | |
221 | OCPP16ServiceUtils.buildSampledValue( | |
222 | voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate, | |
223 | voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue, | |
72092cfc | 224 | undefined, |
5edd8ba0 JB |
225 | phaseLineToLineValue as OCPP16MeterValuePhase, |
226 | ), | |
78085c42 JB |
227 | ); |
228 | } | |
229 | } | |
230 | } | |
231 | // Power.Active.Import measurand | |
ed3d2808 | 232 | const powerSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 233 | chargingStation, |
78085c42 | 234 | connectorId, |
5edd8ba0 | 235 | OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT, |
78085c42 | 236 | ); |
abe9e9dd | 237 | let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {}; |
78085c42 JB |
238 | if (chargingStation.getNumberOfPhases() === 3) { |
239 | powerPerPhaseSampledValueTemplates = { | |
ed3d2808 | 240 | L1: OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 241 | chargingStation, |
78085c42 JB |
242 | connectorId, |
243 | OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT, | |
5edd8ba0 | 244 | OCPP16MeterValuePhase.L1_N, |
78085c42 | 245 | ), |
ed3d2808 | 246 | L2: OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 247 | chargingStation, |
78085c42 JB |
248 | connectorId, |
249 | OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT, | |
5edd8ba0 | 250 | OCPP16MeterValuePhase.L2_N, |
78085c42 | 251 | ), |
ed3d2808 | 252 | L3: OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 253 | chargingStation, |
78085c42 JB |
254 | connectorId, |
255 | OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT, | |
5edd8ba0 | 256 | OCPP16MeterValuePhase.L3_N, |
78085c42 JB |
257 | ), |
258 | }; | |
259 | } | |
260 | if (powerSampledValueTemplate) { | |
261 | OCPP16ServiceUtils.checkMeasurandPowerDivider( | |
262 | chargingStation, | |
e1d9a0f4 | 263 | powerSampledValueTemplate.measurand!, |
78085c42 | 264 | ); |
fc040c43 | 265 | const errMsg = `MeterValues measurand ${ |
78085c42 JB |
266 | powerSampledValueTemplate.measurand ?? |
267 | OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
268 | }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${ | |
2484ac1e | 269 | chargingStation.templateFile |
78085c42 JB |
270 | }, cannot calculate ${ |
271 | powerSampledValueTemplate.measurand ?? | |
272 | OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
273 | } measurand value`; | |
e1d9a0f4 | 274 | const powerMeasurandValues: MeasurandValues = {} as MeasurandValues; |
78085c42 | 275 | const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1; |
1b6498ba JB |
276 | const connectorMaximumAvailablePower = |
277 | chargingStation.getConnectorMaximumAvailablePower(connectorId); | |
278 | const connectorMaximumPower = Math.round(connectorMaximumAvailablePower); | |
ad8537a7 | 279 | const connectorMaximumPowerPerPhase = Math.round( |
5edd8ba0 | 280 | connectorMaximumAvailablePower / chargingStation.getNumberOfPhases(), |
78085c42 | 281 | ); |
e1d9a0f4 | 282 | const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue!) ?? 0; |
860ef183 | 283 | const connectorMinimumPowerPerPhase = Math.round( |
5edd8ba0 | 284 | connectorMinimumPower / chargingStation.getNumberOfPhases(), |
860ef183 | 285 | ); |
78085c42 JB |
286 | switch (chargingStation.getCurrentOutType()) { |
287 | case CurrentType.AC: | |
288 | if (chargingStation.getNumberOfPhases() === 3) { | |
289 | const defaultFluctuatedPowerPerPhase = | |
290 | powerSampledValueTemplate.value && | |
9bf0ef23 | 291 | getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
292 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
293 | powerSampledValueTemplate.value, | |
294 | connectorMaximumPower / unitDivider, | |
5edd8ba0 | 295 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
34464008 | 296 | ) / chargingStation.getNumberOfPhases(), |
78085c42 | 297 | powerSampledValueTemplate.fluctuationPercent ?? |
5edd8ba0 | 298 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 JB |
299 | ); |
300 | const phase1FluctuatedValue = | |
e1d9a0f4 | 301 | powerPerPhaseSampledValueTemplates.L1?.value && |
9bf0ef23 | 302 | getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
303 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
304 | powerPerPhaseSampledValueTemplates.L1.value, | |
305 | connectorMaximumPowerPerPhase / unitDivider, | |
5edd8ba0 | 306 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
34464008 | 307 | ), |
78085c42 | 308 | powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ?? |
5edd8ba0 | 309 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 JB |
310 | ); |
311 | const phase2FluctuatedValue = | |
e1d9a0f4 | 312 | powerPerPhaseSampledValueTemplates.L2?.value && |
9bf0ef23 | 313 | getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
314 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
315 | powerPerPhaseSampledValueTemplates.L2.value, | |
316 | connectorMaximumPowerPerPhase / unitDivider, | |
5edd8ba0 | 317 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
34464008 | 318 | ), |
78085c42 | 319 | powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ?? |
5edd8ba0 | 320 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 JB |
321 | ); |
322 | const phase3FluctuatedValue = | |
e1d9a0f4 | 323 | powerPerPhaseSampledValueTemplates.L3?.value && |
9bf0ef23 | 324 | getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
325 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
326 | powerPerPhaseSampledValueTemplates.L3.value, | |
327 | connectorMaximumPowerPerPhase / unitDivider, | |
5edd8ba0 | 328 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
34464008 | 329 | ), |
78085c42 | 330 | powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ?? |
5edd8ba0 | 331 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 JB |
332 | ); |
333 | powerMeasurandValues.L1 = | |
e1d9a0f4 JB |
334 | (phase1FluctuatedValue as number) ?? |
335 | (defaultFluctuatedPowerPerPhase as number) ?? | |
9bf0ef23 | 336 | getRandomFloatRounded( |
860ef183 | 337 | connectorMaximumPowerPerPhase / unitDivider, |
5edd8ba0 | 338 | connectorMinimumPowerPerPhase / unitDivider, |
860ef183 | 339 | ); |
78085c42 | 340 | powerMeasurandValues.L2 = |
e1d9a0f4 JB |
341 | (phase2FluctuatedValue as number) ?? |
342 | (defaultFluctuatedPowerPerPhase as number) ?? | |
9bf0ef23 | 343 | getRandomFloatRounded( |
860ef183 | 344 | connectorMaximumPowerPerPhase / unitDivider, |
5edd8ba0 | 345 | connectorMinimumPowerPerPhase / unitDivider, |
860ef183 | 346 | ); |
78085c42 | 347 | powerMeasurandValues.L3 = |
e1d9a0f4 JB |
348 | (phase3FluctuatedValue as number) ?? |
349 | (defaultFluctuatedPowerPerPhase as number) ?? | |
9bf0ef23 | 350 | getRandomFloatRounded( |
860ef183 | 351 | connectorMaximumPowerPerPhase / unitDivider, |
5edd8ba0 | 352 | connectorMinimumPowerPerPhase / unitDivider, |
860ef183 | 353 | ); |
78085c42 JB |
354 | } else { |
355 | powerMeasurandValues.L1 = powerSampledValueTemplate.value | |
9bf0ef23 | 356 | ? getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
357 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
358 | powerSampledValueTemplate.value, | |
359 | connectorMaximumPower / unitDivider, | |
5edd8ba0 | 360 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
34464008 | 361 | ), |
78085c42 | 362 | powerSampledValueTemplate.fluctuationPercent ?? |
5edd8ba0 | 363 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 | 364 | ) |
9bf0ef23 | 365 | : getRandomFloatRounded( |
860ef183 | 366 | connectorMaximumPower / unitDivider, |
5edd8ba0 | 367 | connectorMinimumPower / unitDivider, |
860ef183 | 368 | ); |
78085c42 JB |
369 | powerMeasurandValues.L2 = 0; |
370 | powerMeasurandValues.L3 = 0; | |
371 | } | |
9bf0ef23 | 372 | powerMeasurandValues.allPhases = roundTo( |
78085c42 | 373 | powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3, |
5edd8ba0 | 374 | 2, |
78085c42 JB |
375 | ); |
376 | break; | |
377 | case CurrentType.DC: | |
378 | powerMeasurandValues.allPhases = powerSampledValueTemplate.value | |
9bf0ef23 | 379 | ? getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
380 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
381 | powerSampledValueTemplate.value, | |
382 | connectorMaximumPower / unitDivider, | |
5edd8ba0 | 383 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
34464008 | 384 | ), |
78085c42 | 385 | powerSampledValueTemplate.fluctuationPercent ?? |
5edd8ba0 | 386 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 | 387 | ) |
9bf0ef23 | 388 | : getRandomFloatRounded( |
860ef183 | 389 | connectorMaximumPower / unitDivider, |
5edd8ba0 | 390 | connectorMinimumPower / unitDivider, |
860ef183 | 391 | ); |
78085c42 JB |
392 | break; |
393 | default: | |
fc040c43 | 394 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`); |
78085c42 JB |
395 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); |
396 | } | |
397 | meterValue.sampledValue.push( | |
398 | OCPP16ServiceUtils.buildSampledValue( | |
399 | powerSampledValueTemplate, | |
5edd8ba0 JB |
400 | powerMeasurandValues.allPhases, |
401 | ), | |
78085c42 JB |
402 | ); |
403 | const sampledValuesIndex = meterValue.sampledValue.length - 1; | |
9bf0ef23 JB |
404 | const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2); |
405 | const connectorMinimumPowerRounded = roundTo(connectorMinimumPower / unitDivider, 2); | |
78085c42 | 406 | if ( |
9bf0ef23 | 407 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > |
ad8537a7 | 408 | connectorMaximumPowerRounded || |
9bf0ef23 | 409 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) < |
860ef183 | 410 | connectorMinimumPowerRounded || |
78085c42 JB |
411 | debug |
412 | ) { | |
413 | logger.error( | |
414 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
415 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
416 | OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
5edd8ba0 | 417 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerRounded}/${ |
78085c42 | 418 | meterValue.sampledValue[sampledValuesIndex].value |
5edd8ba0 | 419 | }/${connectorMaximumPowerRounded}`, |
78085c42 JB |
420 | ); |
421 | } | |
422 | for ( | |
423 | let phase = 1; | |
424 | chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); | |
425 | phase++ | |
426 | ) { | |
427 | const phaseValue = `L${phase}-N`; | |
428 | meterValue.sampledValue.push( | |
429 | OCPP16ServiceUtils.buildSampledValue( | |
a37fc6dc JB |
430 | powerPerPhaseSampledValueTemplates[ |
431 | `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates | |
432 | ]! ?? powerSampledValueTemplate, | |
433 | powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates], | |
72092cfc | 434 | undefined, |
5edd8ba0 JB |
435 | phaseValue as OCPP16MeterValuePhase, |
436 | ), | |
78085c42 JB |
437 | ); |
438 | const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1; | |
9bf0ef23 | 439 | const connectorMaximumPowerPerPhaseRounded = roundTo( |
ad8537a7 | 440 | connectorMaximumPowerPerPhase / unitDivider, |
5edd8ba0 | 441 | 2, |
ad8537a7 | 442 | ); |
9bf0ef23 | 443 | const connectorMinimumPowerPerPhaseRounded = roundTo( |
860ef183 | 444 | connectorMinimumPowerPerPhase / unitDivider, |
5edd8ba0 | 445 | 2, |
860ef183 | 446 | ); |
78085c42 | 447 | if ( |
9bf0ef23 | 448 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > |
ad8537a7 | 449 | connectorMaximumPowerPerPhaseRounded || |
9bf0ef23 | 450 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) < |
860ef183 | 451 | connectorMinimumPowerPerPhaseRounded || |
78085c42 JB |
452 | debug |
453 | ) { | |
454 | logger.error( | |
455 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
456 | meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ?? | |
457 | OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
458 | }: phase ${ | |
459 | meterValue.sampledValue[sampledValuesPerPhaseIndex].phase | |
5edd8ba0 | 460 | }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${ |
78085c42 | 461 | meterValue.sampledValue[sampledValuesPerPhaseIndex].value |
5edd8ba0 | 462 | }/${connectorMaximumPowerPerPhaseRounded}`, |
78085c42 JB |
463 | ); |
464 | } | |
465 | } | |
466 | } | |
467 | // Current.Import measurand | |
ed3d2808 | 468 | const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 469 | chargingStation, |
78085c42 | 470 | connectorId, |
5edd8ba0 | 471 | OCPP16MeterValueMeasurand.CURRENT_IMPORT, |
78085c42 | 472 | ); |
abe9e9dd | 473 | let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {}; |
78085c42 JB |
474 | if (chargingStation.getNumberOfPhases() === 3) { |
475 | currentPerPhaseSampledValueTemplates = { | |
ed3d2808 | 476 | L1: OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 477 | chargingStation, |
78085c42 JB |
478 | connectorId, |
479 | OCPP16MeterValueMeasurand.CURRENT_IMPORT, | |
5edd8ba0 | 480 | OCPP16MeterValuePhase.L1, |
78085c42 | 481 | ), |
ed3d2808 | 482 | L2: OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 483 | chargingStation, |
78085c42 JB |
484 | connectorId, |
485 | OCPP16MeterValueMeasurand.CURRENT_IMPORT, | |
5edd8ba0 | 486 | OCPP16MeterValuePhase.L2, |
78085c42 | 487 | ), |
ed3d2808 | 488 | L3: OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 489 | chargingStation, |
78085c42 JB |
490 | connectorId, |
491 | OCPP16MeterValueMeasurand.CURRENT_IMPORT, | |
5edd8ba0 | 492 | OCPP16MeterValuePhase.L3, |
78085c42 JB |
493 | ), |
494 | }; | |
495 | } | |
496 | if (currentSampledValueTemplate) { | |
497 | OCPP16ServiceUtils.checkMeasurandPowerDivider( | |
498 | chargingStation, | |
e1d9a0f4 | 499 | currentSampledValueTemplate.measurand!, |
78085c42 | 500 | ); |
fc040c43 | 501 | const errMsg = `MeterValues measurand ${ |
78085c42 JB |
502 | currentSampledValueTemplate.measurand ?? |
503 | OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
504 | }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${ | |
2484ac1e | 505 | chargingStation.templateFile |
78085c42 JB |
506 | }, cannot calculate ${ |
507 | currentSampledValueTemplate.measurand ?? | |
508 | OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
509 | } measurand value`; | |
abe9e9dd | 510 | const currentMeasurandValues: MeasurandValues = {} as MeasurandValues; |
1b6498ba JB |
511 | const connectorMaximumAvailablePower = |
512 | chargingStation.getConnectorMaximumAvailablePower(connectorId); | |
860ef183 | 513 | const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0; |
ad8537a7 | 514 | let connectorMaximumAmperage: number; |
78085c42 JB |
515 | switch (chargingStation.getCurrentOutType()) { |
516 | case CurrentType.AC: | |
ad8537a7 | 517 | connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower( |
78085c42 | 518 | chargingStation.getNumberOfPhases(), |
1b6498ba | 519 | connectorMaximumAvailablePower, |
5edd8ba0 | 520 | chargingStation.getVoltageOut(), |
78085c42 JB |
521 | ); |
522 | if (chargingStation.getNumberOfPhases() === 3) { | |
523 | const defaultFluctuatedAmperagePerPhase = | |
524 | currentSampledValueTemplate.value && | |
9bf0ef23 | 525 | getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
526 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
527 | currentSampledValueTemplate.value, | |
528 | connectorMaximumAmperage, | |
5edd8ba0 | 529 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
7bc31f9c | 530 | ), |
78085c42 | 531 | currentSampledValueTemplate.fluctuationPercent ?? |
5edd8ba0 | 532 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 JB |
533 | ); |
534 | const phase1FluctuatedValue = | |
e1d9a0f4 | 535 | currentPerPhaseSampledValueTemplates.L1?.value && |
9bf0ef23 | 536 | getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
537 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
538 | currentPerPhaseSampledValueTemplates.L1.value, | |
539 | connectorMaximumAmperage, | |
5edd8ba0 | 540 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
34464008 | 541 | ), |
78085c42 | 542 | currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ?? |
5edd8ba0 | 543 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 JB |
544 | ); |
545 | const phase2FluctuatedValue = | |
e1d9a0f4 | 546 | currentPerPhaseSampledValueTemplates.L2?.value && |
9bf0ef23 | 547 | getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
548 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
549 | currentPerPhaseSampledValueTemplates.L2.value, | |
550 | connectorMaximumAmperage, | |
5edd8ba0 | 551 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
34464008 | 552 | ), |
78085c42 | 553 | currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ?? |
5edd8ba0 | 554 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 JB |
555 | ); |
556 | const phase3FluctuatedValue = | |
e1d9a0f4 | 557 | currentPerPhaseSampledValueTemplates.L3?.value && |
9bf0ef23 | 558 | getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
559 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
560 | currentPerPhaseSampledValueTemplates.L3.value, | |
561 | connectorMaximumAmperage, | |
5edd8ba0 | 562 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
34464008 | 563 | ), |
78085c42 | 564 | currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ?? |
5edd8ba0 | 565 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 JB |
566 | ); |
567 | currentMeasurandValues.L1 = | |
e1d9a0f4 JB |
568 | (phase1FluctuatedValue as number) ?? |
569 | (defaultFluctuatedAmperagePerPhase as number) ?? | |
9bf0ef23 | 570 | getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); |
78085c42 | 571 | currentMeasurandValues.L2 = |
e1d9a0f4 JB |
572 | (phase2FluctuatedValue as number) ?? |
573 | (defaultFluctuatedAmperagePerPhase as number) ?? | |
9bf0ef23 | 574 | getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); |
78085c42 | 575 | currentMeasurandValues.L3 = |
e1d9a0f4 JB |
576 | (phase3FluctuatedValue as number) ?? |
577 | (defaultFluctuatedAmperagePerPhase as number) ?? | |
9bf0ef23 | 578 | getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); |
78085c42 JB |
579 | } else { |
580 | currentMeasurandValues.L1 = currentSampledValueTemplate.value | |
9bf0ef23 | 581 | ? getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
582 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
583 | currentSampledValueTemplate.value, | |
584 | connectorMaximumAmperage, | |
5edd8ba0 | 585 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
7bc31f9c | 586 | ), |
78085c42 | 587 | currentSampledValueTemplate.fluctuationPercent ?? |
5edd8ba0 | 588 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 | 589 | ) |
9bf0ef23 | 590 | : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); |
78085c42 JB |
591 | currentMeasurandValues.L2 = 0; |
592 | currentMeasurandValues.L3 = 0; | |
593 | } | |
9bf0ef23 | 594 | currentMeasurandValues.allPhases = roundTo( |
78085c42 JB |
595 | (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) / |
596 | chargingStation.getNumberOfPhases(), | |
5edd8ba0 | 597 | 2, |
78085c42 JB |
598 | ); |
599 | break; | |
600 | case CurrentType.DC: | |
ad8537a7 | 601 | connectorMaximumAmperage = DCElectricUtils.amperage( |
1b6498ba | 602 | connectorMaximumAvailablePower, |
5edd8ba0 | 603 | chargingStation.getVoltageOut(), |
78085c42 JB |
604 | ); |
605 | currentMeasurandValues.allPhases = currentSampledValueTemplate.value | |
9bf0ef23 | 606 | ? getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
607 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
608 | currentSampledValueTemplate.value, | |
609 | connectorMaximumAmperage, | |
5edd8ba0 | 610 | { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }, |
7bc31f9c | 611 | ), |
78085c42 | 612 | currentSampledValueTemplate.fluctuationPercent ?? |
5edd8ba0 | 613 | Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 | 614 | ) |
9bf0ef23 | 615 | : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); |
78085c42 JB |
616 | break; |
617 | default: | |
fc040c43 | 618 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`); |
78085c42 JB |
619 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); |
620 | } | |
621 | meterValue.sampledValue.push( | |
622 | OCPP16ServiceUtils.buildSampledValue( | |
623 | currentSampledValueTemplate, | |
5edd8ba0 JB |
624 | currentMeasurandValues.allPhases, |
625 | ), | |
78085c42 JB |
626 | ); |
627 | const sampledValuesIndex = meterValue.sampledValue.length - 1; | |
628 | if ( | |
9bf0ef23 | 629 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > |
ad8537a7 | 630 | connectorMaximumAmperage || |
9bf0ef23 | 631 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) < |
860ef183 | 632 | connectorMinimumAmperage || |
78085c42 JB |
633 | debug |
634 | ) { | |
635 | logger.error( | |
636 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
637 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
638 | OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
5edd8ba0 | 639 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${ |
78085c42 | 640 | meterValue.sampledValue[sampledValuesIndex].value |
5edd8ba0 | 641 | }/${connectorMaximumAmperage}`, |
78085c42 JB |
642 | ); |
643 | } | |
644 | for ( | |
645 | let phase = 1; | |
646 | chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); | |
647 | phase++ | |
648 | ) { | |
649 | const phaseValue = `L${phase}`; | |
650 | meterValue.sampledValue.push( | |
651 | OCPP16ServiceUtils.buildSampledValue( | |
a37fc6dc JB |
652 | currentPerPhaseSampledValueTemplates[ |
653 | phaseValue as keyof MeasurandPerPhaseSampledValueTemplates | |
654 | ]! ?? currentSampledValueTemplate, | |
655 | currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates], | |
72092cfc | 656 | undefined, |
5edd8ba0 JB |
657 | phaseValue as OCPP16MeterValuePhase, |
658 | ), | |
78085c42 JB |
659 | ); |
660 | const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1; | |
661 | if ( | |
9bf0ef23 | 662 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > |
ad8537a7 | 663 | connectorMaximumAmperage || |
9bf0ef23 | 664 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) < |
860ef183 | 665 | connectorMinimumAmperage || |
78085c42 JB |
666 | debug |
667 | ) { | |
668 | logger.error( | |
669 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
670 | meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ?? | |
671 | OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
672 | }: phase ${ | |
673 | meterValue.sampledValue[sampledValuesPerPhaseIndex].phase | |
5edd8ba0 | 674 | }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${ |
78085c42 | 675 | meterValue.sampledValue[sampledValuesPerPhaseIndex].value |
5edd8ba0 | 676 | }/${connectorMaximumAmperage}`, |
78085c42 JB |
677 | ); |
678 | } | |
679 | } | |
680 | } | |
681 | // Energy.Active.Import.Register measurand (default) | |
ed3d2808 | 682 | const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 683 | chargingStation, |
5edd8ba0 | 684 | connectorId, |
492cf6ab | 685 | ); |
78085c42 JB |
686 | if (energySampledValueTemplate) { |
687 | OCPP16ServiceUtils.checkMeasurandPowerDivider( | |
688 | chargingStation, | |
e1d9a0f4 | 689 | energySampledValueTemplate.measurand!, |
78085c42 JB |
690 | ); |
691 | const unitDivider = | |
692 | energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; | |
1b6498ba JB |
693 | const connectorMaximumAvailablePower = |
694 | chargingStation.getConnectorMaximumAvailablePower(connectorId); | |
9bf0ef23 | 695 | const connectorMaximumEnergyRounded = roundTo( |
1b6498ba | 696 | (connectorMaximumAvailablePower * interval) / (3600 * 1000), |
5edd8ba0 | 697 | 2, |
78085c42 JB |
698 | ); |
699 | const energyValueRounded = energySampledValueTemplate.value | |
700 | ? // Cumulate the fluctuated value around the static one | |
9bf0ef23 | 701 | getRandomFloatFluctuatedRounded( |
7bc31f9c JB |
702 | OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( |
703 | energySampledValueTemplate.value, | |
704 | connectorMaximumEnergyRounded, | |
705 | { | |
706 | limitationEnabled: chargingStation.getCustomValueLimitationMeterValues(), | |
707 | unitMultiplier: unitDivider, | |
5edd8ba0 | 708 | }, |
34464008 | 709 | ), |
5edd8ba0 | 710 | energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT, |
78085c42 | 711 | ) |
9bf0ef23 | 712 | : getRandomFloatRounded(connectorMaximumEnergyRounded); |
78085c42 | 713 | // Persist previous value on connector |
e1d9a0f4 JB |
714 | if (connector) { |
715 | if ( | |
716 | isNullOrUndefined(connector.energyActiveImportRegisterValue) === false && | |
717 | connector.energyActiveImportRegisterValue! >= 0 && | |
718 | isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false && | |
719 | connector.transactionEnergyActiveImportRegisterValue! >= 0 | |
720 | ) { | |
721 | connector.energyActiveImportRegisterValue! += energyValueRounded; | |
722 | connector.transactionEnergyActiveImportRegisterValue! += energyValueRounded; | |
723 | } else { | |
724 | connector.energyActiveImportRegisterValue = 0; | |
725 | connector.transactionEnergyActiveImportRegisterValue = 0; | |
726 | } | |
78085c42 JB |
727 | } |
728 | meterValue.sampledValue.push( | |
729 | OCPP16ServiceUtils.buildSampledValue( | |
730 | energySampledValueTemplate, | |
9bf0ef23 | 731 | roundTo( |
78085c42 JB |
732 | chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) / |
733 | unitDivider, | |
5edd8ba0 JB |
734 | 2, |
735 | ), | |
736 | ), | |
78085c42 JB |
737 | ); |
738 | const sampledValuesIndex = meterValue.sampledValue.length - 1; | |
ad8537a7 | 739 | if (energyValueRounded > connectorMaximumEnergyRounded || debug) { |
78085c42 JB |
740 | logger.error( |
741 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
742 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
743 | OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
04c32a95 | 744 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`, |
78085c42 JB |
745 | ); |
746 | } | |
747 | } | |
748 | return meterValue; | |
749 | } | |
750 | ||
e7aeea18 JB |
751 | public static buildTransactionBeginMeterValue( |
752 | chargingStation: ChargingStation, | |
753 | connectorId: number, | |
5edd8ba0 | 754 | meterStart: number, |
e7aeea18 | 755 | ): OCPP16MeterValue { |
fd0c36fa | 756 | const meterValue: OCPP16MeterValue = { |
c38f0ced | 757 | timestamp: new Date(), |
fd0c36fa JB |
758 | sampledValue: [], |
759 | }; | |
9ccca265 | 760 | // Energy.Active.Import.Register measurand (default) |
ed3d2808 | 761 | const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 762 | chargingStation, |
5edd8ba0 | 763 | connectorId, |
492cf6ab | 764 | ); |
9ccca265 | 765 | const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; |
e7aeea18 JB |
766 | meterValue.sampledValue.push( |
767 | OCPP16ServiceUtils.buildSampledValue( | |
e1d9a0f4 | 768 | sampledValueTemplate!, |
9bf0ef23 | 769 | roundTo((meterStart ?? 0) / unitDivider, 4), |
5edd8ba0 JB |
770 | MeterValueContext.TRANSACTION_BEGIN, |
771 | ), | |
e7aeea18 | 772 | ); |
fd0c36fa JB |
773 | return meterValue; |
774 | } | |
775 | ||
e7aeea18 JB |
776 | public static buildTransactionEndMeterValue( |
777 | chargingStation: ChargingStation, | |
778 | connectorId: number, | |
5edd8ba0 | 779 | meterStop: number, |
e7aeea18 | 780 | ): OCPP16MeterValue { |
fd0c36fa | 781 | const meterValue: OCPP16MeterValue = { |
c38f0ced | 782 | timestamp: new Date(), |
fd0c36fa JB |
783 | sampledValue: [], |
784 | }; | |
9ccca265 | 785 | // Energy.Active.Import.Register measurand (default) |
ed3d2808 | 786 | const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( |
492cf6ab | 787 | chargingStation, |
5edd8ba0 | 788 | connectorId, |
492cf6ab | 789 | ); |
9ccca265 | 790 | const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; |
e7aeea18 JB |
791 | meterValue.sampledValue.push( |
792 | OCPP16ServiceUtils.buildSampledValue( | |
e1d9a0f4 | 793 | sampledValueTemplate!, |
9bf0ef23 | 794 | roundTo((meterStop ?? 0) / unitDivider, 4), |
5edd8ba0 JB |
795 | MeterValueContext.TRANSACTION_END, |
796 | ), | |
e7aeea18 | 797 | ); |
fd0c36fa JB |
798 | return meterValue; |
799 | } | |
800 | ||
e7aeea18 JB |
801 | public static buildTransactionDataMeterValues( |
802 | transactionBeginMeterValue: OCPP16MeterValue, | |
5edd8ba0 | 803 | transactionEndMeterValue: OCPP16MeterValue, |
e7aeea18 | 804 | ): OCPP16MeterValue[] { |
fd0c36fa JB |
805 | const meterValues: OCPP16MeterValue[] = []; |
806 | meterValues.push(transactionBeginMeterValue); | |
807 | meterValues.push(transactionEndMeterValue); | |
808 | return meterValues; | |
809 | } | |
7bc31f9c | 810 | |
d19b10a8 JB |
811 | public static remoteStopTransaction = async ( |
812 | chargingStation: ChargingStation, | |
813 | connectorId: number, | |
814 | ): Promise<GenericResponse> => { | |
815 | await OCPP16ServiceUtils.sendAndSetConnectorStatus( | |
816 | chargingStation, | |
817 | connectorId, | |
818 | OCPP16ChargePointStatus.Finishing, | |
819 | ); | |
820 | const stopResponse = await chargingStation.stopTransactionOnConnector( | |
821 | connectorId, | |
822 | OCPP16StopTransactionReason.REMOTE, | |
823 | ); | |
824 | if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) { | |
825 | return OCPP16Constants.OCPP_RESPONSE_ACCEPTED; | |
826 | } | |
827 | return OCPP16Constants.OCPP_RESPONSE_REJECTED; | |
828 | }; | |
829 | ||
366f75f6 JB |
830 | public static changeAvailability = async ( |
831 | chargingStation: ChargingStation, | |
225e32b0 | 832 | connectorIds: number[], |
366f75f6 JB |
833 | chargePointStatus: OCPP16ChargePointStatus, |
834 | availabilityType: OCPP16AvailabilityType, | |
835 | ): Promise<OCPP16ChangeAvailabilityResponse> => { | |
225e32b0 JB |
836 | const responses: OCPP16ChangeAvailabilityResponse[] = []; |
837 | for (const connectorId of connectorIds) { | |
838 | let response: OCPP16ChangeAvailabilityResponse = | |
839 | OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; | |
840 | const connectorStatus = chargingStation.getConnectorStatus(connectorId)!; | |
841 | if (connectorStatus?.transactionStarted === true) { | |
842 | response = OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED; | |
843 | } | |
844 | connectorStatus.availability = availabilityType; | |
845 | if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) { | |
846 | await OCPP16ServiceUtils.sendAndSetConnectorStatus( | |
847 | chargingStation, | |
848 | connectorId, | |
849 | chargePointStatus, | |
850 | ); | |
851 | } | |
852 | responses.push(response); | |
366f75f6 | 853 | } |
3b0ed034 | 854 | if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) { |
225e32b0 | 855 | return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED; |
366f75f6 | 856 | } |
225e32b0 | 857 | return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED; |
366f75f6 JB |
858 | }; |
859 | ||
ed3d2808 JB |
860 | public static setChargingProfile( |
861 | chargingStation: ChargingStation, | |
862 | connectorId: number, | |
5edd8ba0 | 863 | cp: OCPP16ChargingProfile, |
ed3d2808 | 864 | ): void { |
9bf0ef23 | 865 | if (isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) { |
ed3d2808 | 866 | logger.error( |
5edd8ba0 | 867 | `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`, |
ed3d2808 | 868 | ); |
e1d9a0f4 | 869 | chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []; |
ed3d2808 | 870 | } |
72092cfc JB |
871 | if ( |
872 | Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false | |
873 | ) { | |
ed3d2808 | 874 | logger.error( |
bbb55ee4 | 875 | `${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`, |
ed3d2808 | 876 | ); |
e1d9a0f4 | 877 | chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []; |
ed3d2808 JB |
878 | } |
879 | let cpReplaced = false; | |
9bf0ef23 | 880 | if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) { |
ed3d2808 JB |
881 | chargingStation |
882 | .getConnectorStatus(connectorId) | |
72092cfc | 883 | ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => { |
ed3d2808 JB |
884 | if ( |
885 | chargingProfile.chargingProfileId === cp.chargingProfileId || | |
886 | (chargingProfile.stackLevel === cp.stackLevel && | |
887 | chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose) | |
888 | ) { | |
e1d9a0f4 | 889 | chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp; |
ed3d2808 JB |
890 | cpReplaced = true; |
891 | } | |
892 | }); | |
893 | } | |
72092cfc | 894 | !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp); |
ed3d2808 JB |
895 | } |
896 | ||
73d87be1 JB |
897 | public static clearChargingProfiles = ( |
898 | chargingStation: ChargingStation, | |
899 | commandPayload: ClearChargingProfileRequest, | |
900 | chargingProfiles: OCPP16ChargingProfile[] | undefined, | |
901 | ): boolean => { | |
0d1f33ba | 902 | const { id, chargingProfilePurpose, stackLevel } = commandPayload; |
73d87be1 JB |
903 | let clearedCP = false; |
904 | if (isNotEmptyArray(chargingProfiles)) { | |
905 | chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => { | |
906 | let clearCurrentCP = false; | |
0d1f33ba | 907 | if (chargingProfile.chargingProfileId === id) { |
73d87be1 JB |
908 | clearCurrentCP = true; |
909 | } | |
0d1f33ba | 910 | if (!chargingProfilePurpose && chargingProfile.stackLevel === stackLevel) { |
73d87be1 JB |
911 | clearCurrentCP = true; |
912 | } | |
0d1f33ba | 913 | if (!stackLevel && chargingProfile.chargingProfilePurpose === chargingProfilePurpose) { |
73d87be1 JB |
914 | clearCurrentCP = true; |
915 | } | |
916 | if ( | |
0d1f33ba JB |
917 | chargingProfile.stackLevel === stackLevel && |
918 | chargingProfile.chargingProfilePurpose === chargingProfilePurpose | |
73d87be1 JB |
919 | ) { |
920 | clearCurrentCP = true; | |
921 | } | |
922 | if (clearCurrentCP) { | |
923 | chargingProfiles.splice(index, 1); | |
924 | logger.debug( | |
925 | `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`, | |
926 | chargingProfile, | |
927 | ); | |
928 | clearedCP = true; | |
929 | } | |
930 | }); | |
931 | } | |
932 | return clearedCP; | |
933 | }; | |
934 | ||
ef9e3b33 JB |
935 | public static composeChargingSchedules = ( |
936 | chargingSchedule1: OCPP16ChargingSchedule | undefined, | |
937 | chargingSchedule2: OCPP16ChargingSchedule | undefined, | |
938 | targetInterval: Interval, | |
939 | ): OCPP16ChargingSchedule | undefined => { | |
940 | if (!chargingSchedule1 && !chargingSchedule2) { | |
941 | return undefined; | |
942 | } | |
943 | if (chargingSchedule1 && !chargingSchedule2) { | |
944 | return OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1, targetInterval); | |
945 | } | |
946 | if (!chargingSchedule1 && chargingSchedule2) { | |
947 | return OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2, targetInterval); | |
948 | } | |
949 | const compositeChargingSchedule1: OCPP16ChargingSchedule | undefined = | |
950 | OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1!, targetInterval); | |
951 | const compositeChargingSchedule2: OCPP16ChargingSchedule | undefined = | |
952 | OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2!, targetInterval); | |
953 | const compositeChargingScheduleInterval1: Interval = { | |
954 | start: compositeChargingSchedule1!.startSchedule!, | |
955 | end: addSeconds( | |
956 | compositeChargingSchedule1!.startSchedule!, | |
957 | compositeChargingSchedule1!.duration!, | |
958 | ), | |
959 | }; | |
960 | const compositeChargingScheduleInterval2: Interval = { | |
961 | start: compositeChargingSchedule2!.startSchedule!, | |
962 | end: addSeconds( | |
963 | compositeChargingSchedule2!.startSchedule!, | |
964 | compositeChargingSchedule2!.duration!, | |
965 | ), | |
966 | }; | |
967 | if ( | |
968 | !areIntervalsOverlapping( | |
969 | compositeChargingScheduleInterval1, | |
970 | compositeChargingScheduleInterval2, | |
971 | ) | |
972 | ) { | |
973 | return { | |
974 | ...OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1!, targetInterval)!, | |
975 | ...OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2!, targetInterval)!, | |
976 | }; | |
977 | } | |
978 | // FIXME: Handle overlapping intervals | |
979 | }; | |
980 | ||
90aceaf6 JB |
981 | public static hasReservation = ( |
982 | chargingStation: ChargingStation, | |
983 | connectorId: number, | |
984 | idTag: string, | |
985 | ): boolean => { | |
986 | const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId); | |
987 | const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0); | |
988 | if ( | |
989 | (chargingStation.getConnectorStatus(connectorId)?.status === | |
990 | OCPP16ChargePointStatus.Reserved && | |
991 | connectorReservation && | |
56563a3c | 992 | !hasReservationExpired(connectorReservation) && |
90aceaf6 | 993 | // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing |
56563a3c | 994 | connectorReservation?.idTag === idTag) || |
90aceaf6 JB |
995 | (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved && |
996 | chargingStationReservation && | |
56563a3c JB |
997 | !hasReservationExpired(chargingStationReservation) && |
998 | chargingStationReservation?.idTag === idTag) | |
90aceaf6 | 999 | ) { |
88499f52 JB |
1000 | logger.debug( |
1001 | `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`, | |
1002 | connectorReservation ?? chargingStationReservation, | |
1003 | ); | |
56563a3c | 1004 | return true; |
90aceaf6 | 1005 | } |
56563a3c | 1006 | return false; |
90aceaf6 JB |
1007 | }; |
1008 | ||
1b271a54 JB |
1009 | public static parseJsonSchemaFile<T extends JsonType>( |
1010 | relativePath: string, | |
1011 | moduleName?: string, | |
5edd8ba0 | 1012 | methodName?: string, |
1b271a54 | 1013 | ): JSONSchemaType<T> { |
7164966d | 1014 | return super.parseJsonSchemaFile<T>( |
51022aa0 | 1015 | relativePath, |
1b271a54 JB |
1016 | OCPPVersion.VERSION_16, |
1017 | moduleName, | |
5edd8ba0 | 1018 | methodName, |
7164966d | 1019 | ); |
130783a7 JB |
1020 | } |
1021 | ||
ef9e3b33 JB |
1022 | private static composeChargingSchedule = ( |
1023 | chargingSchedule: OCPP16ChargingSchedule, | |
1024 | targetInterval: Interval, | |
1025 | ): OCPP16ChargingSchedule | undefined => { | |
1026 | const chargingScheduleInterval: Interval = { | |
1027 | start: chargingSchedule.startSchedule!, | |
1028 | end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!), | |
1029 | }; | |
1030 | if (areIntervalsOverlapping(chargingScheduleInterval, targetInterval)) { | |
1031 | chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod); | |
1032 | if (isBefore(chargingScheduleInterval.start, targetInterval.start)) { | |
1033 | return { | |
1034 | ...chargingSchedule, | |
1035 | startSchedule: targetInterval.start as Date, | |
1036 | duration: differenceInSeconds(chargingScheduleInterval.end, targetInterval.start as Date), | |
1037 | chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter( | |
1038 | (schedulePeriod, index) => { | |
1039 | if ( | |
1040 | isWithinInterval( | |
1041 | addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!, | |
1042 | targetInterval, | |
1043 | ) | |
1044 | ) { | |
1045 | return true; | |
1046 | } | |
1047 | if ( | |
1048 | index < chargingSchedule.chargingSchedulePeriod.length - 1 && | |
1049 | !isWithinInterval( | |
1050 | addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod), | |
1051 | targetInterval, | |
1052 | ) && | |
1053 | isWithinInterval( | |
1054 | addSeconds( | |
1055 | chargingScheduleInterval.start, | |
1056 | chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod, | |
1057 | ), | |
1058 | targetInterval, | |
1059 | ) | |
1060 | ) { | |
1061 | schedulePeriod.startPeriod = 0; | |
1062 | return true; | |
1063 | } | |
1064 | return false; | |
1065 | }, | |
1066 | ), | |
1067 | }; | |
1068 | } | |
1069 | if (isAfter(chargingScheduleInterval.end, targetInterval.end)) { | |
1070 | return { | |
1071 | ...chargingSchedule, | |
1072 | duration: differenceInSeconds(targetInterval.end as Date, chargingScheduleInterval.start), | |
1073 | chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) => | |
1074 | isWithinInterval( | |
1075 | addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!, | |
1076 | targetInterval, | |
1077 | ), | |
1078 | ), | |
1079 | }; | |
1080 | } | |
1081 | return chargingSchedule; | |
1082 | } | |
1083 | }; | |
1084 | ||
7bc31f9c JB |
1085 | private static buildSampledValue( |
1086 | sampledValueTemplate: SampledValueTemplate, | |
1087 | value: number, | |
1088 | context?: MeterValueContext, | |
5edd8ba0 | 1089 | phase?: OCPP16MeterValuePhase, |
7bc31f9c JB |
1090 | ): OCPP16SampledValue { |
1091 | const sampledValueValue = value ?? sampledValueTemplate?.value ?? null; | |
1092 | const sampledValueContext = context ?? sampledValueTemplate?.context ?? null; | |
1093 | const sampledValueLocation = | |
1094 | sampledValueTemplate?.location ?? | |
e1d9a0f4 | 1095 | OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate.measurand!); |
7bc31f9c JB |
1096 | const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null; |
1097 | return { | |
9bf0ef23 | 1098 | ...(!isNullOrUndefined(sampledValueTemplate.unit) && { |
7bc31f9c JB |
1099 | unit: sampledValueTemplate.unit, |
1100 | }), | |
9bf0ef23 JB |
1101 | ...(!isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }), |
1102 | ...(!isNullOrUndefined(sampledValueTemplate.measurand) && { | |
7bc31f9c JB |
1103 | measurand: sampledValueTemplate.measurand, |
1104 | }), | |
9bf0ef23 JB |
1105 | ...(!isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }), |
1106 | ...(!isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }), | |
1107 | ...(!isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }), | |
e1d9a0f4 | 1108 | } as OCPP16SampledValue; |
7bc31f9c JB |
1109 | } |
1110 | ||
1111 | private static checkMeasurandPowerDivider( | |
1112 | chargingStation: ChargingStation, | |
5edd8ba0 | 1113 | measurandType: OCPP16MeterValueMeasurand, |
7bc31f9c | 1114 | ): void { |
9bf0ef23 | 1115 | if (isUndefined(chargingStation.powerDivider)) { |
fc040c43 | 1116 | const errMsg = `MeterValues measurand ${ |
7bc31f9c JB |
1117 | measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER |
1118 | }: powerDivider is undefined`; | |
fc040c43 | 1119 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`); |
7bc31f9c | 1120 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); |
fa7bccf4 | 1121 | } else if (chargingStation?.powerDivider <= 0) { |
fc040c43 | 1122 | const errMsg = `MeterValues measurand ${ |
7bc31f9c | 1123 | measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER |
fa7bccf4 | 1124 | }: powerDivider have zero or below value ${chargingStation.powerDivider}`; |
fc040c43 | 1125 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`); |
7bc31f9c JB |
1126 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); |
1127 | } | |
1128 | } | |
1129 | ||
1130 | private static getMeasurandDefaultLocation( | |
5edd8ba0 | 1131 | measurandType: OCPP16MeterValueMeasurand, |
7bc31f9c JB |
1132 | ): MeterValueLocation | undefined { |
1133 | switch (measurandType) { | |
1134 | case OCPP16MeterValueMeasurand.STATE_OF_CHARGE: | |
1135 | return MeterValueLocation.EV; | |
1136 | } | |
1137 | } | |
1138 | ||
3b0ed034 JB |
1139 | // private static getMeasurandDefaultUnit( |
1140 | // measurandType: OCPP16MeterValueMeasurand, | |
1141 | // ): MeterValueUnit | undefined { | |
1142 | // switch (measurandType) { | |
1143 | // case OCPP16MeterValueMeasurand.CURRENT_EXPORT: | |
1144 | // case OCPP16MeterValueMeasurand.CURRENT_IMPORT: | |
1145 | // case OCPP16MeterValueMeasurand.CURRENT_OFFERED: | |
1146 | // return MeterValueUnit.AMP; | |
1147 | // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: | |
1148 | // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: | |
1149 | // return MeterValueUnit.WATT_HOUR; | |
1150 | // case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT: | |
1151 | // case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT: | |
1152 | // case OCPP16MeterValueMeasurand.POWER_OFFERED: | |
1153 | // return MeterValueUnit.WATT; | |
1154 | // case OCPP16MeterValueMeasurand.STATE_OF_CHARGE: | |
1155 | // return MeterValueUnit.PERCENT; | |
1156 | // case OCPP16MeterValueMeasurand.VOLTAGE: | |
1157 | // return MeterValueUnit.VOLT; | |
1158 | // } | |
1159 | // } | |
6ed92bc1 | 1160 | } |