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