Real-Time Battery State-of-Health Monitoring for EVs

Real-Time Battery State-of-Health Monitoring for EVs

Battery state-of-health (SoH) is the ratio of a battery's current usable capacity to its original rated capacity. Monitoring SoH in real time — rather than recalculating it offline overnight — lets fleet operators and charging network managers intervene before a degraded pack causes a failed charge session or an unexpected range shortfall.

Why Battery SoH Monitoring Matters

An EV battery pack degrades gradually through charge cycles, temperature extremes, and high C-rate charging events. By the time SoH drops below 80% — the industry threshold for pack replacement — the degradation has been happening for months. The challenge is detecting that trend early enough to act: schedule a service appointment, adjust charging limits on the OCPP charger, or flag the vehicle for fleet rotation.

Traditional approaches compute SoH in a nightly batch job by comparing historical charge curves. This introduces a 12-to-24-hour blind spot. If a pack degrades sharply during a day of fast DC charging (150 kW CCS sessions), the first signal arrives the next morning — after another full day of potentially damaging use.

A streaming approach processes each MeterValues event from the charging station as it arrives, computing rolling SoH estimates and comparing them to the vehicle's baseline capacity. Alerts fire within seconds of a degradation anomaly rather than overnight.

The Streaming SQL Solution

RisingWave reads battery telemetry from Kafka. Materialized views compute per-vehicle rolling averages, cycle counts, and capacity fade. A sink pushes SoH-below-threshold alerts to a notification topic.

The streaming pipeline replaces a combination of a Python batch script, a time-series database, and a custom alerting daemon. All three concerns are expressed in SQL and maintained incrementally by RisingWave.

Tutorial: Building It Step by Step

Step 1: Set Up the Data Source

-- Battery telemetry from BMS (Battery Management System) via Kafka
CREATE SOURCE battery_telemetry (
    vehicle_id          VARCHAR,
    session_id          VARCHAR,
    station_id          VARCHAR,
    connector_type      VARCHAR,   -- CCS | CHAdeMO
    event_ts            TIMESTAMPTZ,
    voltage_v           DOUBLE PRECISION,
    current_a           DOUBLE PRECISION,
    soc_percent         INT,       -- State of Charge 0-100
    soh_percent         DOUBLE PRECISION, -- State of Health reported by BMS
    temperature_c       DOUBLE PRECISION,
    energy_kwh          DOUBLE PRECISION, -- cumulative energy this session
    charge_power_kw     DOUBLE PRECISION,
    pack_rated_kwh      DOUBLE PRECISION  -- manufacturer-rated capacity
)
WITH (
    connector = 'kafka',
    topic = 'ev.battery.telemetry',
    properties.bootstrap.server = 'kafka:9092',
    scan.startup.mode = 'latest'
) FORMAT PLAIN ENCODE JSON;

-- Vehicle baseline capacity registry (from PostgreSQL CDC or static seed)
CREATE SOURCE vehicle_baselines (
    vehicle_id          VARCHAR,
    manufacturer        VARCHAR,
    model_year          INT,
    rated_capacity_kwh  DOUBLE PRECISION,
    registered_at       TIMESTAMPTZ
)
WITH (
    connector = 'kafka',
    topic = 'ev.vehicle.baselines',
    properties.bootstrap.server = 'kafka:9092',
    scan.startup.mode = 'earliest'
) FORMAT PLAIN ENCODE JSON;

Step 2: Build Real-Time Aggregations

-- Per-vehicle SoH trend over last 24 hours (sliding window)
CREATE MATERIALIZED VIEW vehicle_soh_trend AS
SELECT
    t.vehicle_id,
    window_start,
    window_end,
    AVG(t.soh_percent)                    AS avg_soh_pct,
    MIN(t.soh_percent)                    AS min_soh_pct,
    MAX(t.temperature_c)                  AS max_temp_c,
    AVG(t.charge_power_kw)                AS avg_charge_power_kw,
    COUNT(DISTINCT t.session_id)          AS charge_sessions,
    SUM(t.energy_kwh)                     AS total_energy_kwh
FROM HOP(battery_telemetry t, event_ts, INTERVAL '1 HOUR', INTERVAL '24 HOURS')
GROUP BY t.vehicle_id, window_start, window_end;

-- Capacity fade detection: compare latest measured capacity to rated
CREATE MATERIALIZED VIEW capacity_fade AS
SELECT
    t.vehicle_id,
    b.rated_capacity_kwh,
    MAX(t.energy_kwh)                     AS measured_session_kwh,
    MAX(t.soh_percent)                    AS latest_soh_pct,
    b.rated_capacity_kwh * MAX(t.soh_percent) / 100.0 AS estimated_usable_kwh,
    b.rated_capacity_kwh - (b.rated_capacity_kwh * MAX(t.soh_percent) / 100.0) AS capacity_loss_kwh,
    MAX(t.event_ts)                       AS last_updated
