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