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