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