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