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:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -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
});
});
});