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