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