Infer charging state from trend of last 50 data points
When no state field is configured, the panel now uses a least-squares slope over the most recent ≤ 50 data points to determine whether the battery is charging, on battery, or stable — instead of relying on the overall first-to-last rate which could be misleading over long ranges. Priority order for state resolution: 1. Explicit state field (if enabled and configured) 2. Inferred trend from last ≤ 50 points (always computed) The slope threshold (0.01) avoids flicker from sensor noise. Status badge now shows 'On Battery' instead of 'Discharging'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -42,6 +42,40 @@ function slopePercentPerMinute(times: number[], values: number[]): number {
|
|||||||
return den === 0 ? 0 : num / den;
|
return den === 0 ? 0 : num / den;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Infer battery state from the trend of the last ≤ maxPoints values. */
|
||||||
|
function inferStateFromTrend(
|
||||||
|
values: number[],
|
||||||
|
maxPoints = 50,
|
||||||
|
threshold = 0.01,
|
||||||
|
): 'charging' | 'discharging' | 'stable' {
|
||||||
|
const tail = values.length > maxPoints ? values.slice(-maxPoints) : values;
|
||||||
|
if (tail.length < 2) {
|
||||||
|
return 'stable';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple least-squares slope over the tail window
|
||||||
|
const n = tail.length;
|
||||||
|
let sumX = 0;
|
||||||
|
let sumY = 0;
|
||||||
|
let sumXY = 0;
|
||||||
|
let sumXX = 0;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
sumX += i;
|
||||||
|
sumY += tail[i];
|
||||||
|
sumXY += i * tail[i];
|
||||||
|
sumXX += i * i;
|
||||||
|
}
|
||||||
|
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
||||||
|
|
||||||
|
if (slope > threshold) {
|
||||||
|
return 'charging';
|
||||||
|
}
|
||||||
|
if (slope < -threshold) {
|
||||||
|
return 'discharging';
|
||||||
|
}
|
||||||
|
return 'stable';
|
||||||
|
}
|
||||||
|
|
||||||
/** Find a field by configured name, falling back to first field of given type. */
|
/** Find a field by configured name, falling back to first field of given type. */
|
||||||
function resolveField(
|
function resolveField(
|
||||||
fields: Field[],
|
fields: Field[],
|
||||||
@@ -325,6 +359,7 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options,
|
|||||||
let hi = 0;
|
let hi = 0;
|
||||||
let loadValue: number | undefined;
|
let loadValue: number | undefined;
|
||||||
let stateFromField: 'charging' | 'discharging' | 'stable' | undefined;
|
let stateFromField: 'charging' | 'discharging' | 'stable' | undefined;
|
||||||
|
let inferredState: 'charging' | 'discharging' | 'stable' = 'stable';
|
||||||
|
|
||||||
if (data.series.length > 0) {
|
if (data.series.length > 0) {
|
||||||
const series = data.series[0];
|
const series = data.series[0];
|
||||||
@@ -361,6 +396,9 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options,
|
|||||||
r = deltaMinutes !== 0 ? deltaPercent / deltaMinutes : 0;
|
r = deltaMinutes !== 0 ? deltaPercent / deltaMinutes : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Infer state from recent trend (last ≤ 50 points)
|
||||||
|
inferredState = inferStateFromTrend(values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,14 +434,14 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { currentValue: cur, rate: r, low: lo, high: hi, loadValue, stateFromField };
|
return { currentValue: cur, rate: r, low: lo, high: hi, loadValue, stateFromField, inferredState };
|
||||||
}, [
|
}, [
|
||||||
data.series, useRegression, enableLoad, enableStateField,
|
data.series, useRegression, enableLoad, enableStateField,
|
||||||
options.chargeField, options.loadField, options.stateField,
|
options.chargeField, options.loadField, options.stateField,
|
||||||
stateChargingValue, stateDischargingValue,
|
stateChargingValue, stateDischargingValue,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { currentValue, rate, low, high, loadValue, stateFromField } = extracted;
|
const { currentValue, rate, low, high, loadValue, stateFromField, inferredState } = extracted;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Derived visual values
|
// Derived visual values
|
||||||
@@ -420,9 +458,10 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options,
|
|||||||
let startColour = fillColour;
|
let startColour = fillColour;
|
||||||
let endColour = fillColour;
|
let endColour = fillColour;
|
||||||
|
|
||||||
// Determine charging state: prefer state field, else use computed rate
|
// Determine charging state: state field → inferred trend from last ≤50 points → stable
|
||||||
const isCharging = stateFromField ? stateFromField === 'charging' : rate > 0;
|
const resolvedState = stateFromField ?? inferredState;
|
||||||
const isDischarging = stateFromField ? stateFromField === 'discharging' : rate < 0;
|
const isCharging = resolvedState === 'charging';
|
||||||
|
const isDischarging = resolvedState === 'discharging';
|
||||||
|
|
||||||
if (gradientFill) {
|
if (gradientFill) {
|
||||||
if (isCharging) {
|
if (isCharging) {
|
||||||
@@ -622,7 +661,7 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options,
|
|||||||
{/* Status badge */}
|
{/* Status badge */}
|
||||||
{showStatus && !isCompact && (
|
{showStatus && !isCompact && (
|
||||||
<div className={statusBadgeStyle}>
|
<div className={statusBadgeStyle}>
|
||||||
{isCharging ? '⚡ Charging' : isDischarging ? '↓ Discharging' : '— Stable'}
|
{isCharging ? '⚡ Charging' : isDischarging ? '🔋 On Battery' : '— Stable'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user