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