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