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