@citrineos/base
Version:
The base module for OCPP v2.0.1 including all interfaces. This module is not intended to be used directly, but rather as a dependency for other modules.
206 lines • 9.19 kB
JavaScript
import { MeasurandEnum, PhaseEnum, ReadingContextEnum, } from '../interfaces/dto/types/enums.js';
export class MeterValueUtils {
static validContexts = new Set([
ReadingContextEnum['Transaction.Begin'],
ReadingContextEnum['Sample.Periodic'],
ReadingContextEnum['Transaction.End'],
]);
/**
* Calculate the total Kwh
*
* @param {array} meterValues - meterValues of a transaction.
* @param {number} currentTotal - the current total Kwh to add to interval values, if needed.
* @param {number} meterStart - the starting Kwh value at the beginning of the transaction, if available.
* @return {number} total Kwh based on the best available energy measurement.
*/
static getTotalKwh(meterValues, currentTotal, meterStart) {
const filteredValues = this.filterValidMeterValues(meterValues);
if (filteredValues.length === 0) {
return 0;
}
const registerMap = this.getRegisterValuesMap(filteredValues);
if (registerMap.size > 0) {
const sorted = this.getSortedKwhByTimestampAscending(registerMap);
if (meterStart === undefined) {
return sorted[sorted.length - 1] - sorted[0];
}
return sorted[sorted.length - 1] - meterStart;
}
const intervalMap = this.getIntervalValuesMap(filteredValues);
if (intervalMap.size > 0) {
const sorted = this.getSortedKwhByTimestampAscending(intervalMap);
return sorted.reduce((sum, v) => sum + v, currentTotal);
}
const netMap = this.getNetValuesMap(filteredValues);
if (netMap.size > 0) {
const latestTimestamp = Math.max(...Array.from(netMap.keys()));
return netMap.get(latestTimestamp);
}
return 0;
}
static getMeterStart(meterValues) {
const filteredValues = this.filterValidMeterValues(meterValues);
if (filteredValues.length === 0) {
return null;
}
const registerMap = this.getRegisterValuesMap(filteredValues);
if (registerMap.size > 0) {
const sorted = this.getSortedKwhByTimestampAscending(registerMap);
return sorted[0];
}
return null;
}
/**
* Filter out meter values whose context is not one of the valid reading contexts.
* @param meterValues Array of MeterValueType to filter.
* @returns Filtered array containing only meter values in Transaction_Begin, Sample_Periodic or Transaction_End contexts.
*/
static filterValidMeterValues(meterValues) {
return meterValues.filter((mv) =>
// When missing, context is by default Sample_Periodic by spec
!mv.sampledValue[0].context || this.validContexts.has(mv.sampledValue[0].context));
}
/**
* Extracts Energy.Active.Import.Register measurand values into a timestamp-to-kWh map.
* @param meterValues Array of MeterValueType to search for register readings.
* @returns Map where each key is the reading timestamp (ms since epoch) and each value is the normalized kWh.
*/
static getRegisterValuesMap(meterValues) {
const valuesMap = new Map();
for (const mv of meterValues) {
const ts = Date.parse(mv.timestamp);
let val = this.findMeasurandValue(mv.sampledValue, MeasurandEnum['Energy.Active.Import.Register'], false);
if (val === null) {
val = this.sumPhasedValues(mv.sampledValue, MeasurandEnum['Energy.Active.Import.Register']);
}
if (val !== null) {
valuesMap.set(ts, val);
}
}
return valuesMap;
}
/**
* Extracts Energy.Active.Import.Interval measurand values into a timestamp-to-kWh map.
* @param meterValues Array of MeterValueType to search for interval readings.
* @returns Map where each key is the reading timestamp (ms since epoch) and each value is the normalized kWh.
*/
static getIntervalValuesMap(meterValues) {
const valuesMap = new Map();
for (const mv of meterValues) {
const ts = Date.parse(mv.timestamp);
let val = this.findMeasurandValue(mv.sampledValue, MeasurandEnum['Energy.Active.Import.Interval'], false);
if (val === null) {
val = this.sumPhasedValues(mv.sampledValue, MeasurandEnum['Energy.Active.Import.Interval']);
}
if (val !== null) {
valuesMap.set(ts, val);
}
}
return valuesMap;
}
/**
* Extracts Energy.Active.Net measurand values into a timestamp-to-kWh map.
* @param meterValues Array of MeterValueType to search for net readings.
* @returns Map where each key is the reading timestamp (ms since epoch) and each value is the normalized kWh.
*/
static getNetValuesMap(meterValues) {
const valuesMap = new Map();
for (const mv of meterValues) {
const ts = Date.parse(mv.timestamp);
const val = this.findMeasurandValue(mv.sampledValue, MeasurandEnum['Energy.Active.Net'], false);
if (val !== null) {
valuesMap.set(ts, val);
}
}
return valuesMap;
}
/**
* Find a specific measurand value from sampledValues
* @param sampledValues Array of sampled values
* @param measurand The measurand type to look for
* @param phased Whether to look for phased values (true) or non-phased values (false)
* @returns The normalized value in kWh, or null if not found
*/
static findMeasurandValue(sampledValues, measurand, phased) {
const value = sampledValues.find((sv) => (sv.measurand === measurand ||
(!sv.measurand && // Default to Energy.Active.Import.Register if measurand is missing
measurand === MeasurandEnum['Energy.Active.Import.Register'])) &&
!phased === !sv.phase);
return value ? this.normalizeToKwh(value) : null;
}
/**
* Sum phased values for a specific measurand
* @param sampledValues Array of sampled values
* @param measurand The measurand type to sum
* @returns The sum of phase values in kWh, or null if no valid phase values found
*/
static sumPhasedValues(sampledValues, measurand) {
// Find all values for this measurand that have individual phases L1, L2, or L3
const phaseValues = sampledValues.filter((sv) => sv.measurand === measurand &&
sv.phase && // Must have a phase specified
(sv.phase === PhaseEnum.L1 || sv.phase === PhaseEnum.L2 || sv.phase === PhaseEnum.L3));
// If no phase values found, try phase-to-neutral as a last resort
if (phaseValues.length === 0) {
const phaseNeutralValues = sampledValues.filter((sv) => sv.measurand === measurand &&
sv.phase && // Must have a phase specified
(sv.phase === PhaseEnum['L1-N'] ||
sv.phase === PhaseEnum['L2-N'] ||
sv.phase === PhaseEnum['L3-N']));
if (phaseNeutralValues.length === 0) {
return null;
}
let sum = 0;
for (const value of phaseNeutralValues) {
const normalizedValue = this.normalizeToKwh(value);
if (normalizedValue !== null) {
sum += normalizedValue;
}
}
return sum;
}
// Sum all the normalized phase values
let sum = 0;
for (const value of phaseValues) {
const normalizedValue = this.normalizeToKwh(value);
if (normalizedValue !== null) {
sum += normalizedValue;
}
}
return sum;
}
/**
* Convert a sampled value to kWh, applying unit multipliers.
* @param value A SampledValueType entry.
* @returns The converted value in kWh, or null if unit is missing.
*/
static normalizeToKwh(value) {
let powerOfTen = value.unitOfMeasure?.multiplier ?? 0;
const unit = value.unitOfMeasure?.unit?.toUpperCase();
switch (unit) {
case 'KWH':
case 'KVARH': // For reactive energy
case 'KVAH': // For apparent energy
break;
case 'WH':
case 'VARH': // For reactive energy
case 'VAH': // For apparent energy
case undefined:
powerOfTen -= 3;
break;
default:
throw new Error(`Unknown unit for energy measurement: ${unit}`);
}
return value.value * 10 ** powerOfTen;
}
/**
* Sort the entries of a timestamp-to-kWh map ascending by timestamp and return the kWh values.
* @param valuesMap Map of timestamp (ms since epoch) to kWh.
* @returns Array of kWh values sorted by timestamp.
*/
static getSortedKwhByTimestampAscending(valuesMap) {
return Array.from(valuesMap.entries())
.sort((a, b) => a[0] - b[0])
.map(([, v]) => v);
}
}
//# sourceMappingURL=MeterValueUtils.js.map