From 34c3d2728247d5f481909f1d46045ae3e60f8212 Mon Sep 17 00:00:00 2001 From: Jerry Kezar Date: Mon, 20 Jul 2026 21:35:41 +0000 Subject: [PATCH] fix(smart): correct inverted v.b.power sign on SQ/SE (issue #40) Both OVMS smart modules (453/forfour = SQ, ED/fortwo = SE) report v.b.power with an inverted 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. 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. Because v.b.current is correct in both the buggy and a future upstream-fixed firmware, the correction is self-healing: once v.b.power's sign is fixed upstream it becomes a no-op, with no plugin change needed to retire it. Only power is affected. Upstream firmware bugs filed separately against vehicle_smarteq and vehicle_smarted. --- CHANGELOG.md | 15 ++++++++++++ lib/abrp.js | 21 +++++++++++++++- lib/abrp.test.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) 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]) + }) +})