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