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