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