Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MeterValueUtils, type MeterValueDto, type SampledValue } from '../../index';
|
||||
|
||||
function makeMeterValue(
|
||||
ts: string,
|
||||
measurand: SampledValue['measurand'],
|
||||
value: number,
|
||||
unit: string = 'kWh',
|
||||
context?: SampledValue['context'],
|
||||
): MeterValueDto {
|
||||
return {
|
||||
timestamp: ts,
|
||||
sampledValue: [
|
||||
{
|
||||
measurand,
|
||||
unitOfMeasure: { unit, multiplier: 0 },
|
||||
value,
|
||||
context,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe('MeterValueUtils', () => {
|
||||
describe('getTotalKwh', () => {
|
||||
describe('Register values', () => {
|
||||
it('calculates difference between first and last register readings without meterStart', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Register', 100),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Import.Register', 150),
|
||||
makeMeterValue('2025-05-29T12:03:00Z', 'Energy.Active.Import.Register', 200),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(100); // 200 - 100 = 100
|
||||
});
|
||||
|
||||
it('calculates difference from meterStart when provided', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Register', 100),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Import.Register', 150),
|
||||
makeMeterValue('2025-05-29T12:03:00Z', 'Energy.Active.Import.Register', 200),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0, 50)).toBe(150); // 200 - 50 = 150
|
||||
});
|
||||
|
||||
it('handles single register reading without meterStart', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Register', 100),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(0); // 100 - 100 = 0
|
||||
});
|
||||
|
||||
it('handles single register reading with meterStart', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Register', 100),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0, 50)).toBe(50); // 100 - 50 = 50
|
||||
});
|
||||
|
||||
it('treats missing measurand as Energy.Active.Import.Register', () => {
|
||||
const meterValues: MeterValueDto[] = [
|
||||
{
|
||||
timestamp: '2025-05-29T12:01:00Z',
|
||||
sampledValue: [{ value: 100, unitOfMeasure: { unit: 'kWh', multiplier: 0 } }],
|
||||
},
|
||||
{
|
||||
timestamp: '2025-05-29T12:02:00Z',
|
||||
sampledValue: [{ value: 200, unitOfMeasure: { unit: 'kWh', multiplier: 0 } }],
|
||||
},
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interval values', () => {
|
||||
it('sums interval readings starting from currentTotal', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Interval', 50),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Import.Interval', 49),
|
||||
makeMeterValue('2025-05-29T12:03:00Z', 'Energy.Active.Import.Interval', 46),
|
||||
makeMeterValue('2025-05-29T12:04:00Z', 'Energy.Active.Import.Interval', 52),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(50 + 49 + 46 + 52);
|
||||
});
|
||||
|
||||
it('adds interval readings to currentTotal', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Interval', 50),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Import.Interval', 50),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 100)).toBe(200); // 100 + 50 + 50
|
||||
});
|
||||
});
|
||||
|
||||
describe('Net values', () => {
|
||||
it('picks latest net reading', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Net', 50),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Net', 100),
|
||||
makeMeterValue('2025-05-29T12:03:00Z', 'Energy.Active.Net', 153),
|
||||
makeMeterValue('2025-05-29T12:04:00Z', 'Energy.Active.Net', 201),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 50)).toBe(201);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filtering by context', () => {
|
||||
it('filters out invalid context values', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue(
|
||||
'2025-05-29T12:00:00Z',
|
||||
'Energy.Active.Import.Register',
|
||||
150,
|
||||
'kWh',
|
||||
'Trigger',
|
||||
), // Invalid context, should be filtered
|
||||
makeMeterValue(
|
||||
'2025-05-29T12:01:00Z',
|
||||
'Energy.Active.Import.Register',
|
||||
100,
|
||||
'kWh',
|
||||
'Transaction.Begin',
|
||||
),
|
||||
makeMeterValue(
|
||||
'2025-05-29T12:03:00Z',
|
||||
'Energy.Active.Import.Register',
|
||||
200,
|
||||
'kWh',
|
||||
'Transaction.End',
|
||||
),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(100); // 200 - 100, ignoring 150
|
||||
});
|
||||
|
||||
it('includes Sample.Periodic context', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue(
|
||||
'2025-05-29T12:01:00Z',
|
||||
'Energy.Active.Import.Register',
|
||||
100,
|
||||
'kWh',
|
||||
'Transaction.Begin',
|
||||
),
|
||||
makeMeterValue(
|
||||
'2025-05-29T12:02:00Z',
|
||||
'Energy.Active.Import.Register',
|
||||
150,
|
||||
'kWh',
|
||||
'Sample.Periodic',
|
||||
),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(50);
|
||||
});
|
||||
|
||||
it('treats undefined context as Sample.Periodic (valid)', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Register', 100),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Import.Register', 200),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unit normalization', () => {
|
||||
it('normalizes Wh to kWh', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Register', 1000, 'Wh'),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Import.Register', 5000, 'Wh'),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(4); // (5000 - 1000) / 1000
|
||||
});
|
||||
|
||||
it('handles kWh without conversion', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Register', 100, 'kWh'),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Import.Register', 150, 'kWh'),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(50);
|
||||
});
|
||||
|
||||
it('applies multiplier correctly', () => {
|
||||
const meterValues: MeterValueDto[] = [
|
||||
{
|
||||
timestamp: '2025-05-29T12:01:00Z',
|
||||
sampledValue: [
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
value: 100,
|
||||
unitOfMeasure: { unit: 'Wh', multiplier: 3 }, // multiplier=3 means *1000, so 100kWh
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamp: '2025-05-29T12:02:00Z',
|
||||
sampledValue: [
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
value: 200,
|
||||
unitOfMeasure: { unit: 'Wh', multiplier: 3 }, // 200kWh
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(100); // 200kWh - 100kWh = 100kWh
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 0 for empty meter values', () => {
|
||||
expect(MeterValueUtils.getTotalKwh([], 0)).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 0 when all values have invalid contexts', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue(
|
||||
'2025-05-29T12:01:00Z',
|
||||
'Energy.Active.Import.Register',
|
||||
100,
|
||||
'kWh',
|
||||
'Other',
|
||||
),
|
||||
makeMeterValue(
|
||||
'2025-05-29T12:02:00Z',
|
||||
'Energy.Active.Import.Register',
|
||||
200,
|
||||
'kWh',
|
||||
'Other',
|
||||
),
|
||||
];
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMeterStart', () => {
|
||||
it('returns the first register value', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:03:00Z', 'Energy.Active.Import.Register', 300),
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Register', 100),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Import.Register', 200),
|
||||
];
|
||||
expect(MeterValueUtils.getMeterStart(meterValues)).toBe(100);
|
||||
});
|
||||
|
||||
it('returns null for empty meter values', () => {
|
||||
expect(MeterValueUtils.getMeterStart([])).toBe(null);
|
||||
});
|
||||
|
||||
it('returns null when there are no register values', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Interval', 50),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Import.Interval', 60),
|
||||
];
|
||||
expect(MeterValueUtils.getMeterStart(meterValues)).toBe(null);
|
||||
});
|
||||
|
||||
it('returns null when all values have invalid contexts', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue(
|
||||
'2025-05-29T12:01:00Z',
|
||||
'Energy.Active.Import.Register',
|
||||
100,
|
||||
'kWh',
|
||||
'Other',
|
||||
),
|
||||
];
|
||||
expect(MeterValueUtils.getMeterStart(meterValues)).toBe(null);
|
||||
});
|
||||
|
||||
it('normalizes units when getting meter start', () => {
|
||||
const meterValues = [
|
||||
makeMeterValue('2025-05-29T12:01:00Z', 'Energy.Active.Import.Register', 5000, 'Wh'),
|
||||
makeMeterValue('2025-05-29T12:02:00Z', 'Energy.Active.Import.Register', 10000, 'Wh'),
|
||||
];
|
||||
expect(MeterValueUtils.getMeterStart(meterValues)).toBe(5); // 5000 Wh = 5 kWh
|
||||
});
|
||||
});
|
||||
|
||||
describe('Phased values', () => {
|
||||
it('sums L1, L2, L3 phased values for register measurand', () => {
|
||||
const meterValues: MeterValueDto[] = [
|
||||
{
|
||||
timestamp: '2025-05-29T12:01:00Z',
|
||||
sampledValue: [
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L1',
|
||||
value: 10,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L2',
|
||||
value: 20,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L3',
|
||||
value: 30,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamp: '2025-05-29T12:02:00Z',
|
||||
sampledValue: [
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L1',
|
||||
value: 20,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L2',
|
||||
value: 40,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L3',
|
||||
value: 60,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
// First reading: 10 + 20 + 30 = 60
|
||||
// Second reading: 20 + 40 + 60 = 120
|
||||
// Difference: 120 - 60 = 60
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(60);
|
||||
});
|
||||
|
||||
it('falls back to L1-N, L2-N, L3-N phased values', () => {
|
||||
const meterValues: MeterValueDto[] = [
|
||||
{
|
||||
timestamp: '2025-05-29T12:01:00Z',
|
||||
sampledValue: [
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L1-N',
|
||||
value: 10,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L2-N',
|
||||
value: 20,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L3-N',
|
||||
value: 30,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamp: '2025-05-29T12:02:00Z',
|
||||
sampledValue: [
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L1-N',
|
||||
value: 50,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L2-N',
|
||||
value: 60,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
{
|
||||
measurand: 'Energy.Active.Import.Register',
|
||||
phase: 'L3-N',
|
||||
value: 70,
|
||||
unitOfMeasure: { unit: 'kWh', multiplier: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
// First: 10 + 20 + 30 = 60, Second: 50 + 60 + 70 = 180
|
||||
expect(MeterValueUtils.getTotalKwh(meterValues, 0)).toBe(120); // 180 - 60
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user