diff --git a/CHANGELOG.md b/CHANGELOG.md index f39b8f3..e81c740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # CHANGELOG +## Version 2.3.1, 2026-07-20, `kezarjg` + +- Fix: correct the inverted battery-power sign on smart 453/forfour (`SQ`) and smart + ED/fortwo (`SE`). Both OVMS smart modules report `v.b.power` with the wrong sign — + negative while driving (consuming), positive while charging — the opposite of the + OVMS core / Iternio convention. ABRP uses `power` for consumption calibration, so it + read consumption as regen and the calibrated reference consumption drifted toward zero + (issue #40). `overrideMetricMap` now corrects `power` for these vehicles by keeping the + module's power magnitude and taking its sign from `v.b.current`, which the same modules + report correctly. This is self-healing: `v.b.current` is correct in both the current + (buggy) and a future upstream-fixed firmware, so once the module's `v.b.power` sign is + fixed the correction becomes a no-op — no plugin change needed to retire it. Only + `power` is affected; `current`, `is_charging`, and `is_dcfc` are unchanged. Upstream + firmware bugs filed separately against `vehicle_smarteq` and `vehicle_smarted`. + ## Version 2.3.0, 2026-06-25, `kezarjg` - Switched telemetry to batched bulk uploads (`/1/tlm/bulk`) behind a queue, with diff --git a/lib/abrp.js b/lib/abrp.js index 860f07f..fcb1652 100644 --- a/lib/abrp.js +++ b/lib/abrp.js @@ -5,7 +5,7 @@ // Module constants const OVMS_API_KEY = '32b2162f-9599-4647-8139-66e9f9528370' -const VERSION = '2.3.0' +const VERSION = '2.3.1' const Logger = logger() // Configuration constants @@ -311,6 +311,25 @@ function overrideMetricMap() { }; } break; + case 'SQ': + case 'SE': + // smart 453/forfour (SQ) and smart ED/fortwo (SE) report v.b.power with an + // inverted sign (negative while driving/consuming) until the upstream firmware + // fix lands (see iternio/ovms-link#40). v.b.current is correctly signed + // (discharge-positive) in BOTH the buggy and the fixed firmware, so keep power's + // magnitude and take its sign from current: corrects the bug now, and becomes a + // no-op once v.b.power's sign is fixed upstream (no plugin change needed). + if (entry.key === 'power') { + entry.requiredMetrics = ['v.b.power', 'v.b.current']; + entry.metric = function(metrics) { + var power = metrics['v.b.power']; + var current = metrics['v.b.current']; + if (current < 0) { return -Math.abs(power); } + if (current > 0) { return Math.abs(power); } + return power; // current == 0: power is ~0, sign irrelevant + }; + } + break; // Add cases for other vehicle types as needed } }); diff --git a/lib/abrp.test.js b/lib/abrp.test.js index 4b65cef..8c579fc 100644 --- a/lib/abrp.test.js +++ b/lib/abrp.test.js @@ -816,3 +816,65 @@ describe('charge/drive overlap state machine', () => { expect(count('ticker.1')).toBe(1) // sampler subscribed at startup }) }) + +// smart 453/forfour (SQ) and smart ED/fortwo (SE) report v.b.power with an inverted +// sign (charge-positive) until the upstream firmware fix; v.b.current is correctly +// signed in both firmware states. overrideMetricMap corrects power by keeping its +// magnitude and taking its sign from current: fixes the bug now, no-op once fixed +// upstream. See iternio/ovms-link#40. +describe('overrideMetricMap: smart SQ/SE power sign correction', () => { + function withSmart(present, vtype) { + const abrp = loadAbrp({ + OvmsMetrics: { + HasValue: (k) => Object.prototype.hasOwnProperty.call(present, k), + GetValues: (keys) => { + const o = {} + keys.forEach((k) => { + o[k] = present[k] + }) + return o + }, + Value: (k) => (k === 'v.type' ? vtype : present[k]), + }, + }) + abrp.__test.overrideMetricMap() + return abrp + } + + test('SQ: inverted power while driving is corrected to discharge-positive', () => { + // driving: current +9A (correct, discharge), power reported -3.0kW (inverted) + const abrp = withSmart({ 'v.b.power': -3.0, 'v.b.current': 9 }, 'SQ') + expect(abrp.getOVMSMetric('power')).toEqual([true, 3.0]) + }) + + test('SQ: inverted power while charging is corrected to charge-negative', () => { + // charging: current -20A (correct, charge), power reported +7.0kW (inverted) + const abrp = withSmart({ 'v.b.power': 7.0, 'v.b.current': -20 }, 'SQ') + expect(abrp.getOVMSMetric('power')).toEqual([true, -7.0]) + }) + + test('SQ: already-correct power (post upstream fix) passes through unchanged', () => { + const abrp = withSmart({ 'v.b.power': 3.0, 'v.b.current': 9 }, 'SQ') + expect(abrp.getOVMSMetric('power')).toEqual([true, 3.0]) + }) + + test('SQ: zero current leaves power as-is (magnitude ~0, sign irrelevant)', () => { + const abrp = withSmart({ 'v.b.power': -0.02, 'v.b.current': 0 }, 'SQ') + expect(abrp.getOVMSMetric('power')).toEqual([true, -0.02]) + }) + + test('SE shares the smart override: inverted power corrected', () => { + const abrp = withSmart({ 'v.b.power': -3.0, 'v.b.current': 9 }, 'SE') + expect(abrp.getOVMSMetric('power')).toEqual([true, 3.0]) + }) + + test('SQ: power is unsupported when v.b.current is absent', () => { + const abrp = withSmart({ 'v.b.power': -3.0 }, 'SQ') + expect(abrp.getOVMSMetric('power')).toEqual([false, null]) + }) + + test('non-smart vehicle: power is a plain passthrough (no sign correction)', () => { + const abrp = withSmart({ 'v.b.power': -3.0, 'v.b.current': 9 }, 'UNKNOWN') + expect(abrp.getOVMSMetric('power')).toEqual([true, -3.0]) + }) +})