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