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