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