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