FROM battery_telemetry t
JOIN vehicle_baselines b ON t.vehicle_id = b.vehicle_id
GROUP BY t.vehicle_id, b.rated_capacity_kwh;

-- High-temperature charging sessions (thermal stress indicator)
CREATE MATERIALIZED VIEW thermal_stress_sessions AS
SELECT
    vehicle_id,
    session_id,
    connector_type,
    window_start,
    window_end,
    AVG(temperature_c)                    AS avg_temp_c,
    MAX(temperature_c)                    AS peak_temp_c,
    AVG(charge_power_kw)                  AS avg_power_kw,
    SUM(energy_kwh)                       AS energy_kwh
FROM TUMBLE(battery_telemetry, event_ts, INTERVAL '10 MINUTES')
WHERE temperature_c > 40.0
GROUP BY vehicle_id, session_id, connector_type, window_start, window_end;

Step 3: Detect Anomalies or Generate Alerts

-- Alert when SoH drops below 80% (pack replacement threshold)
CREATE MATERIALIZED VIEW soh_alerts AS
SELECT
    vehicle_id,
    latest_soh_pct,
    rated_capacity_kwh,
    estimated_usable_kwh,
    capacity_loss_kwh,
    last_updated,
    CASE
        WHEN latest_soh_pct < 70 THEN 'CRITICAL'
        WHEN latest_soh_pct < 80 THEN 'WARNING'
        ELSE 'OK'
    END AS alert_level
FROM capacity_fade
WHERE latest_soh_pct < 80;

-- Sink: push SoH alerts to Kafka for fleet management system
CREATE SINK soh_alert_sink
FROM soh_alerts
WITH (
    connector = 'kafka',
    topic = 'alerts.battery.soh',
    properties.bootstrap.server = 'kafka:9092'
) FORMAT PLAIN ENCODE JSON;

-- Sink: rapid thermal stress alert (battery at risk during current session)
CREATE SINK thermal_stress_alert_sink
FROM (
    SELECT
        vehicle_id,
        session_id,
        connector_type,
        peak_temp_c,
        avg_power_kw,
        window_end AS detected_at,
        'THERMAL_STRESS' AS alert_type
    FROM thermal_stress_sessions
    WHERE peak_temp_c > 45.0
)
WITH (
    connector = 'kafka',
    topic = 'alerts.battery.thermal',
    properties.bootstrap.server = 'kafka:9092'
) FORMAT PLAIN ENCODE JSON;

Comparison Table

Nightly Batch SoH JobRisingWave Streaming SQL
Detection latency12–24 hoursSeconds
Session contextReconstructed from logsNative per-session windows
Temperature correlationPost-hoc joinInline window aggregation
Alert deliveryEmail digest next morningReal-time Kafka sink
InfrastructureSpark/Python + schedulerSingle streaming SQL layer
State managementExternal Redis or RDBBuilt into RisingWave

FAQ

What BMS protocols can feed into this pipeline? The pipeline is protocol-agnostic at the RisingWave layer. You need a Kafka producer that converts your BMS protocol (CAN bus, OCPP MeterValues, proprietary OEM API) into JSON messages on the Kafka topic. RisingWave reads the resulting JSON.

Can I combine OCPP station data with vehicle telemetry in the same view? Yes. Define separate sources for OCPP events and vehicle BMS data, then join them on session_id or vehicle_id in a materialized view. RisingWave handles the stream-stream join with watermark-based state cleanup.

How accurate is streaming SoH compared to full coulomb counting? The accuracy depends on your BMS reporting frequency and the quality of the SoH value in the MeterValues payload. RisingWave computes statistical aggregations over the reported values; it does not replace a physics-based electrochemical model.

What does RisingWave do when battery_telemetry messages arrive out of order? RisingWave uses watermarks to handle late-arriving events. You configure the watermark clause on your source or window to tolerate a bounded out-of-order delay, after which late events are dropped or placed in the correct window depending on your configuration.

Key Takeaways

  • Streaming SoH monitoring reduces detection latency from overnight to seconds by processing each MeterValues OCPP event as it arrives.
  • HOP windows over 24-hour intervals provide a rolling trend, while TUMBLE windows over 10-minute intervals catch session-level thermal stress events.
  • Joining battery telemetry with a vehicle baseline source in a materialized view enables capacity-fade calculations without any application code.
  • Alert sinks publish to Kafka in real time, connecting the streaming pipeline to existing fleet management systems and notification workflows.

Best-in-Class Event Streaming
for Agents, Apps, and Analytics
GitHubXLinkedInSlackYouTube
Sign up for our to stay updated.