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