Commit | Line | Data |
---|---|---|
66a7748d JB |
1 | import { readFileSync } from 'node:fs' |
2 | import { dirname, join } from 'node:path' | |
3 | import { fileURLToPath } from 'node:url' | |
7164966d | 4 | |
66a7748d JB |
5 | import type { DefinedError, ErrorObject, JSONSchemaType } from 'ajv' |
6 | import { isDate } from 'date-fns' | |
06ad945f | 7 | |
a6ef1ece JB |
8 | import { |
9 | type ChargingStation, | |
10 | getConfigurationKey, | |
66a7748d JB |
11 | getIdTagsFile |
12 | } from '../../charging-station/index.js' | |
13 | import { BaseError, OCPPError } from '../../exception/index.js' | |
6e939d9e | 14 | import { |
ae725be3 JB |
15 | AuthorizationStatus, |
16 | type AuthorizeRequest, | |
17 | type AuthorizeResponse, | |
268a74bb | 18 | ChargePointErrorCode, |
3e888c65 | 19 | ChargingStationEvents, |
268a74bb | 20 | type ConnectorStatusEnum, |
41f3983a | 21 | CurrentType, |
268a74bb JB |
22 | ErrorType, |
23 | FileType, | |
6e939d9e | 24 | IncomingRequestCommand, |
268a74bb | 25 | type JsonType, |
41f3983a JB |
26 | type MeasurandPerPhaseSampledValueTemplates, |
27 | type MeasurandValues, | |
6e939d9e | 28 | MessageTrigger, |
268a74bb | 29 | MessageType, |
41f3983a JB |
30 | type MeterValue, |
31 | MeterValueContext, | |
32 | MeterValueLocation, | |
268a74bb | 33 | MeterValueMeasurand, |
41f3983a JB |
34 | MeterValuePhase, |
35 | MeterValueUnit, | |
cc6845fc | 36 | type OCPP16ChargePointStatus, |
268a74bb | 37 | type OCPP16StatusNotificationRequest, |
cc6845fc | 38 | type OCPP20ConnectorStatusEnumType, |
268a74bb JB |
39 | type OCPP20StatusNotificationRequest, |
40 | OCPPVersion, | |
6e939d9e | 41 | RequestCommand, |
41f3983a | 42 | type SampledValue, |
268a74bb JB |
43 | type SampledValueTemplate, |
44 | StandardParametersKey, | |
6e939d9e | 45 | type StatusNotificationRequest, |
66a7748d JB |
46 | type StatusNotificationResponse |
47 | } from '../../types/index.js' | |
9bf0ef23 | 48 | import { |
41f3983a JB |
49 | ACElectricUtils, |
50 | Constants, | |
41f3983a JB |
51 | convertToFloat, |
52 | convertToInt, | |
4c3f6c20 | 53 | DCElectricUtils, |
41f3983a JB |
54 | getRandomFloatFluctuatedRounded, |
55 | getRandomFloatRounded, | |
56 | getRandomInteger, | |
9bf0ef23 JB |
57 | handleFileException, |
58 | isNotEmptyArray, | |
59 | isNotEmptyString, | |
9bf0ef23 | 60 | logger, |
4c3f6c20 | 61 | logPrefix, |
d71ce3fa | 62 | max, |
5adf6ca4 | 63 | min, |
66a7748d JB |
64 | roundTo |
65 | } from '../../utils/index.js' | |
4c3f6c20 JB |
66 | import { OCPP16Constants } from './1.6/OCPP16Constants.js' |
67 | import { OCPP20Constants } from './2.0/OCPP20Constants.js' | |
68 | import { OCPPConstants } from './OCPPConstants.js' | |
06ad945f | 69 | |
c510c989 | 70 | export const getMessageTypeString = (messageType: MessageType | undefined): string => { |
041365be JB |
71 | switch (messageType) { |
72 | case MessageType.CALL_MESSAGE: | |
66a7748d | 73 | return 'request' |
041365be | 74 | case MessageType.CALL_RESULT_MESSAGE: |
66a7748d | 75 | return 'response' |
041365be | 76 | case MessageType.CALL_ERROR_MESSAGE: |
66a7748d | 77 | return 'error' |
041365be | 78 | default: |
66a7748d | 79 | return 'unknown' |
041365be | 80 | } |
66a7748d | 81 | } |
041365be | 82 | |
dd21af15 | 83 | const buildStatusNotificationRequest = ( |
041365be JB |
84 | chargingStation: ChargingStation, |
85 | connectorId: number, | |
86 | status: ConnectorStatusEnum, | |
66a7748d | 87 | evseId?: number |
041365be JB |
88 | ): StatusNotificationRequest => { |
89 | switch (chargingStation.stationInfo?.ocppVersion) { | |
90 | case OCPPVersion.VERSION_16: | |
91 | return { | |
92 | connectorId, | |
cc6845fc | 93 | status: status as OCPP16ChargePointStatus, |
66a7748d JB |
94 | errorCode: ChargePointErrorCode.NO_ERROR |
95 | } satisfies OCPP16StatusNotificationRequest | |
041365be JB |
96 | case OCPPVersion.VERSION_20: |
97 | case OCPPVersion.VERSION_201: | |
98 | return { | |
99 | timestamp: new Date(), | |
cc6845fc | 100 | connectorStatus: status as OCPP20ConnectorStatusEnumType, |
041365be | 101 | connectorId, |
cc6845fc JB |
102 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
103 | evseId: evseId! | |
66a7748d | 104 | } satisfies OCPP20StatusNotificationRequest |
041365be | 105 | default: |
66a7748d | 106 | throw new BaseError('Cannot build status notification payload: OCPP version not supported') |
041365be | 107 | } |
66a7748d | 108 | } |
041365be JB |
109 | |
110 | export const isIdTagAuthorized = async ( | |
111 | chargingStation: ChargingStation, | |
112 | connectorId: number, | |
66a7748d | 113 | idTag: string |
041365be JB |
114 | ): Promise<boolean> => { |
115 | if ( | |
116 | !chargingStation.getLocalAuthListEnabled() && | |
66a7748d | 117 | chargingStation.stationInfo?.remoteAuthorization === false |
041365be JB |
118 | ) { |
119 | logger.warn( | |
66a7748d JB |
120 | `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur` |
121 | ) | |
041365be | 122 | } |
f938317f JB |
123 | const connectorStatus = chargingStation.getConnectorStatus(connectorId) |
124 | if ( | |
125 | connectorStatus != null && | |
126 | chargingStation.getLocalAuthListEnabled() && | |
127 | isIdTagLocalAuthorized(chargingStation, idTag) | |
128 | ) { | |
66a7748d JB |
129 | connectorStatus.localAuthorizeIdTag = idTag |
130 | connectorStatus.idTagLocalAuthorized = true | |
131 | return true | |
132 | } else if (chargingStation.stationInfo?.remoteAuthorization === true) { | |
133 | return await isIdTagRemoteAuthorized(chargingStation, connectorId, idTag) | |
041365be | 134 | } |
66a7748d JB |
135 | return false |
136 | } | |
041365be JB |
137 | |
138 | const isIdTagLocalAuthorized = (chargingStation: ChargingStation, idTag: string): boolean => { | |
139 | return ( | |
66a7748d | 140 | chargingStation.hasIdTags() && |
041365be JB |
141 | isNotEmptyString( |
142 | chargingStation.idTagsCache | |
66a7748d | 143 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
5199f9fd | 144 | .getIdTags(getIdTagsFile(chargingStation.stationInfo!)!) |
a974c8e4 | 145 | ?.find(tag => tag === idTag) |
041365be | 146 | ) |
66a7748d JB |
147 | ) |
148 | } | |
041365be JB |
149 | |
150 | const isIdTagRemoteAuthorized = async ( | |
151 | chargingStation: ChargingStation, | |
152 | connectorId: number, | |
66a7748d | 153 | idTag: string |
041365be | 154 | ): Promise<boolean> => { |
66a7748d JB |
155 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
156 | chargingStation.getConnectorStatus(connectorId)!.authorizeIdTag = idTag | |
041365be JB |
157 | return ( |
158 | ( | |
159 | await chargingStation.ocppRequestService.requestHandler<AuthorizeRequest, AuthorizeResponse>( | |
160 | chargingStation, | |
161 | RequestCommand.AUTHORIZE, | |
162 | { | |
66a7748d JB |
163 | idTag |
164 | } | |
041365be | 165 | ) |
5199f9fd | 166 | ).idTagInfo.status === AuthorizationStatus.ACCEPTED |
66a7748d JB |
167 | ) |
168 | } | |
041365be JB |
169 | |
170 | export const sendAndSetConnectorStatus = async ( | |
171 | chargingStation: ChargingStation, | |
172 | connectorId: number, | |
173 | status: ConnectorStatusEnum, | |
174 | evseId?: number, | |
66a7748d | 175 | options?: { send: boolean } |
041365be | 176 | ): Promise<void> => { |
66a7748d | 177 | options = { send: true, ...options } |
041365be | 178 | if (options.send) { |
66a7748d | 179 | checkConnectorStatusTransition(chargingStation, connectorId, status) |
041365be | 180 | await chargingStation.ocppRequestService.requestHandler< |
66a7748d JB |
181 | StatusNotificationRequest, |
182 | StatusNotificationResponse | |
041365be JB |
183 | >( |
184 | chargingStation, | |
185 | RequestCommand.STATUS_NOTIFICATION, | |
66a7748d JB |
186 | buildStatusNotificationRequest(chargingStation, connectorId, status, evseId) |
187 | ) | |
041365be | 188 | } |
66a7748d JB |
189 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
190 | chargingStation.getConnectorStatus(connectorId)!.status = status | |
041365be JB |
191 | chargingStation.emit(ChargingStationEvents.connectorStatusChanged, { |
192 | connectorId, | |
66a7748d JB |
193 | ...chargingStation.getConnectorStatus(connectorId) |
194 | }) | |
195 | } | |
041365be JB |
196 | |
197 | const checkConnectorStatusTransition = ( | |
198 | chargingStation: ChargingStation, | |
199 | connectorId: number, | |
66a7748d | 200 | status: ConnectorStatusEnum |
041365be | 201 | ): boolean => { |
f938317f | 202 | const fromStatus = chargingStation.getConnectorStatus(connectorId)?.status |
66a7748d | 203 | let transitionAllowed = false |
041365be JB |
204 | switch (chargingStation.stationInfo?.ocppVersion) { |
205 | case OCPPVersion.VERSION_16: | |
206 | if ( | |
207 | (connectorId === 0 && | |
208 | OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex( | |
a974c8e4 | 209 | transition => transition.from === fromStatus && transition.to === status |
041365be JB |
210 | ) !== -1) || |
211 | (connectorId > 0 && | |
212 | OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex( | |
a974c8e4 | 213 | transition => transition.from === fromStatus && transition.to === status |
041365be JB |
214 | ) !== -1) |
215 | ) { | |
66a7748d | 216 | transitionAllowed = true |
041365be | 217 | } |
66a7748d | 218 | break |
041365be JB |
219 | case OCPPVersion.VERSION_20: |
220 | case OCPPVersion.VERSION_201: | |
221 | if ( | |
222 | (connectorId === 0 && | |
223 | OCPP20Constants.ChargingStationStatusTransitions.findIndex( | |
a974c8e4 | 224 | transition => transition.from === fromStatus && transition.to === status |
041365be JB |
225 | ) !== -1) || |
226 | (connectorId > 0 && | |
227 | OCPP20Constants.ConnectorStatusTransitions.findIndex( | |
a974c8e4 | 228 | transition => transition.from === fromStatus && transition.to === status |
041365be JB |
229 | ) !== -1) |
230 | ) { | |
66a7748d | 231 | transitionAllowed = true |
041365be | 232 | } |
66a7748d | 233 | break |
041365be JB |
234 | default: |
235 | throw new BaseError( | |
66a7748d JB |
236 | `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported` |
237 | ) | |
041365be | 238 | } |
66a7748d | 239 | if (!transitionAllowed) { |
041365be | 240 | logger.warn( |
5199f9fd JB |
241 | `${chargingStation.logPrefix()} OCPP ${ |
242 | chargingStation.stationInfo.ocppVersion | |
a223d9be JB |
243 | } connector id ${connectorId} status transition from '${ |
244 | chargingStation.getConnectorStatus(connectorId)?.status | |
245 | }' to '${status}' is not allowed` | |
66a7748d | 246 | ) |
041365be | 247 | } |
66a7748d JB |
248 | return transitionAllowed |
249 | } | |
041365be | 250 | |
41f3983a JB |
251 | export const buildMeterValue = ( |
252 | chargingStation: ChargingStation, | |
253 | connectorId: number, | |
254 | transactionId: number, | |
255 | interval: number, | |
66a7748d | 256 | debug = false |
41f3983a | 257 | ): MeterValue => { |
66a7748d JB |
258 | const connector = chargingStation.getConnectorStatus(connectorId) |
259 | let meterValue: MeterValue | |
260 | let socSampledValueTemplate: SampledValueTemplate | undefined | |
261 | let voltageSampledValueTemplate: SampledValueTemplate | undefined | |
262 | let powerSampledValueTemplate: SampledValueTemplate | undefined | |
263 | let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {} | |
264 | let currentSampledValueTemplate: SampledValueTemplate | undefined | |
265 | let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {} | |
266 | let energySampledValueTemplate: SampledValueTemplate | undefined | |
41f3983a JB |
267 | switch (chargingStation.stationInfo?.ocppVersion) { |
268 | case OCPPVersion.VERSION_16: | |
269 | meterValue = { | |
270 | timestamp: new Date(), | |
66a7748d JB |
271 | sampledValue: [] |
272 | } | |
41f3983a JB |
273 | // SoC measurand |
274 | socSampledValueTemplate = getSampledValueTemplate( | |
275 | chargingStation, | |
276 | connectorId, | |
66a7748d JB |
277 | MeterValueMeasurand.STATE_OF_CHARGE |
278 | ) | |
279 | if (socSampledValueTemplate != null) { | |
280 | const socMaximumValue = 100 | |
281 | const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0 | |
41f3983a JB |
282 | const socSampledValueTemplateValue = isNotEmptyString(socSampledValueTemplate.value) |
283 | ? getRandomFloatFluctuatedRounded( | |
66a7748d JB |
284 | parseInt(socSampledValueTemplate.value), |
285 | socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT | |
286 | ) | |
287 | : getRandomInteger(socMaximumValue, socMinimumValue) | |
41f3983a | 288 | meterValue.sampledValue.push( |
66a7748d JB |
289 | buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue) |
290 | ) | |
291 | const sampledValuesIndex = meterValue.sampledValue.length - 1 | |
41f3983a JB |
292 | if ( |
293 | convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue || | |
294 | convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue || | |
295 | debug | |
296 | ) { | |
297 | logger.error( | |
298 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
299 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
300 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
301 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${socMinimumValue}/${ | |
302 | meterValue.sampledValue[sampledValuesIndex].value | |
66a7748d JB |
303 | }/${socMaximumValue}` |
304 | ) | |
41f3983a JB |
305 | } |
306 | } | |
307 | // Voltage measurand | |
308 | voltageSampledValueTemplate = getSampledValueTemplate( | |
309 | chargingStation, | |
310 | connectorId, | |
66a7748d JB |
311 | MeterValueMeasurand.VOLTAGE |
312 | ) | |
313 | if (voltageSampledValueTemplate != null) { | |
41f3983a JB |
314 | const voltageSampledValueTemplateValue = isNotEmptyString(voltageSampledValueTemplate.value) |
315 | ? parseInt(voltageSampledValueTemplate.value) | |
66a7748d JB |
316 | : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
317 | chargingStation.stationInfo.voltageOut! | |
41f3983a | 318 | const fluctuationPercent = |
66a7748d | 319 | voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT |
41f3983a JB |
320 | const voltageMeasurandValue = getRandomFloatFluctuatedRounded( |
321 | voltageSampledValueTemplateValue, | |
66a7748d JB |
322 | fluctuationPercent |
323 | ) | |
41f3983a JB |
324 | if ( |
325 | chargingStation.getNumberOfPhases() !== 3 || | |
326 | (chargingStation.getNumberOfPhases() === 3 && | |
5199f9fd | 327 | chargingStation.stationInfo.mainVoltageMeterValues === true) |
41f3983a JB |
328 | ) { |
329 | meterValue.sampledValue.push( | |
66a7748d JB |
330 | buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue) |
331 | ) | |
41f3983a JB |
332 | } |
333 | for ( | |
334 | let phase = 1; | |
335 | chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); | |
336 | phase++ | |
337 | ) { | |
66a7748d | 338 | const phaseLineToNeutralValue = `L${phase}-N` |
41f3983a JB |
339 | const voltagePhaseLineToNeutralSampledValueTemplate = getSampledValueTemplate( |
340 | chargingStation, | |
341 | connectorId, | |
342 | MeterValueMeasurand.VOLTAGE, | |
66a7748d JB |
343 | phaseLineToNeutralValue as MeterValuePhase |
344 | ) | |
345 | let voltagePhaseLineToNeutralMeasurandValue: number | undefined | |
346 | if (voltagePhaseLineToNeutralSampledValueTemplate != null) { | |
41f3983a | 347 | const voltagePhaseLineToNeutralSampledValueTemplateValue = isNotEmptyString( |
66a7748d | 348 | voltagePhaseLineToNeutralSampledValueTemplate.value |
41f3983a JB |
349 | ) |
350 | ? parseInt(voltagePhaseLineToNeutralSampledValueTemplate.value) | |
66a7748d JB |
351 | : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
352 | chargingStation.stationInfo.voltageOut! | |
41f3983a JB |
353 | const fluctuationPhaseToNeutralPercent = |
354 | voltagePhaseLineToNeutralSampledValueTemplate.fluctuationPercent ?? | |
66a7748d | 355 | Constants.DEFAULT_FLUCTUATION_PERCENT |
41f3983a JB |
356 | voltagePhaseLineToNeutralMeasurandValue = getRandomFloatFluctuatedRounded( |
357 | voltagePhaseLineToNeutralSampledValueTemplateValue, | |
66a7748d JB |
358 | fluctuationPhaseToNeutralPercent |
359 | ) | |
41f3983a JB |
360 | } |
361 | meterValue.sampledValue.push( | |
362 | buildSampledValue( | |
363 | voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate, | |
364 | voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue, | |
365 | undefined, | |
66a7748d JB |
366 | phaseLineToNeutralValue as MeterValuePhase |
367 | ) | |
368 | ) | |
5199f9fd | 369 | if (chargingStation.stationInfo.phaseLineToLineVoltageMeterValues === true) { |
41f3983a JB |
370 | const phaseLineToLineValue = `L${phase}-L${ |
371 | (phase + 1) % chargingStation.getNumberOfPhases() !== 0 | |
372 | ? (phase + 1) % chargingStation.getNumberOfPhases() | |
373 | : chargingStation.getNumberOfPhases() | |
66a7748d | 374 | }` |
41f3983a JB |
375 | const voltagePhaseLineToLineValueRounded = roundTo( |
376 | Math.sqrt(chargingStation.getNumberOfPhases()) * | |
66a7748d | 377 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
41f3983a | 378 | chargingStation.stationInfo.voltageOut!, |
66a7748d JB |
379 | 2 |
380 | ) | |
41f3983a JB |
381 | const voltagePhaseLineToLineSampledValueTemplate = getSampledValueTemplate( |
382 | chargingStation, | |
383 | connectorId, | |
384 | MeterValueMeasurand.VOLTAGE, | |
66a7748d JB |
385 | phaseLineToLineValue as MeterValuePhase |
386 | ) | |
387 | let voltagePhaseLineToLineMeasurandValue: number | undefined | |
388 | if (voltagePhaseLineToLineSampledValueTemplate != null) { | |
41f3983a | 389 | const voltagePhaseLineToLineSampledValueTemplateValue = isNotEmptyString( |
66a7748d | 390 | voltagePhaseLineToLineSampledValueTemplate.value |
41f3983a JB |
391 | ) |
392 | ? parseInt(voltagePhaseLineToLineSampledValueTemplate.value) | |
66a7748d | 393 | : voltagePhaseLineToLineValueRounded |
41f3983a JB |
394 | const fluctuationPhaseLineToLinePercent = |
395 | voltagePhaseLineToLineSampledValueTemplate.fluctuationPercent ?? | |
66a7748d | 396 | Constants.DEFAULT_FLUCTUATION_PERCENT |
41f3983a JB |
397 | voltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded( |
398 | voltagePhaseLineToLineSampledValueTemplateValue, | |
66a7748d JB |
399 | fluctuationPhaseLineToLinePercent |
400 | ) | |
41f3983a JB |
401 | } |
402 | const defaultVoltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded( | |
403 | voltagePhaseLineToLineValueRounded, | |
66a7748d JB |
404 | fluctuationPercent |
405 | ) | |
41f3983a JB |
406 | meterValue.sampledValue.push( |
407 | buildSampledValue( | |
408 | voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate, | |
409 | voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue, | |
410 | undefined, | |
66a7748d JB |
411 | phaseLineToLineValue as MeterValuePhase |
412 | ) | |
413 | ) | |
41f3983a JB |
414 | } |
415 | } | |
416 | } | |
417 | // Power.Active.Import measurand | |
418 | powerSampledValueTemplate = getSampledValueTemplate( | |
419 | chargingStation, | |
420 | connectorId, | |
66a7748d JB |
421 | MeterValueMeasurand.POWER_ACTIVE_IMPORT |
422 | ) | |
41f3983a JB |
423 | if (chargingStation.getNumberOfPhases() === 3) { |
424 | powerPerPhaseSampledValueTemplates = { | |
425 | L1: getSampledValueTemplate( | |
426 | chargingStation, | |
427 | connectorId, | |
428 | MeterValueMeasurand.POWER_ACTIVE_IMPORT, | |
66a7748d | 429 | MeterValuePhase.L1_N |
41f3983a JB |
430 | ), |
431 | L2: getSampledValueTemplate( | |
432 | chargingStation, | |
433 | connectorId, | |
434 | MeterValueMeasurand.POWER_ACTIVE_IMPORT, | |
66a7748d | 435 | MeterValuePhase.L2_N |
41f3983a JB |
436 | ), |
437 | L3: getSampledValueTemplate( | |
438 | chargingStation, | |
439 | connectorId, | |
440 | MeterValueMeasurand.POWER_ACTIVE_IMPORT, | |
66a7748d JB |
441 | MeterValuePhase.L3_N |
442 | ) | |
443 | } | |
41f3983a | 444 | } |
66a7748d | 445 | if (powerSampledValueTemplate != null) { |
5199f9fd | 446 | checkMeasurandPowerDivider(chargingStation, powerSampledValueTemplate.measurand) |
41f3983a JB |
447 | const errMsg = `MeterValues measurand ${ |
448 | powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
5199f9fd | 449 | }: Unknown ${chargingStation.stationInfo.currentOutType} currentOutType in template file ${ |
41f3983a JB |
450 | chargingStation.templateFile |
451 | }, cannot calculate ${ | |
452 | powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
66a7748d JB |
453 | } measurand value` |
454 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions | |
455 | const powerMeasurandValues: MeasurandValues = {} as MeasurandValues | |
5199f9fd | 456 | const unitDivider = powerSampledValueTemplate.unit === MeterValueUnit.KILO_WATT ? 1000 : 1 |
41f3983a | 457 | const connectorMaximumAvailablePower = |
66a7748d JB |
458 | chargingStation.getConnectorMaximumAvailablePower(connectorId) |
459 | const connectorMaximumPower = Math.round(connectorMaximumAvailablePower) | |
41f3983a | 460 | const connectorMaximumPowerPerPhase = Math.round( |
66a7748d JB |
461 | connectorMaximumAvailablePower / chargingStation.getNumberOfPhases() |
462 | ) | |
463 | const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue ?? 0) | |
41f3983a | 464 | const connectorMinimumPowerPerPhase = Math.round( |
66a7748d JB |
465 | connectorMinimumPower / chargingStation.getNumberOfPhases() |
466 | ) | |
5199f9fd | 467 | switch (chargingStation.stationInfo.currentOutType) { |
41f3983a JB |
468 | case CurrentType.AC: |
469 | if (chargingStation.getNumberOfPhases() === 3) { | |
470 | const defaultFluctuatedPowerPerPhase = isNotEmptyString( | |
66a7748d | 471 | powerSampledValueTemplate.value |
41f3983a JB |
472 | ) |
473 | ? getRandomFloatFluctuatedRounded( | |
66a7748d JB |
474 | getLimitFromSampledValueTemplateCustomValue( |
475 | powerSampledValueTemplate.value, | |
476 | connectorMaximumPower / unitDivider, | |
477 | connectorMinimumPower / unitDivider, | |
478 | { | |
479 | limitationEnabled: | |
5199f9fd | 480 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
481 | fallbackValue: connectorMinimumPower / unitDivider |
482 | } | |
483 | ) / chargingStation.getNumberOfPhases(), | |
484 | powerSampledValueTemplate.fluctuationPercent ?? | |
485 | Constants.DEFAULT_FLUCTUATION_PERCENT | |
486 | ) | |
487 | : undefined | |
41f3983a | 488 | const phase1FluctuatedValue = isNotEmptyString( |
66a7748d | 489 | powerPerPhaseSampledValueTemplates.L1?.value |
41f3983a JB |
490 | ) |
491 | ? getRandomFloatFluctuatedRounded( | |
66a7748d | 492 | getLimitFromSampledValueTemplateCustomValue( |
5dc7c990 | 493 | powerPerPhaseSampledValueTemplates.L1.value, |
66a7748d JB |
494 | connectorMaximumPowerPerPhase / unitDivider, |
495 | connectorMinimumPowerPerPhase / unitDivider, | |
496 | { | |
497 | limitationEnabled: | |
5199f9fd | 498 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
499 | fallbackValue: connectorMinimumPowerPerPhase / unitDivider |
500 | } | |
501 | ), | |
5dc7c990 | 502 | powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ?? |
66a7748d JB |
503 | Constants.DEFAULT_FLUCTUATION_PERCENT |
504 | ) | |
505 | : undefined | |
41f3983a | 506 | const phase2FluctuatedValue = isNotEmptyString( |
66a7748d | 507 | powerPerPhaseSampledValueTemplates.L2?.value |
41f3983a JB |
508 | ) |
509 | ? getRandomFloatFluctuatedRounded( | |
66a7748d | 510 | getLimitFromSampledValueTemplateCustomValue( |
5dc7c990 | 511 | powerPerPhaseSampledValueTemplates.L2.value, |
66a7748d JB |
512 | connectorMaximumPowerPerPhase / unitDivider, |
513 | connectorMinimumPowerPerPhase / unitDivider, | |
514 | { | |
515 | limitationEnabled: | |
5199f9fd | 516 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
517 | fallbackValue: connectorMinimumPowerPerPhase / unitDivider |
518 | } | |
519 | ), | |
5dc7c990 | 520 | powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ?? |
66a7748d JB |
521 | Constants.DEFAULT_FLUCTUATION_PERCENT |
522 | ) | |
523 | : undefined | |
41f3983a | 524 | const phase3FluctuatedValue = isNotEmptyString( |
66a7748d | 525 | powerPerPhaseSampledValueTemplates.L3?.value |
41f3983a JB |
526 | ) |
527 | ? getRandomFloatFluctuatedRounded( | |
66a7748d | 528 | getLimitFromSampledValueTemplateCustomValue( |
5dc7c990 | 529 | powerPerPhaseSampledValueTemplates.L3.value, |
66a7748d JB |
530 | connectorMaximumPowerPerPhase / unitDivider, |
531 | connectorMinimumPowerPerPhase / unitDivider, | |
532 | { | |
533 | limitationEnabled: | |
5199f9fd | 534 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
535 | fallbackValue: connectorMinimumPowerPerPhase / unitDivider |
536 | } | |
537 | ), | |
5dc7c990 | 538 | powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ?? |
66a7748d JB |
539 | Constants.DEFAULT_FLUCTUATION_PERCENT |
540 | ) | |
541 | : undefined | |
41f3983a JB |
542 | powerMeasurandValues.L1 = |
543 | phase1FluctuatedValue ?? | |
544 | defaultFluctuatedPowerPerPhase ?? | |
545 | getRandomFloatRounded( | |
546 | connectorMaximumPowerPerPhase / unitDivider, | |
66a7748d JB |
547 | connectorMinimumPowerPerPhase / unitDivider |
548 | ) | |
41f3983a JB |
549 | powerMeasurandValues.L2 = |
550 | phase2FluctuatedValue ?? | |
551 | defaultFluctuatedPowerPerPhase ?? | |
552 | getRandomFloatRounded( | |
553 | connectorMaximumPowerPerPhase / unitDivider, | |
66a7748d JB |
554 | connectorMinimumPowerPerPhase / unitDivider |
555 | ) | |
41f3983a JB |
556 | powerMeasurandValues.L3 = |
557 | phase3FluctuatedValue ?? | |
558 | defaultFluctuatedPowerPerPhase ?? | |
559 | getRandomFloatRounded( | |
560 | connectorMaximumPowerPerPhase / unitDivider, | |
66a7748d JB |
561 | connectorMinimumPowerPerPhase / unitDivider |
562 | ) | |
41f3983a JB |
563 | } else { |
564 | powerMeasurandValues.L1 = isNotEmptyString(powerSampledValueTemplate.value) | |
565 | ? getRandomFloatFluctuatedRounded( | |
41f3983a JB |
566 | getLimitFromSampledValueTemplateCustomValue( |
567 | powerSampledValueTemplate.value, | |
568 | connectorMaximumPower / unitDivider, | |
569 | connectorMinimumPower / unitDivider, | |
570 | { | |
571 | limitationEnabled: | |
5199f9fd | 572 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
573 | fallbackValue: connectorMinimumPower / unitDivider |
574 | } | |
41f3983a JB |
575 | ), |
576 | powerSampledValueTemplate.fluctuationPercent ?? | |
66a7748d | 577 | Constants.DEFAULT_FLUCTUATION_PERCENT |
41f3983a | 578 | ) |
66a7748d JB |
579 | : getRandomFloatRounded( |
580 | connectorMaximumPower / unitDivider, | |
581 | connectorMinimumPower / unitDivider | |
582 | ) | |
583 | powerMeasurandValues.L2 = 0 | |
584 | powerMeasurandValues.L3 = 0 | |
585 | } | |
586 | powerMeasurandValues.allPhases = roundTo( | |
587 | powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3, | |
588 | 2 | |
589 | ) | |
590 | break | |
591 | case CurrentType.DC: | |
592 | powerMeasurandValues.allPhases = isNotEmptyString(powerSampledValueTemplate.value) | |
593 | ? getRandomFloatFluctuatedRounded( | |
594 | getLimitFromSampledValueTemplateCustomValue( | |
595 | powerSampledValueTemplate.value, | |
41f3983a JB |
596 | connectorMaximumPower / unitDivider, |
597 | connectorMinimumPower / unitDivider, | |
66a7748d JB |
598 | { |
599 | limitationEnabled: | |
5199f9fd | 600 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
601 | fallbackValue: connectorMinimumPower / unitDivider |
602 | } | |
603 | ), | |
604 | powerSampledValueTemplate.fluctuationPercent ?? | |
605 | Constants.DEFAULT_FLUCTUATION_PERCENT | |
606 | ) | |
607 | : getRandomFloatRounded( | |
608 | connectorMaximumPower / unitDivider, | |
609 | connectorMinimumPower / unitDivider | |
610 | ) | |
611 | break | |
41f3983a | 612 | default: |
66a7748d JB |
613 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`) |
614 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES) | |
41f3983a JB |
615 | } |
616 | meterValue.sampledValue.push( | |
66a7748d JB |
617 | buildSampledValue(powerSampledValueTemplate, powerMeasurandValues.allPhases) |
618 | ) | |
619 | const sampledValuesIndex = meterValue.sampledValue.length - 1 | |
620 | const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2) | |
621 | const connectorMinimumPowerRounded = roundTo(connectorMinimumPower / unitDivider, 2) | |
41f3983a JB |
622 | if ( |
623 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > | |
624 | connectorMaximumPowerRounded || | |
625 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) < | |
626 | connectorMinimumPowerRounded || | |
627 | debug | |
628 | ) { | |
629 | logger.error( | |
630 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
631 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
632 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
633 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerRounded}/${ | |
634 | meterValue.sampledValue[sampledValuesIndex].value | |
66a7748d JB |
635 | }/${connectorMaximumPowerRounded}` |
636 | ) | |
41f3983a JB |
637 | } |
638 | for ( | |
639 | let phase = 1; | |
640 | chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); | |
641 | phase++ | |
642 | ) { | |
66a7748d | 643 | const phaseValue = `L${phase}-N` |
41f3983a JB |
644 | meterValue.sampledValue.push( |
645 | buildSampledValue( | |
646 | powerPerPhaseSampledValueTemplates[ | |
647 | `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates | |
648 | ] ?? powerSampledValueTemplate, | |
649 | powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates], | |
650 | undefined, | |
66a7748d JB |
651 | phaseValue as MeterValuePhase |
652 | ) | |
653 | ) | |
654 | const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1 | |
41f3983a JB |
655 | const connectorMaximumPowerPerPhaseRounded = roundTo( |
656 | connectorMaximumPowerPerPhase / unitDivider, | |
66a7748d JB |
657 | 2 |
658 | ) | |
41f3983a JB |
659 | const connectorMinimumPowerPerPhaseRounded = roundTo( |
660 | connectorMinimumPowerPerPhase / unitDivider, | |
66a7748d JB |
661 | 2 |
662 | ) | |
41f3983a JB |
663 | if ( |
664 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > | |
665 | connectorMaximumPowerPerPhaseRounded || | |
666 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) < | |
667 | connectorMinimumPowerPerPhaseRounded || | |
668 | debug | |
669 | ) { | |
670 | logger.error( | |
671 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
672 | meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ?? | |
673 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
674 | }: phase ${ | |
675 | meterValue.sampledValue[sampledValuesPerPhaseIndex].phase | |
676 | }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${ | |
677 | meterValue.sampledValue[sampledValuesPerPhaseIndex].value | |
66a7748d JB |
678 | }/${connectorMaximumPowerPerPhaseRounded}` |
679 | ) | |
41f3983a JB |
680 | } |
681 | } | |
682 | } | |
683 | // Current.Import measurand | |
684 | currentSampledValueTemplate = getSampledValueTemplate( | |
685 | chargingStation, | |
686 | connectorId, | |
66a7748d JB |
687 | MeterValueMeasurand.CURRENT_IMPORT |
688 | ) | |
41f3983a JB |
689 | if (chargingStation.getNumberOfPhases() === 3) { |
690 | currentPerPhaseSampledValueTemplates = { | |
691 | L1: getSampledValueTemplate( | |
692 | chargingStation, | |
693 | connectorId, | |
694 | MeterValueMeasurand.CURRENT_IMPORT, | |
66a7748d | 695 | MeterValuePhase.L1 |
41f3983a JB |
696 | ), |
697 | L2: getSampledValueTemplate( | |
698 | chargingStation, | |
699 | connectorId, | |
700 | MeterValueMeasurand.CURRENT_IMPORT, | |
66a7748d | 701 | MeterValuePhase.L2 |
41f3983a JB |
702 | ), |
703 | L3: getSampledValueTemplate( | |
704 | chargingStation, | |
705 | connectorId, | |
706 | MeterValueMeasurand.CURRENT_IMPORT, | |
66a7748d JB |
707 | MeterValuePhase.L3 |
708 | ) | |
709 | } | |
41f3983a | 710 | } |
66a7748d JB |
711 | if (currentSampledValueTemplate != null) { |
712 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
5199f9fd | 713 | checkMeasurandPowerDivider(chargingStation, currentSampledValueTemplate.measurand) |
41f3983a JB |
714 | const errMsg = `MeterValues measurand ${ |
715 | currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
5199f9fd | 716 | }: Unknown ${chargingStation.stationInfo.currentOutType} currentOutType in template file ${ |
41f3983a JB |
717 | chargingStation.templateFile |
718 | }, cannot calculate ${ | |
719 | currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
66a7748d JB |
720 | } measurand value` |
721 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions | |
722 | const currentMeasurandValues: MeasurandValues = {} as MeasurandValues | |
41f3983a | 723 | const connectorMaximumAvailablePower = |
66a7748d JB |
724 | chargingStation.getConnectorMaximumAvailablePower(connectorId) |
725 | const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0 | |
726 | let connectorMaximumAmperage: number | |
5199f9fd | 727 | switch (chargingStation.stationInfo.currentOutType) { |
41f3983a JB |
728 | case CurrentType.AC: |
729 | connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower( | |
730 | chargingStation.getNumberOfPhases(), | |
731 | connectorMaximumAvailablePower, | |
66a7748d JB |
732 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
733 | chargingStation.stationInfo.voltageOut! | |
734 | ) | |
41f3983a JB |
735 | if (chargingStation.getNumberOfPhases() === 3) { |
736 | const defaultFluctuatedAmperagePerPhase = isNotEmptyString( | |
66a7748d | 737 | currentSampledValueTemplate.value |
41f3983a JB |
738 | ) |
739 | ? getRandomFloatFluctuatedRounded( | |
66a7748d JB |
740 | getLimitFromSampledValueTemplateCustomValue( |
741 | currentSampledValueTemplate.value, | |
742 | connectorMaximumAmperage, | |
743 | connectorMinimumAmperage, | |
744 | { | |
745 | limitationEnabled: | |
5199f9fd | 746 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
747 | fallbackValue: connectorMinimumAmperage |
748 | } | |
749 | ), | |
750 | currentSampledValueTemplate.fluctuationPercent ?? | |
751 | Constants.DEFAULT_FLUCTUATION_PERCENT | |
752 | ) | |
753 | : undefined | |
41f3983a | 754 | const phase1FluctuatedValue = isNotEmptyString( |
66a7748d | 755 | currentPerPhaseSampledValueTemplates.L1?.value |
41f3983a JB |
756 | ) |
757 | ? getRandomFloatFluctuatedRounded( | |
66a7748d | 758 | getLimitFromSampledValueTemplateCustomValue( |
5dc7c990 | 759 | currentPerPhaseSampledValueTemplates.L1.value, |
66a7748d JB |
760 | connectorMaximumAmperage, |
761 | connectorMinimumAmperage, | |
762 | { | |
763 | limitationEnabled: | |
5199f9fd | 764 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
765 | fallbackValue: connectorMinimumAmperage |
766 | } | |
767 | ), | |
5dc7c990 | 768 | currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ?? |
66a7748d JB |
769 | Constants.DEFAULT_FLUCTUATION_PERCENT |
770 | ) | |
771 | : undefined | |
41f3983a | 772 | const phase2FluctuatedValue = isNotEmptyString( |
66a7748d | 773 | currentPerPhaseSampledValueTemplates.L2?.value |
41f3983a JB |
774 | ) |
775 | ? getRandomFloatFluctuatedRounded( | |
66a7748d | 776 | getLimitFromSampledValueTemplateCustomValue( |
5dc7c990 | 777 | currentPerPhaseSampledValueTemplates.L2.value, |
66a7748d JB |
778 | connectorMaximumAmperage, |
779 | connectorMinimumAmperage, | |
780 | { | |
781 | limitationEnabled: | |
5199f9fd | 782 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
783 | fallbackValue: connectorMinimumAmperage |
784 | } | |
785 | ), | |
5dc7c990 | 786 | currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ?? |
66a7748d JB |
787 | Constants.DEFAULT_FLUCTUATION_PERCENT |
788 | ) | |
789 | : undefined | |
41f3983a | 790 | const phase3FluctuatedValue = isNotEmptyString( |
66a7748d | 791 | currentPerPhaseSampledValueTemplates.L3?.value |
41f3983a JB |
792 | ) |
793 | ? getRandomFloatFluctuatedRounded( | |
66a7748d | 794 | getLimitFromSampledValueTemplateCustomValue( |
5dc7c990 | 795 | currentPerPhaseSampledValueTemplates.L3.value, |
66a7748d JB |
796 | connectorMaximumAmperage, |
797 | connectorMinimumAmperage, | |
798 | { | |
799 | limitationEnabled: | |
5199f9fd | 800 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
801 | fallbackValue: connectorMinimumAmperage |
802 | } | |
803 | ), | |
5dc7c990 | 804 | currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ?? |
66a7748d JB |
805 | Constants.DEFAULT_FLUCTUATION_PERCENT |
806 | ) | |
807 | : undefined | |
41f3983a JB |
808 | currentMeasurandValues.L1 = |
809 | phase1FluctuatedValue ?? | |
810 | defaultFluctuatedAmperagePerPhase ?? | |
66a7748d | 811 | getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) |
41f3983a JB |
812 | currentMeasurandValues.L2 = |
813 | phase2FluctuatedValue ?? | |
814 | defaultFluctuatedAmperagePerPhase ?? | |
66a7748d | 815 | getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) |
41f3983a JB |
816 | currentMeasurandValues.L3 = |
817 | phase3FluctuatedValue ?? | |
818 | defaultFluctuatedAmperagePerPhase ?? | |
66a7748d | 819 | getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) |
41f3983a JB |
820 | } else { |
821 | currentMeasurandValues.L1 = isNotEmptyString(currentSampledValueTemplate.value) | |
822 | ? getRandomFloatFluctuatedRounded( | |
66a7748d JB |
823 | getLimitFromSampledValueTemplateCustomValue( |
824 | currentSampledValueTemplate.value, | |
825 | connectorMaximumAmperage, | |
826 | connectorMinimumAmperage, | |
827 | { | |
828 | limitationEnabled: | |
5199f9fd | 829 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
830 | fallbackValue: connectorMinimumAmperage |
831 | } | |
832 | ), | |
833 | currentSampledValueTemplate.fluctuationPercent ?? | |
834 | Constants.DEFAULT_FLUCTUATION_PERCENT | |
835 | ) | |
836 | : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) | |
837 | currentMeasurandValues.L2 = 0 | |
838 | currentMeasurandValues.L3 = 0 | |
41f3983a JB |
839 | } |
840 | currentMeasurandValues.allPhases = roundTo( | |
841 | (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) / | |
842 | chargingStation.getNumberOfPhases(), | |
66a7748d JB |
843 | 2 |
844 | ) | |
845 | break | |
41f3983a JB |
846 | case CurrentType.DC: |
847 | connectorMaximumAmperage = DCElectricUtils.amperage( | |
848 | connectorMaximumAvailablePower, | |
66a7748d JB |
849 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
850 | chargingStation.stationInfo.voltageOut! | |
851 | ) | |
41f3983a JB |
852 | currentMeasurandValues.allPhases = isNotEmptyString(currentSampledValueTemplate.value) |
853 | ? getRandomFloatFluctuatedRounded( | |
66a7748d JB |
854 | getLimitFromSampledValueTemplateCustomValue( |
855 | currentSampledValueTemplate.value, | |
856 | connectorMaximumAmperage, | |
857 | connectorMinimumAmperage, | |
858 | { | |
859 | limitationEnabled: | |
5199f9fd | 860 | chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
861 | fallbackValue: connectorMinimumAmperage |
862 | } | |
863 | ), | |
864 | currentSampledValueTemplate.fluctuationPercent ?? | |
865 | Constants.DEFAULT_FLUCTUATION_PERCENT | |
866 | ) | |
867 | : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) | |
868 | break | |
41f3983a | 869 | default: |
66a7748d JB |
870 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`) |
871 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES) | |
41f3983a JB |
872 | } |
873 | meterValue.sampledValue.push( | |
66a7748d JB |
874 | buildSampledValue(currentSampledValueTemplate, currentMeasurandValues.allPhases) |
875 | ) | |
876 | const sampledValuesIndex = meterValue.sampledValue.length - 1 | |
41f3983a JB |
877 | if ( |
878 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > | |
879 | connectorMaximumAmperage || | |
880 | convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) < | |
881 | connectorMinimumAmperage || | |
882 | debug | |
883 | ) { | |
884 | logger.error( | |
885 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
886 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
887 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
888 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${ | |
889 | meterValue.sampledValue[sampledValuesIndex].value | |
66a7748d JB |
890 | }/${connectorMaximumAmperage}` |
891 | ) | |
41f3983a JB |
892 | } |
893 | for ( | |
894 | let phase = 1; | |
895 | chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); | |
896 | phase++ | |
897 | ) { | |
66a7748d | 898 | const phaseValue = `L${phase}` |
41f3983a JB |
899 | meterValue.sampledValue.push( |
900 | buildSampledValue( | |
901 | currentPerPhaseSampledValueTemplates[ | |
902 | phaseValue as keyof MeasurandPerPhaseSampledValueTemplates | |
903 | ] ?? currentSampledValueTemplate, | |
904 | currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates], | |
905 | undefined, | |
66a7748d JB |
906 | phaseValue as MeterValuePhase |
907 | ) | |
908 | ) | |
909 | const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1 | |
41f3983a JB |
910 | if ( |
911 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > | |
912 | connectorMaximumAmperage || | |
913 | convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) < | |
914 | connectorMinimumAmperage || | |
915 | debug | |
916 | ) { | |
917 | logger.error( | |
918 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
919 | meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ?? | |
920 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
921 | }: phase ${ | |
922 | meterValue.sampledValue[sampledValuesPerPhaseIndex].phase | |
923 | }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${ | |
924 | meterValue.sampledValue[sampledValuesPerPhaseIndex].value | |
66a7748d JB |
925 | }/${connectorMaximumAmperage}` |
926 | ) | |
41f3983a JB |
927 | } |
928 | } | |
929 | } | |
930 | // Energy.Active.Import.Register measurand (default) | |
66a7748d JB |
931 | energySampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId) |
932 | if (energySampledValueTemplate != null) { | |
5199f9fd | 933 | checkMeasurandPowerDivider(chargingStation, energySampledValueTemplate.measurand) |
41f3983a | 934 | const unitDivider = |
5199f9fd | 935 | energySampledValueTemplate.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1 |
41f3983a | 936 | const connectorMaximumAvailablePower = |
66a7748d | 937 | chargingStation.getConnectorMaximumAvailablePower(connectorId) |
41f3983a JB |
938 | const connectorMaximumEnergyRounded = roundTo( |
939 | (connectorMaximumAvailablePower * interval) / (3600 * 1000), | |
66a7748d JB |
940 | 2 |
941 | ) | |
41f3983a JB |
942 | const connectorMinimumEnergyRounded = roundTo( |
943 | energySampledValueTemplate.minimumValue ?? 0, | |
66a7748d JB |
944 | 2 |
945 | ) | |
41f3983a JB |
946 | const energyValueRounded = isNotEmptyString(energySampledValueTemplate.value) |
947 | ? getRandomFloatFluctuatedRounded( | |
66a7748d JB |
948 | getLimitFromSampledValueTemplateCustomValue( |
949 | energySampledValueTemplate.value, | |
950 | connectorMaximumEnergyRounded, | |
951 | connectorMinimumEnergyRounded, | |
952 | { | |
5199f9fd | 953 | limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, |
66a7748d JB |
954 | fallbackValue: connectorMinimumEnergyRounded, |
955 | unitMultiplier: unitDivider | |
956 | } | |
957 | ), | |
958 | energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT | |
959 | ) | |
960 | : getRandomFloatRounded(connectorMaximumEnergyRounded, connectorMinimumEnergyRounded) | |
41f3983a | 961 | // Persist previous value on connector |
66a7748d | 962 | if (connector != null) { |
41f3983a | 963 | if ( |
be9f397b JB |
964 | connector.energyActiveImportRegisterValue != null && |
965 | connector.energyActiveImportRegisterValue >= 0 && | |
966 | connector.transactionEnergyActiveImportRegisterValue != null && | |
967 | connector.transactionEnergyActiveImportRegisterValue >= 0 | |
41f3983a | 968 | ) { |
be9f397b JB |
969 | connector.energyActiveImportRegisterValue += energyValueRounded |
970 | connector.transactionEnergyActiveImportRegisterValue += energyValueRounded | |
41f3983a | 971 | } else { |
66a7748d JB |
972 | connector.energyActiveImportRegisterValue = 0 |
973 | connector.transactionEnergyActiveImportRegisterValue = 0 | |
41f3983a JB |
974 | } |
975 | } | |
976 | meterValue.sampledValue.push( | |
977 | buildSampledValue( | |
978 | energySampledValueTemplate, | |
979 | roundTo( | |
980 | chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) / | |
981 | unitDivider, | |
66a7748d JB |
982 | 2 |
983 | ) | |
984 | ) | |
985 | ) | |
986 | const sampledValuesIndex = meterValue.sampledValue.length - 1 | |
41f3983a JB |
987 | if ( |
988 | energyValueRounded > connectorMaximumEnergyRounded || | |
989 | energyValueRounded < connectorMinimumEnergyRounded || | |
990 | debug | |
991 | ) { | |
992 | logger.error( | |
993 | `${chargingStation.logPrefix()} MeterValues measurand ${ | |
994 | meterValue.sampledValue[sampledValuesIndex].measurand ?? | |
995 | MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
66a7748d JB |
996 | }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumEnergyRounded}/${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms` |
997 | ) | |
41f3983a JB |
998 | } |
999 | } | |
66a7748d | 1000 | return meterValue |
41f3983a JB |
1001 | case OCPPVersion.VERSION_20: |
1002 | case OCPPVersion.VERSION_201: | |
1003 | default: | |
1004 | throw new BaseError( | |
66a7748d JB |
1005 | `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported` |
1006 | ) | |
41f3983a | 1007 | } |
66a7748d | 1008 | } |
41f3983a JB |
1009 | |
1010 | export const buildTransactionEndMeterValue = ( | |
1011 | chargingStation: ChargingStation, | |
1012 | connectorId: number, | |
5199f9fd | 1013 | meterStop: number | undefined |
41f3983a | 1014 | ): MeterValue => { |
66a7748d JB |
1015 | let meterValue: MeterValue |
1016 | let sampledValueTemplate: SampledValueTemplate | undefined | |
1017 | let unitDivider: number | |
41f3983a JB |
1018 | switch (chargingStation.stationInfo?.ocppVersion) { |
1019 | case OCPPVersion.VERSION_16: | |
1020 | meterValue = { | |
1021 | timestamp: new Date(), | |
66a7748d JB |
1022 | sampledValue: [] |
1023 | } | |
41f3983a | 1024 | // Energy.Active.Import.Register measurand (default) |
66a7748d JB |
1025 | sampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId) |
1026 | unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1 | |
41f3983a JB |
1027 | meterValue.sampledValue.push( |
1028 | buildSampledValue( | |
66a7748d | 1029 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
41f3983a JB |
1030 | sampledValueTemplate!, |
1031 | roundTo((meterStop ?? 0) / unitDivider, 4), | |
66a7748d JB |
1032 | MeterValueContext.TRANSACTION_END |
1033 | ) | |
1034 | ) | |
1035 | return meterValue | |
41f3983a JB |
1036 | case OCPPVersion.VERSION_20: |
1037 | case OCPPVersion.VERSION_201: | |
1038 | default: | |
1039 | throw new BaseError( | |
66a7748d JB |
1040 | `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported` |
1041 | ) | |
41f3983a | 1042 | } |
66a7748d | 1043 | } |
41f3983a JB |
1044 | |
1045 | const checkMeasurandPowerDivider = ( | |
1046 | chargingStation: ChargingStation, | |
5199f9fd | 1047 | measurandType: MeterValueMeasurand | undefined |
41f3983a | 1048 | ): void => { |
300418e9 | 1049 | if (chargingStation.powerDivider == null) { |
41f3983a JB |
1050 | const errMsg = `MeterValues measurand ${ |
1051 | measurandType ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
66a7748d JB |
1052 | }: powerDivider is undefined` |
1053 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`) | |
1054 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES) | |
5199f9fd | 1055 | } else if (chargingStation.powerDivider <= 0) { |
41f3983a JB |
1056 | const errMsg = `MeterValues measurand ${ |
1057 | measurandType ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
66a7748d JB |
1058 | }: powerDivider have zero or below value ${chargingStation.powerDivider}` |
1059 | logger.error(`${chargingStation.logPrefix()} ${errMsg}`) | |
1060 | throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES) | |
41f3983a | 1061 | } |
66a7748d | 1062 | } |
41f3983a JB |
1063 | |
1064 | const getLimitFromSampledValueTemplateCustomValue = ( | |
1065 | value: string | undefined, | |
1066 | maxLimit: number, | |
1067 | minLimit: number, | |
66a7748d | 1068 | options?: { limitationEnabled?: boolean, fallbackValue?: number, unitMultiplier?: number } |
41f3983a JB |
1069 | ): number => { |
1070 | options = { | |
1071 | ...{ | |
1072 | limitationEnabled: false, | |
1073 | unitMultiplier: 1, | |
66a7748d | 1074 | fallbackValue: 0 |
41f3983a | 1075 | }, |
66a7748d JB |
1076 | ...options |
1077 | } | |
1078 | const parsedValue = parseInt(value ?? '') | |
5199f9fd | 1079 | if (options.limitationEnabled === true) { |
41f3983a | 1080 | return max( |
66a7748d | 1081 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
41f3983a | 1082 | min((!isNaN(parsedValue) ? parsedValue : Infinity) * options.unitMultiplier!, maxLimit), |
66a7748d JB |
1083 | minLimit |
1084 | ) | |
41f3983a | 1085 | } |
66a7748d JB |
1086 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
1087 | return (!isNaN(parsedValue) ? parsedValue : options.fallbackValue!) * options.unitMultiplier! | |
1088 | } | |
41f3983a JB |
1089 | |
1090 | const getSampledValueTemplate = ( | |
1091 | chargingStation: ChargingStation, | |
1092 | connectorId: number, | |
1093 | measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER, | |
66a7748d | 1094 | phase?: MeterValuePhase |
41f3983a | 1095 | ): SampledValueTemplate | undefined => { |
66a7748d JB |
1096 | const onPhaseStr = phase != null ? `on phase ${phase} ` : '' |
1097 | if (!OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand)) { | |
41f3983a | 1098 | logger.warn( |
66a7748d JB |
1099 | `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}` |
1100 | ) | |
1101 | return | |
41f3983a JB |
1102 | } |
1103 | if ( | |
1104 | measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER && | |
1105 | getConfigurationKey( | |
1106 | chargingStation, | |
66a7748d | 1107 | StandardParametersKey.MeterValuesSampledData |
41f3983a JB |
1108 | )?.value?.includes(measurand) === false |
1109 | ) { | |
1110 | logger.debug( | |
1111 | `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId} not found in '${ | |
1112 | StandardParametersKey.MeterValuesSampledData | |
66a7748d JB |
1113 | }' OCPP parameter` |
1114 | ) | |
1115 | return | |
41f3983a | 1116 | } |
97608fbd | 1117 | const sampledValueTemplates = |
66a7748d JB |
1118 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
1119 | chargingStation.getConnectorStatus(connectorId)!.MeterValues | |
41f3983a JB |
1120 | for ( |
1121 | let index = 0; | |
66a7748d | 1122 | isNotEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length; |
41f3983a JB |
1123 | index++ |
1124 | ) { | |
1125 | if ( | |
66a7748d JB |
1126 | !OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes( |
1127 | sampledValueTemplates[index]?.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER | |
1128 | ) | |
41f3983a JB |
1129 | ) { |
1130 | logger.warn( | |
66a7748d JB |
1131 | `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}` |
1132 | ) | |
41f3983a | 1133 | } else if ( |
66a7748d | 1134 | phase != null && |
41f3983a JB |
1135 | sampledValueTemplates[index]?.phase === phase && |
1136 | sampledValueTemplates[index]?.measurand === measurand && | |
1137 | getConfigurationKey( | |
1138 | chargingStation, | |
66a7748d | 1139 | StandardParametersKey.MeterValuesSampledData |
41f3983a JB |
1140 | )?.value?.includes(measurand) === true |
1141 | ) { | |
66a7748d | 1142 | return sampledValueTemplates[index] |
41f3983a | 1143 | } else if ( |
66a7748d JB |
1144 | phase == null && |
1145 | sampledValueTemplates[index]?.phase == null && | |
41f3983a JB |
1146 | sampledValueTemplates[index]?.measurand === measurand && |
1147 | getConfigurationKey( | |
1148 | chargingStation, | |
66a7748d | 1149 | StandardParametersKey.MeterValuesSampledData |
41f3983a JB |
1150 | )?.value?.includes(measurand) === true |
1151 | ) { | |
66a7748d | 1152 | return sampledValueTemplates[index] |
41f3983a JB |
1153 | } else if ( |
1154 | measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER && | |
66a7748d | 1155 | (sampledValueTemplates[index]?.measurand == null || |
41f3983a JB |
1156 | sampledValueTemplates[index]?.measurand === measurand) |
1157 | ) { | |
66a7748d | 1158 | return sampledValueTemplates[index] |
41f3983a JB |
1159 | } |
1160 | } | |
1161 | if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) { | |
66a7748d JB |
1162 | const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connector id ${connectorId}` |
1163 | logger.error(`${chargingStation.logPrefix()} ${errorMsg}`) | |
1164 | throw new BaseError(errorMsg) | |
41f3983a JB |
1165 | } |
1166 | logger.debug( | |
66a7748d JB |
1167 | `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId}` |
1168 | ) | |
1169 | } | |
41f3983a JB |
1170 | |
1171 | const buildSampledValue = ( | |
1172 | sampledValueTemplate: SampledValueTemplate, | |
1173 | value: number, | |
1174 | context?: MeterValueContext, | |
66a7748d | 1175 | phase?: MeterValuePhase |
41f3983a | 1176 | ): SampledValue => { |
5199f9fd | 1177 | const sampledValueContext = context ?? sampledValueTemplate.context |
41f3983a | 1178 | const sampledValueLocation = |
66a7748d | 1179 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
5199f9fd JB |
1180 | sampledValueTemplate.location ?? getMeasurandDefaultLocation(sampledValueTemplate.measurand!) |
1181 | const sampledValuePhase = phase ?? sampledValueTemplate.phase | |
41f3983a | 1182 | return { |
be9f397b | 1183 | ...(sampledValueTemplate.unit != null && { |
66a7748d | 1184 | unit: sampledValueTemplate.unit |
41f3983a | 1185 | }), |
be9f397b JB |
1186 | ...(sampledValueContext != null && { context: sampledValueContext }), |
1187 | ...(sampledValueTemplate.measurand != null && { | |
66a7748d | 1188 | measurand: sampledValueTemplate.measurand |
41f3983a | 1189 | }), |
be9f397b | 1190 | ...(sampledValueLocation != null && { location: sampledValueLocation }), |
5199f9fd | 1191 | ...{ value: value.toString() }, |
be9f397b | 1192 | ...(sampledValuePhase != null && { phase: sampledValuePhase }) |
5199f9fd | 1193 | } satisfies SampledValue |
66a7748d | 1194 | } |
41f3983a JB |
1195 | |
1196 | const getMeasurandDefaultLocation = ( | |
66a7748d | 1197 | measurandType: MeterValueMeasurand |
41f3983a JB |
1198 | ): MeterValueLocation | undefined => { |
1199 | switch (measurandType) { | |
1200 | case MeterValueMeasurand.STATE_OF_CHARGE: | |
66a7748d | 1201 | return MeterValueLocation.EV |
41f3983a | 1202 | } |
66a7748d | 1203 | } |
41f3983a JB |
1204 | |
1205 | // const getMeasurandDefaultUnit = ( | |
66a7748d | 1206 | // measurandType: MeterValueMeasurand |
41f3983a JB |
1207 | // ): MeterValueUnit | undefined => { |
1208 | // switch (measurandType) { | |
1209 | // case MeterValueMeasurand.CURRENT_EXPORT: | |
1210 | // case MeterValueMeasurand.CURRENT_IMPORT: | |
1211 | // case MeterValueMeasurand.CURRENT_OFFERED: | |
66a7748d | 1212 | // return MeterValueUnit.AMP |
41f3983a JB |
1213 | // case MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: |
1214 | // case MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: | |
66a7748d | 1215 | // return MeterValueUnit.WATT_HOUR |
41f3983a JB |
1216 | // case MeterValueMeasurand.POWER_ACTIVE_EXPORT: |
1217 | // case MeterValueMeasurand.POWER_ACTIVE_IMPORT: | |
1218 | // case MeterValueMeasurand.POWER_OFFERED: | |
66a7748d | 1219 | // return MeterValueUnit.WATT |
41f3983a | 1220 | // case MeterValueMeasurand.STATE_OF_CHARGE: |
66a7748d | 1221 | // return MeterValueUnit.PERCENT |
41f3983a | 1222 | // case MeterValueMeasurand.VOLTAGE: |
66a7748d | 1223 | // return MeterValueUnit.VOLT |
41f3983a | 1224 | // } |
66a7748d | 1225 | // } |
41f3983a | 1226 | |
66a7748d | 1227 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class |
90befdb8 | 1228 | export class OCPPServiceUtils { |
1d6f2eb4 JB |
1229 | public static readonly getMessageTypeString = getMessageTypeString |
1230 | public static readonly sendAndSetConnectorStatus = sendAndSetConnectorStatus | |
1231 | public static readonly isIdTagAuthorized = isIdTagAuthorized | |
1232 | public static readonly buildTransactionEndMeterValue = buildTransactionEndMeterValue | |
66a7748d JB |
1233 | protected static getSampledValueTemplate = getSampledValueTemplate |
1234 | protected static buildSampledValue = buildSampledValue | |
041365be | 1235 | |
66a7748d | 1236 | protected constructor () { |
d5bd1c00 JB |
1237 | // This is intentional |
1238 | } | |
1239 | ||
5dc7c990 | 1240 | public static ajvErrorsToErrorType (errors: ErrorObject[] | undefined | null): ErrorType { |
66a7748d | 1241 | if (isNotEmptyArray(errors)) { |
9ff486f4 JB |
1242 | for (const error of errors as DefinedError[]) { |
1243 | switch (error.keyword) { | |
1244 | case 'type': | |
66a7748d | 1245 | return ErrorType.TYPE_CONSTRAINT_VIOLATION |
9ff486f4 JB |
1246 | case 'dependencies': |
1247 | case 'required': | |
66a7748d | 1248 | return ErrorType.OCCURRENCE_CONSTRAINT_VIOLATION |
9ff486f4 JB |
1249 | case 'pattern': |
1250 | case 'format': | |
66a7748d | 1251 | return ErrorType.PROPERTY_CONSTRAINT_VIOLATION |
9ff486f4 | 1252 | } |
06ad945f JB |
1253 | } |
1254 | } | |
66a7748d | 1255 | return ErrorType.FORMAT_VIOLATION |
06ad945f JB |
1256 | } |
1257 | ||
66a7748d | 1258 | public static isRequestCommandSupported ( |
fd3c56d1 | 1259 | chargingStation: ChargingStation, |
66a7748d | 1260 | command: RequestCommand |
ed3d2808 | 1261 | ): boolean { |
66a7748d | 1262 | const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command) |
ed3d2808 | 1263 | if ( |
66a7748d JB |
1264 | isRequestCommand && |
1265 | chargingStation.stationInfo?.commandsSupport?.outgoingCommands == null | |
ed3d2808 | 1266 | ) { |
66a7748d | 1267 | return true |
ed3d2808 | 1268 | } else if ( |
66a7748d JB |
1269 | isRequestCommand && |
1270 | chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] != null | |
ed3d2808 | 1271 | ) { |
5199f9fd | 1272 | return chargingStation.stationInfo.commandsSupport.outgoingCommands[command] |
ed3d2808 | 1273 | } |
66a7748d JB |
1274 | logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`) |
1275 | return false | |
ed3d2808 JB |
1276 | } |
1277 | ||
66a7748d | 1278 | public static isIncomingRequestCommandSupported ( |
fd3c56d1 | 1279 | chargingStation: ChargingStation, |
66a7748d | 1280 | command: IncomingRequestCommand |
ed3d2808 | 1281 | ): boolean { |
edd13439 | 1282 | const isIncomingRequestCommand = |
66a7748d | 1283 | Object.values<IncomingRequestCommand>(IncomingRequestCommand).includes(command) |
ed3d2808 | 1284 | if ( |
66a7748d JB |
1285 | isIncomingRequestCommand && |
1286 | chargingStation.stationInfo?.commandsSupport?.incomingCommands == null | |
ed3d2808 | 1287 | ) { |
66a7748d | 1288 | return true |
ed3d2808 | 1289 | } else if ( |
66a7748d | 1290 | isIncomingRequestCommand && |
5199f9fd | 1291 | chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] != null |
ed3d2808 | 1292 | ) { |
5199f9fd | 1293 | return chargingStation.stationInfo.commandsSupport.incomingCommands[command] |
ed3d2808 | 1294 | } |
66a7748d JB |
1295 | logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`) |
1296 | return false | |
ed3d2808 JB |
1297 | } |
1298 | ||
66a7748d | 1299 | public static isMessageTriggerSupported ( |
c60ed4b8 | 1300 | chargingStation: ChargingStation, |
66a7748d | 1301 | messageTrigger: MessageTrigger |
c60ed4b8 | 1302 | ): boolean { |
66a7748d JB |
1303 | const isMessageTrigger = Object.values(MessageTrigger).includes(messageTrigger) |
1304 | if (isMessageTrigger && chargingStation.stationInfo?.messageTriggerSupport == null) { | |
1305 | return true | |
1c9de2b9 | 1306 | } else if ( |
66a7748d JB |
1307 | isMessageTrigger && |
1308 | chargingStation.stationInfo?.messageTriggerSupport?.[messageTrigger] != null | |
1c9de2b9 | 1309 | ) { |
5199f9fd | 1310 | return chargingStation.stationInfo.messageTriggerSupport[messageTrigger] |
c60ed4b8 JB |
1311 | } |
1312 | logger.error( | |
66a7748d JB |
1313 | `${chargingStation.logPrefix()} Unknown incoming OCPP message trigger '${messageTrigger}'` |
1314 | ) | |
1315 | return false | |
c60ed4b8 JB |
1316 | } |
1317 | ||
66a7748d | 1318 | public static isConnectorIdValid ( |
c60ed4b8 JB |
1319 | chargingStation: ChargingStation, |
1320 | ocppCommand: IncomingRequestCommand, | |
66a7748d | 1321 | connectorId: number |
c60ed4b8 JB |
1322 | ): boolean { |
1323 | if (connectorId < 0) { | |
1324 | logger.error( | |
66a7748d JB |
1325 | `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId}` |
1326 | ) | |
1327 | return false | |
c60ed4b8 | 1328 | } |
66a7748d | 1329 | return true |
c60ed4b8 JB |
1330 | } |
1331 | ||
1e2ec4a6 JB |
1332 | public static convertDateToISOString<T extends JsonType>(object: T): void { |
1333 | for (const key in object) { | |
66a7748d | 1334 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion |
1e2ec4a6 | 1335 | if (isDate(object![key])) { |
66a7748d | 1336 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion |
1e2ec4a6 | 1337 | (object![key] as string) = (object![key] as Date).toISOString() |
5199f9fd | 1338 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-condition |
1e2ec4a6 | 1339 | } else if (typeof object![key] === 'object' && object![key] !== null) { |
66a7748d | 1340 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion |
1e2ec4a6 | 1341 | OCPPServiceUtils.convertDateToISOString<T>(object![key] as T) |
1799761a JB |
1342 | } |
1343 | } | |
1344 | } | |
1345 | ||
66a7748d | 1346 | public static startHeartbeatInterval (chargingStation: ChargingStation, interval: number): void { |
a807045b | 1347 | if (chargingStation.heartbeatSetInterval == null) { |
66a7748d | 1348 | chargingStation.startHeartbeat() |
8f953431 | 1349 | } else if (chargingStation.getHeartbeatInterval() !== interval) { |
66a7748d | 1350 | chargingStation.restartHeartbeat() |
8f953431 JB |
1351 | } |
1352 | } | |
1353 | ||
7164966d | 1354 | protected static parseJsonSchemaFile<T extends JsonType>( |
51022aa0 | 1355 | relativePath: string, |
1b271a54 JB |
1356 | ocppVersion: OCPPVersion, |
1357 | moduleName?: string, | |
66a7748d | 1358 | methodName?: string |
7164966d | 1359 | ): JSONSchemaType<T> { |
66a7748d | 1360 | const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath) |
7164966d | 1361 | try { |
66a7748d | 1362 | return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType<T> |
7164966d | 1363 | } catch (error) { |
fa5995d6 | 1364 | handleFileException( |
7164966d JB |
1365 | filePath, |
1366 | FileType.JsonSchema, | |
1367 | error as NodeJS.ErrnoException, | |
1b271a54 | 1368 | OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName), |
66a7748d JB |
1369 | { throwError: false } |
1370 | ) | |
1371 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions | |
1372 | return {} as JSONSchemaType<T> | |
7164966d | 1373 | } |
130783a7 JB |
1374 | } |
1375 | ||
66a7748d | 1376 | private static readonly logPrefix = ( |
1b271a54 JB |
1377 | ocppVersion: OCPPVersion, |
1378 | moduleName?: string, | |
66a7748d | 1379 | methodName?: string |
1b271a54 JB |
1380 | ): string => { |
1381 | const logMsg = | |
9bf0ef23 | 1382 | isNotEmptyString(moduleName) && isNotEmptyString(methodName) |
1b271a54 | 1383 | ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:` |
66a7748d JB |
1384 | : ` OCPP ${ocppVersion} |` |
1385 | return logPrefix(logMsg) | |
1386 | } | |
90befdb8 | 1387 | } |