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