Skip to content

13 โ€” Battery Health

The integration includes a battery-health subsystem that tracks battery wear, charge speed, and per-job efficiency over time. It samples the vacuum's battery level on every state change, classifies each charge session, and rolls everything into twelve Home Assistant sensors plus a per-job battery_metrics block on every completed job.

The point is to spot a degrading battery long before it starts cutting jobs short. With consistent indoor temperatures (your vacuum doesn't experience the cold-garage swings that confound this kind of measurement), the trend is genuinely useful โ€” not lab-grade, but easily good enough to flag a battery that needs replacing 6-12 months before it would otherwise become a problem.

You don't have to do anything to enable it. After the integration loads, sensors start sampling and the at-a-glance view appears in the Metrics โ†’ Battery sub-tab.


What the system tracks

Three categories of data:

  1. Charge wear โ€” cumulative cycles, computed as cumulative drain รท 100. This is the industry-standard wear measure: every 1 % the battery drops adds 0.01 to the cycle count, regardless of whether you charge it back up. After 100 percentage points of total drain (which can take many partial discharges), one cycle is logged.
  2. Charge speed by zone โ€” the rate at which the battery gains percent per minute, tracked separately for the slow regions where degradation shows up first:
  3. Low zone (โ‰ค 29 %) โ€” slow start of the charge curve.
  4. High zone (โ‰ฅ 80 %) โ€” the constant-voltage taper. This is the most sensitive early-warning indicator; as a battery ages, internal resistance rises and the charger has to taper sooner and slower.
  5. Mid-job (15โ†’75) โ€” the recharges the vacuum does mid-clean to keep going. These are gold-standard data: tight, repeatable window in the pure constant-current region with consistent thermal load.
  6. Per-job efficiency โ€” for every completed cleaning job, the integration records battery used, duration, area cleaned (mยฒ), and computes drain rates: %/min, %/hour, and %/mยฒ. Per-mode/suction/water aggregates accumulate over time.

The sensors

After the first restart following install, twelve sensors appear under your vacuum's device:

Sensor Unit What it shows
_charge_cycles (count) Cumulative drain รท 100. Survives HA restarts.
_charge_rate %/min Last instantaneous charge rate.
_charge_rate_low_zone %/min Last rate when battery โ‰ค 29 %.
_charge_rate_high_zone %/min Last rate when battery โ‰ฅ 80 %.
_mid_job_recharge_rate %/min Rolling mean of mid-job recharge rates.
_last_charge_duration min Length of the most recent completed charge session.
_battery_health % Headline charge speed vs install baseline โ€” the _cv_charge_speed signal capped at 100 % (never "healthier than new"). Its uncapped_pct attribute carries the raw, possibly-over-100 value.
_cv_charge_speed % CV regime (80โ†’90 % charging) speed vs baseline โ€” the resistance proxy.
_cc_charge_speed % CC regime (50โ†’80 % charging) speed vs baseline โ€” the capacity proxy.
_last_job_drain_per_min %/min Battery drain rate of the most recent completed job.
_last_job_drain_per_hour %/h Same metric scaled to hours.
_last_job_drain_per_m2 %/mยฒ Battery used per square metre on the most recent job.

The entity ID prefix is the vacuum's object ID โ€” for example, vacuum.alfred produces sensor.alfred_charge_cycles, sensor.alfred_battery_health, sensor.alfred_cc_charge_speed, etc.

Each sensor also exposes rich attribute data for cards and automations. The three _last_job_* sensors carry per-clean-mode, per-fan-speed, and per-water-level aggregate means in their attributes โ€” see advanced/09-battery-health.md for the full attribute list.

_battery_health is clamped at 100 % for the headline, but it exposes the raw (possibly over 100) CV charge-speed value on its uncapped_pct attribute for anyone who wants the unclamped signal. It also carries baseline_cv_min_per_pct, baseline_cc_min_per_pct, baseline_session_count, baseline_anchored_at, and completed_sessions.

Why two charge-speed sensors plus a headline? A battery's charge curve has two distinct regimes that age in opposite directions. Capacity loss makes the constant-current portion (50โ†’80 %) appear faster per percent (less energy per percent), while resistance rise makes the constant-voltage taper (80โ†’90 %) appear slower per percent (the charger has to back off sooner and longer). Averaging them into one number cancels real signal. The integration tracks each regime separately; the headline (_battery_health) is the CV regime sensor (_cv_charge_speed) capped at 100 % (a battery is never "healthier than new"), since CV-side slowdown is what most people mean by "battery health". Watch CC if you want capacity loss; watch CV (or the headline) for resistance rise.


Reading the Battery sub-tab

Open Metrics โ†’ Battery in the dashboard panel. The view has five sections:

  1. Top chips โ€” four headline numbers: cycles, health %, current charge rate, last-job %/mยฒ.
  2. Charge rates by zone โ€” one row per tracked region (overall, low, high, mid-job, last full session) with the most recent rate and a brief note on what that zone is good for.
  3. Drain per mยฒ by single-bucket job โ€” a running mean drain rate per clean mode, fan speed, and water level. Only jobs where every room used the same setting feed these means; mixed-mode runs feed only the All jobs row. Until you've run enough single-bucket jobs, the per-bucket rows show "no single-bucket jobs yet".
  4. Most recent completed job โ€” full per-job metrics. Once the post-job recharge finishes, that session's duration, delta percent, and average rate appear here too โ€” useful for comparing "this job used 25 %" against "the recharge that followed took 73 minutes 60โ†’100 %".
  5. Raw data files โ€” paths to the CSV and JSONL the integration writes (see below).

Charting

You don't need extra software to chart these sensors โ€” they live in HA's recorder like any other. Drop one of these into your dashboard:

type: history-graph
title: Battery health trend
hours_to_show: 720   # 30 days
entities:
  - sensor.alfred_battery_health        # headline (= CV regime)
  - sensor.alfred_cc_charge_speed       # capacity proxy (CC regime)
  - sensor.alfred_mid_job_recharge_rate # rolling mean, third-opinion

Charting CC and CV side-by-side over months is the most informative view: divergence between them tells you whether you're seeing capacity loss (CC drops faster) or resistance rise (CV drops faster), which can point to different replacement urgencies.

For longer windows or per-mode bar charts, apexcharts-card and plotly-graph-card are both excellent. Either can chart the live sensors and pull months of history out of HA's recorder.


Raw data files

For long-term review beyond what the recorder retains, the integration also writes raw data to disk:

config/eufy_vacuum/battery/<object_id>/sessions.csv
config/eufy_vacuum/battery/<object_id>/samples.jsonl
  • sessions.csv โ€” one row per completed charge session: start/end timestamps, duration, start/end battery, delta percent, avg/min/max rate, sample count, and ended reason ("full" vs "stopped"). Open in any spreadsheet.
  • samples.jsonl โ€” every battery sample as a JSON object on its own line: timestamp, battery level, charging flag, delta from previous sample, instantaneous rate, zone, drain added, cumulative cycles. The integration also writes a rejected_delta_pct field on samples where the per-sample guard threw out the observation (firmware glitches, HA-restart gaps); on accepted samples it's null. Easy to tail or process with jq.

These files grow forever; the integration doesn't trim them. If they get unwieldy, archive and rotate them yourself โ€” nothing else reads them, so removing them won't break the live sensors or the card.


What "health %" actually means

The first time the integration sees a qualifying recharge (start โ‰ค 50 %, end โ‰ฅ 90 %), it locks that session in as your baseline โ€” the minutes-per-percent rate for each regime (CC and CV) is recorded as the anchor. After that, the three health-related sensors start publishing. The "current" side of the ratio is a 14-day rolling window of qualifying sessions.

This is a per-install baseline, not an estimate of factory-fresh performance. The integration has no way to know how a brand-new pristine version of your specific battery would charge โ€” that depends on cell chemistry, charger hardware, ambient temperature, and a dozen other factors no software can introspect. So instead of pretending, it anchors on what your battery actually did the first time it was measured. Every future reading is "how does today compare to back then" โ€” which is exactly the question that matters.

CC vs CV: read both regimes, not just the headline

The headline _battery_health shows the CV regime (resistance proxy), capped at 100 %. For most users, that's the signal that matters โ€” CV-phase slowdown is the textbook "battery aging" indicator. But the two regimes age in opposite directions, so reading both gives you the full picture:

  • _cv_charge_speed (the uncapped source of _battery_health) โ€” falls below 100 as resistance rises. The cell can't accept current as fast in the 80โ†’90 % CV taper, so %/min slows. This is what most people mean by "battery health". A drop below 80 over months is a strong signal. Unlike the headline, this diagnostic sensor can read above 100 while the baseline is young.
  • _cc_charge_speed โ€” also falls below 100 as capacity drops. Each percent of a smaller-capacity cell holds less energy, so the constant-current 50โ†’80 % phase appears faster per percent (less to fill), which means the inverted ratio (baseline / current) reads lower. Counterintuitive but consistent with how the math works out.

Both metrics use the same scale: higher = healthier, 100 = matches your baseline. Falls in both over a long horizon are a strong replacement signal. A fall in only one is worth watching but not acting on alone โ€” environmental drift can move either independently for weeks.

Reading the headline number

Health % (the headline) is the ratio of baseline CV speed to current CV speed:

  • 100 % โ€” current CV charge speed matches (or beats) your install baseline. The headline is capped at 100 %, so a battery charging faster than its baseline reads exactly 100 here.
  • Below 100 % โ€” current charges are slower than the baseline. A moderately aged battery drifts to 80-90 %; a battery approaching end-of-life drops below 70 %.

The headline can never read above 100 % โ€” a battery is never "healthier than new". A raw reading above 100 (current charges faster than the baseline, common while the baseline is young, or from measurement noise on a small data set) appears only on the uncapped _cv_charge_speed diagnostic sensor and in the _battery_health sensor's uncapped_pct attribute, where a young battery typically sits around 95-105 %.

The window for what counts as "qualifying" is intentionally wide (50โ†’90 rather than the textbook 30โ†’95): a vacuum that mostly runs single-room jobs rarely drains far enough on its own for the strict window to ever trigger, which would leave the baseline unseeded for months. The 50โ†’90 window covers both the CC region (50โ†’80) and the CV taper (80โ†’90), so a single qualifying recharge is enough to anchor both regimes at once.

The mid-job recharge rate is a useful third opinion alongside the two regime sensors. It's a rolling mean rather than a comparison-to-baseline, so it doesn't have a "100 % means good" interpretation, but its trend over months is the cleanest signal you'll get from this hardware. A steady drop in the mid-job rate is a more reliable sign of degradation than the headline health %.


Seeding the baseline faster

The baseline is anchored on the very first qualifying recharge the integration observes โ€” so if you run heavy enough to drain to โ‰ค 50 % and then let it charge to โ‰ฅ 90 %, that one cycle is enough.

If you don't want to wait for the baseline to seed naturally, force it with a heavy-load job โ€” settings that maximise power draw will get you to โ‰ค 50 % battery in one run:

  1. Queue as many rooms as you can โ€” every room is ideal, but more rooms = more drain whatever the count.
  2. Set max suction (turbo / max โ€” whichever is your top setting).
  3. Set max water level if you're mopping. Mop pads dragged at high pressure pull serious wattage; vacuum-only is fine if you don't have mop pads installed but will drain slower.
  4. Narrow path / 2 passes if your model supports it. Doubles the time-on-job per square metre.
  5. Edge cleaning on. Adds an extra perimeter pass.

Run that and let it dock for an uninterrupted recharge to โ‰ฅ 90 %. As soon as the recharge session closes, the integration anchors both regimes (CC and CV) from that single session, and all three health-related sensors (_battery_health, _cv_charge_speed, _cc_charge_speed) start publishing.

Total time is one job + one recharge โ€” typically 3-5 hours end to end. Best done within the first week or two of installation while the battery is healthy, since this anchor is what every future health % is compared against. Seeding when the battery is already worn means health % will read "100 %" of an already-degraded baseline.

If you've already let the integration run for a while without seeding (existing install, only short jobs / partial charges), forcing one deep cycle still works โ€” the baseline is set the first time it sees a qualifying session in the recent history, regardless of how old the integration is.

Do the recharge part hands-free with a charge step

A run profile can now carry a charge step ("Charge to X%") that docks the vacuum mid-run and waits until the battery reaches a target before continuing โ€” see Steps: charging and waiting mid-run. Adding one that charges to at least 90 % after the heavy-load group above gives you the uninterrupted recharge the baseline needs without having to babysit the dock. The recharge is measured the same way whether you trigger it by hand or as a step โ€” a step just automates the "let it dock and charge up" part.

After replacing the battery

When you swap in a new battery the existing baseline is wrong (it describes the old battery), so health % becomes meaningless. The integration has a service for this โ€” call eufy_vacuum.battery_rebaseline in Developer Tools โ†’ Services with your vacuum's entity ID. The baseline is cleared immediately for both regimes (CC and CV), and the next qualifying recharge re-anchors all of them on the new battery.

The service only clears the baseline anchor โ€” your cycle counter, per-job aggregates, mid-job rate, and session history are all left alone (those are still meaningful: cycles is total wear regardless of which battery, aggregates are about the vacuum's power profile not the battery's age).


Caveats

  • First samples may take a while to be useful. The cycles counter starts at 0 and accumulates; health is None until baseline is seeded; per-mode aggregates need a few single-bucket jobs to mean anything. Expect 1-2 weeks of normal use โ€” or a deliberate seeding pass per the "Seeding the baseline faster" section above โ€” before everything is populated.
  • The vacuum reports SoC %, not actual current/voltage. All the rates here are observed minutes per percentage point โ€” a useful proxy that conflates battery condition, charger behaviour, and ambient temperature. Indoor use stabilises temperature; the charger is fixed; so what's left is mostly the battery.
  • Don't read a single bad week as "the battery is failing". Trends matter more than instantaneous values. Same-season comparisons across months are more diagnostic than month-to-month within a year.
  • Mid-job recharges are gold. If you don't run multi-room jobs that trigger them, the most reliable signal is unavailable; the per-install baseline approach still works but is noisier.

  • The baseline is whatever the first qualifying recharge happened to be. If that first recharge happened on a hot day, with mop pads dragging on a heavy water level, after a long job โ€” your baseline reflects all of that. The point is consistency, not perfection: the trend over months tells you the story, not the absolute number. If you want a cleaner anchor, see "Seeding the baseline faster" โ€” running a planned heavy-load job under known settings gives you a deliberate baseline rather than whatever the integration happened to catch first.

For the full math, attribute schemas, zone-definition rationale, and automation ideas, see advanced/09-battery-health.md.