Skip to content
Merged

Beta #335

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
99ae758
feat(india): add charging data feature and implement get_charging_inf…
Dr-Blank Aug 18, 2026
27c6e4b
feat(india): enhance charging data handling and add charging duration…
Dr-Blank Aug 19, 2026
f0171c4
Merge branch 'beta' of upstream into feat/india-charging
Dr-Blank Aug 19, 2026
68a58b8
fix(sensor): treat bmsPackSOCDsp=0 as invalid, fall back to extendedD…
james Aug 24, 2026
daf4b27
fix(sensor): create SOC sensor for HEV using extendedData1 (#318)
james Aug 25, 2026
b2f1b8b
docs: document MG3 Hybrid+ HEV SOC support, bump to 1.2.7-beta1
james Aug 25, 2026
ecd0deb
Merge pull request #320 from townsmcp/fix/soc-extended-data-fallback-…
townsmcp Aug 25, 2026
73acc71
fix(india): translate unavailable charge status into no charging data
Dr-Blank Aug 26, 2026
bb2403f
feat(trip-stats): expose counter vs SOC/odometer figures side-by-side…
townsmcp Aug 26, 2026
5a641f3
Merge pull request #321 from townsmcp/feat/soc-based-efficiency-compa…
townsmcp Aug 26, 2026
73756b7
fix(sensor): Last Trip Distance wasn't exposing any attributes
townsmcp Aug 26, 2026
711e569
Update version to 1.2.7-beta3
townsmcp Aug 26, 2026
6eedc98
Merge pull request #322 from townsmcp/feat/soc-based-efficiency-compa…
townsmcp Aug 26, 2026
193eb6f
Merge branch 'beta' of upstream into feat/india-charging
Dr-Blank Aug 27, 2026
f4c9611
fix(sensor): gate non-BEV SOC on its own feature
Dr-Blank Aug 27, 2026
394dea6
feat(india): consume charging client 0.1.6
Dr-Blank Aug 27, 2026
dd7292a
feat(P12L): mode-select climate profile for MG IM5 (#326)
townsmcp Aug 27, 2026
9e64cb8
Merge pull request #327 from townsmcp/fix/im5-p12l-climate-profile
townsmcp Aug 27, 2026
794c208
feat(P12L): confirm battery capacity 100kWh for Long Range (#326)
townsmcp Aug 27, 2026
e392a0d
fix(sensors): unbreak Efficiency Since Charge (SOC) and the 3x energy…
townsmcp Aug 30, 2026
c2a70fa
chore: bump to 1.2.7-beta5
townsmcp Aug 30, 2026
ea84e4e
Merge pull request #330 from townsmcp/fix/since-charge-energy-and-las…
townsmcp Aug 30, 2026
1403cfb
refactor: move the energy correction and odometer lookup into logic.p…
townsmcp Aug 30, 2026
63ef073
Merge pull request #331 from townsmcp/refactor/energy-and-odometer-he…
townsmcp Aug 30, 2026
7d2901e
Merge remote-tracking branch 'origin/beta' into feat/india-charging
john-lazarus Aug 30, 2026
b50bb86
fix(india): consume charging data from status poll
john-lazarus Aug 30, 2026
5af8d04
Merge pull request #302 from Dr-Blank/feat/india-charging
john-lazarus Aug 30, 2026
c74a47d
Update version to 1.2.7-beta6
townsmcp Aug 30, 2026
29140a1
feat(capacity): resolve battery capacity once, with the API as a real…
townsmcp Aug 30, 2026
3403f83
Merge pull request #332 from townsmcp/feat/central-battery-capacity-r…
townsmcp Aug 30, 2026
fb5adaa
feat(charge): report the range a charge added (#262)
townsmcp Aug 31, 2026
c8b0089
Update version to 1.2.7-beta8 in manifest.json
townsmcp Aug 31, 2026
8cdcd35
Merge beta (1.2.7-beta7) into feat/charge-session-range-added
townsmcp Aug 31, 2026
e726f77
docs: cover the capacity resolver (#332) and annotate Added Electric …
townsmcp Aug 31, 2026
c8e8d4f
Merge #328's P12L capacity work, at usable rather than nominal capacity
townsmcp Aug 31, 2026
f876b6e
feat: Last Charge Range Added sensor, and usable capacity for IM6 (#2…
townsmcp Aug 31, 2026
467e39e
docs: IM6 has no 75 kWh option in the UK/EU
townsmcp Aug 31, 2026
9601414
Merge pull request #333 from townsmcp/feat/charge-session-range-added
townsmcp Aug 31, 2026
d3d52e6
fix: restore missing electric_range_km import, repoint Estimated Rang…
townsmcp Aug 31, 2026
040e73d
Add pyflakes check for undefined names
townsmcp Aug 31, 2026
179d8aa
Merge pull request #334 from townsmcp/fix/coordinator-import-and-esti…
townsmcp Aug 31, 2026
c02c2cd
Update version to 1.2.7 in manifest.json
townsmcp Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/python-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,20 @@ jobs:
print(next(req for req in requirements if req.startswith("mg-ismart-india-client")))
PY
)"
- name: Check for undefined names
# A missing import is invisible to py_compile and to unit tests that
# never reach the line — 1.2.7-beta8 shipped a NameError in the
# coordinator's poll path this way. pyflakes catches the whole class
# in under a second. Only undefined names fail the build; unused
# imports and similar are reported but tolerated.
run: |
python -m pip install pyflakes
python -m pyflakes custom_components/mg_saic > pyflakes.txt || true
cat pyflakes.txt
if grep -q "undefined name" pyflakes.txt; then
echo "::error::pyflakes found undefined name(s) — see above"
exit 1
fi

- name: Run unit tests
run: python -m unittest discover -s tests -p "test_*.py"
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ The MG/SAIC Custom Integration provides the following sensors, binary sensors, a
- Tyre Pressure Rear Left
- Tyre Pressure Rear Right
#### Electric / Hybrid
- State of Charge (SOC)
- State of Charge (SOC) *(BEV/PHEV; also HEV on self-charging hybrids with no charge port, e.g. MG3 Hybrid+ — see [Vehicle Profiles](#vehicle-profiles))*
- Electric Range
- Instant Power *(kW draw/regen while driving; negative = traction, positive = regen/charge)*
- Fuel Level *(PHEV/HEV/ICE only)*
Expand All @@ -150,14 +150,17 @@ The MG/SAIC Custom Integration provides the following sensors, binary sensors, a
- Charging Current
- Charging Current Limit
- Charging Power
- Estimated Range After Charging
- Estimated Range After Charging *(the range the car expects to reach when the current charge completes)*
- Target SOC *(read-only mirror of the Target SOC slider — shown only on models where the iSmart app supports it)*
- Charging Duration
- Remaining Charging Time
- Added Electric Range
- Added Electric Range *(live during a charge session only, and not reported at all by most cars — see [Trip & efficiency statistics](#trip--efficiency-statistics))*
- Power Usage Since Last Charge
- Mileage Since Last Charge
- Efficiency Since Last Charge *(BEV/PHEV; km/kWh, derived from the two sensors above — see [Trip & efficiency statistics](#trip--efficiency-statistics))*
- Efficiency Since Charge (SOC) *(BEV/PHEV; km/kWh, an SOC/odometer-only alternative independent of the counters above — see [Trip & efficiency statistics](#trip--efficiency-statistics))*
- Last Charge Range Added *(BEV/PHEV; electric range the last completed charge put back — shown in your Home Assistant unit system, so miles if that's what you use)*
- Last Charge Energy *(BEV/PHEV; kWh put **into** the battery by the last completed charge — see [Trip & efficiency statistics](#trip--efficiency-statistics))*
- Last Trip Distance *(distance driven on the last completed drive)*
- Last Trip Efficiency *(BEV/PHEV; switchable km/kWh · mi/kWh · kWh/100km, full breakdown in attributes)*
- Last Trip Fuel Economy *(ICE/HEV/PHEV; L/100km, with the full breakdown in its attributes)*
Expand All @@ -171,9 +174,26 @@ The integration derives per-trip and per-charge efficiency from data it already

**Efficiency Since Last Charge** *(BEV/PHEV)* comes straight from the car's own `Mileage Since Last Charge` and `Power Usage Since Last Charge` figures, so it's available immediately and needs no trip tracking.

**Efficiency Since Charge (SOC)** *(BEV/PHEV)* is an alternative to the sensor above, computed entirely from the odometer and battery percentage — it never touches the `Mileage Since Last Charge` / `Power Usage Since Last Charge` fields at all. It exists because those fields are unreliable on some cars (they can reset spuriously without an actual charge — see below) and permanently unpopulated (`Unknown`) on others; this sensor works either way, and lets you compare the two where both are available. Its "since charge" point is whenever the car's battery percentage was last seen to rise while parked, which may not always be a full charge to 100%.

**Last Charge Energy** *(BEV/PHEV)* reports how much energy the last completed charge put **into** the battery. The API has no field for this — it reports charging power live, and `Power Usage Since Last Charge` (energy taken back *out* afterwards), but there is no "starting power" to subtract from `lastChargeEndingPower` — so the session is measured across its start and end. Two independent figures are produced, and both appear in the attributes:

- `energy_added_kWh_soc` — the rise in battery percentage × the usable capacity. This is the headline value, because it works on any car that reports SOC and has a known capacity (see [Battery capacity override](#battery-capacity-override) if yours is wrong).
- `energy_added_kWh_counter` — the change in the car's own pack-energy figure (`lastChargeEndingPower` minus `Power Usage Since Last Charge`). Independent of the capacity figure, but it relies on the car refreshing `lastChargeEndingPower` promptly when the charge ends, so it's omitted when it doesn't look plausible.

Also in the attributes: `range_added_km` (with `range_start_km` / `range_end_km`), `soc_start_pct`, `soc_end_pct`, `soc_added_pct`, `duration_s`, `average_power_kW`, `method` (which figure was used), and the session's start/end timestamps. A `mg_saic_charge_completed` event fires when a charge finishes, carrying the same data, so you can log or notify on it.

The same figure is also published as its own **Last Charge Range Added** sensor. Prefer that one for dashboards: sensor states are converted to your Home Assistant unit system (so miles on an imperial setup), whereas attribute values never are — the `*_km` attributes below are always kilometres regardless of your settings.

`range_added_km` is the electric range the charge added, measured across the session. Note this is *not* the same as the **Added Electric Range** sensor, which exposes the API's own `chrgngAddedElecRng` — a live counter that runs during a session and resets when it ends, and which on the cars observed so far stays at 0 throughout. The range delta here is derived from the electric range reading at each boundary instead.

Note the energy figure is measured **at the battery**, so it will read lower than a wall meter or smart charger, which also pay for charger and cable losses. A charge that delivers less than 0.5% is ignored (that's the small percentage rebound the pack reports after a drive, not a charge), and a session left open more than 48 hours is abandoned rather than reported. A charging-data dropout is never mistaken for the end of a charge — on some cars the charging endpoint goes quiet the moment a session completes.

**Last Trip** sensors are populated when a drive ends (the car powers off). Distance and electric energy come from the car's own cumulative counters (`Mileage Since Last Charge` / `Power Usage Since Last Charge`), diffed between one trip and the next — so they match the car's own measurements and don't depend on exactly when the trip was detected. (For non-charging models, distance falls back to the odometer.) A charge between trips is handled automatically (the counters reset). A trip is one power-on to power-off, so a journey with a stop in the middle counts as two trips.

On some cars, the since-charge counters occasionally reset on their own without an actual charge. If that happens mid-trip, the trip falls back to the odometer for distance and to the battery-percentage change for energy, and carries a `counter_reset_detected` attribute so it's visible when this happened.
Because the counters aren't always trustworthy (see below), `Last Trip Distance` and `Last Trip Efficiency` also expose the counter-only and odometer/SOC-only figures **independently**, as attributes, alongside the primary (counter-preferred) value — so you can compare them directly for any trip: `distance_km_counter` / `distance_mi_counter` and `distance_km_odometer` / `distance_mi_odometer` on Last Trip Distance; `energy_kWh_counter` / `efficiency_km_per_kWh_counter` / `efficiency_mi_per_kWh_counter` / `consumption_kWh_per_100km_counter` / `consumption_kWh_per_100mi_counter` and the equivalent `_soc` set on Last Trip Efficiency. The counter figures are shown raw/unfiltered, even on a trip where the primary figure discarded them (see `counter_reset_detected` below) — seeing what the counter actually reported is itself useful.

On some cars, the since-charge counters occasionally reset on their own without an actual charge. If that happens mid-trip, the primary trip figure falls back to the odometer for distance and to the battery-percentage change for energy, and carries a `counter_reset_detected` attribute so it's visible when this happened.

If a drive is never seen live — the car wasn't polled while it was powered (a short trip that fell between polls, or a missed vehicle-start message) — the trip is reconstructed afterwards from the odometer movement once the car is next seen parked. These reconstructed trips carry `retrospective: true` and `timing: approximate` attributes, because the exact start/end times aren't known and several short hops in the same gap may be merged into one. If a trip ever gets stuck "open" (its power-off was missed), it's force-closed automatically so it doesn't block new trips.

Expand Down Expand Up @@ -518,7 +538,9 @@ Some MG models share one series code across several battery sizes (the MG4, for

The **Usable battery capacity override (kWh)** option (under **Configure**) lets you set your car's usable capacity yourself. When set, it takes priority over both our built-in per-model value and the API-reported value, and it becomes the figure used everywhere capacity matters: the **Total Battery Capacity** sensor and the electric energy/efficiency calculations (including Last Trip figures on models that fall back to a battery-percentage estimate). Enter the **usable** capacity for your variant; leave it blank to go back to the automatic value. Saving the option takes effect immediately — no restart or reload needed.

The Total Battery Capacity sensor carries a `capacity_source` attribute (`user_override`, `profile`, or `api`) so you can see — and template off — exactly where the displayed figure came from.
The Total Battery Capacity sensor carries a `capacity_source` attribute (`user_override`, `profile`, or `api`) so you can see — and template off — exactly where the displayed figure came from. The same resolved figure feeds every energy calculation derived from capacity, so the displayed pack size and the sensors derived from it can't disagree.

Where a car reports a capacity that can't be trusted, none is used: the `totalBatteryCapacity=725` placeholder (→ 72.5 kWh) is rejected outright, as is anything outside 5–200 kWh. On such a car with no profile figure and no override, Total Battery Capacity reads blank and `capacity_source` is absent, rather than showing a number the car invented and deriving charge and efficiency figures from it. Setting a [battery capacity override](#battery-capacity-override) is the fix if you know your real capacity.

## 📋 Entity States Reference

Expand Down Expand Up @@ -617,7 +639,9 @@ The integration includes built-in profiles for specific MG/SAIC models that corr
| `EC32` | MG Cyberster | 2-door BEV roadster; no rear doors/windows; unreliable live electric range field (falls back to estimated range) |
| `IS31P` | MG S9 PHEV (2025) | Climate status/fan speed mappings confirmed by physical testing |
| `AS33P` | MG HS PHEV (Super Hybrid 2025/2026) | Battery capacity 24.7 kWh; Target SOC and Charging Current Limit not supported by iSmart; electric range uses live SOC-tracking field; energy values corrected for ~3x API over-reporting |
| `S12L` | IM6 (IM by MG Motor) | Battery capacity 100 kWh — corrects the API's bogus `totalBatteryCapacity=725` (→ 72.5 kWh) for the Platinum/Performance pack (#53). ⚠️ Confirmed on the 100 kWh Platinum; if the 75 kWh LFP Premium reports the same series, this will need splitting — Premium owners, please open an issue with debug logs |
| `S12L` | IM6 (IM by MG Motor) | Battery capacity 96.5 kWh usable (100 kWh nominal NMC) — replaces the API's bogus `totalBatteryCapacity=725` (→ 72.5 kWh) (#53). In the UK/EU the IM6 is sold on the 100 kWh pack only, so this covers every variant; a 75 kWh LFP Premium exists in some other markets and would need 73.5 kWh and a split if it reports the same series |
| `P12L` | IM5 (IM by MG Motor) | Mode-select climate scheme mirroring the MGS6 (status code 2 = cool, confirmed, #326) — fixes the car showing as "Fan only" while genuinely cooling. Fan-only/heat/defrost/max-cool values are still unconfirmed best-effort, pending a debug log with the AC confirmed on. Battery capacity set to **96.5 kWh usable** for the confirmed Long Range/Performance pack (100 kWh nominal NCM, #326), replacing the API's bogus `totalBatteryCapacity=725` (→ 72.5 kWh). ⚠️ The IM5 **Standard Range** (75 kWh LFP, 73.5 kWh usable) reports the same series code and will read too high — set a [battery capacity override](#battery-capacity-override) to 73.5 and please comment on #326 so the variants can be split |
| `ZP22 EU` | MG3 Hybrid+ | Self-charging full hybrid (1.83 kWh HV battery, no charge port); reports as vehicle type HEV. State of Charge is now populated from `basicVehicleStatus.extendedData1`, since this vehicle type has no charging-endpoint data to read (#318) |

Models not listed above use safe default values and should work normally. If you notice incorrect sensor readings for your model, please open an issue with your vehicle's debug logs.

Expand Down
38 changes: 38 additions & 0 deletions RELEASE_NOTES_1.2.7-beta5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 1.2.7-beta5

Two fixes that affect every car, and one new sensor. All three came out of [discussion #262](https://github.com/townsmcp/mg-saic-ha/discussions/262) — thanks to @HarryFlatter for the detail.

## Fixed: Efficiency Since Charge (SOC) never showed a value

This sensor has been stuck on `Unknown` since it was introduced — on **all** models, not just PHEVs, and regardless of how much you'd driven since charging.

Two separate faults were stacked on top of each other. The sensor was handing the wrong object to its odometer lookup, and that lookup's backup route was searching a part of the API response that doesn't contain an odometer at all. With no odometer reading, the sensor had nothing to calculate a distance from, so it never produced a figure.

Both are fixed. The underlying charge-detection was working correctly all along, so the sensor starts reporting as soon as you've driven after a charge.

## Fixed: Power Usage Since Last Charge still ~3× too high on MG HS PHEV

The correction for this was added in 1.2.6, but it was applied in a code path this particular sensor never takes — so in practice nothing changed and the sensor kept showing the inflated figure. It's now applied wherever the value is read.

On an HS PHEV, a reading like 20.20 kWh should have been around 6.9 kWh.

Two related sensors — `Efficiency Since Last Charge` and the `Last Trip` energy figures — were already correcting this properly and were never affected. If you have an HS PHEV and your energy figures looked inconsistent with each other, this is why.

## New: Last Charge Energy sensor *(BEV/PHEV)*

Tells you how much energy the last completed charge put **into** the battery. Useful when you've charged somewhere that isn't home and want to know what you actually took — the car reports charging power while it's happening, and how much you've used *since* charging, but nothing about the charge itself.

The API has no field for this, so the integration measures it across the charging session. Two independent figures are calculated and both appear in the sensor's attributes:

- **From battery percentage** — the rise in charge level against your car's usable capacity. This is the headline value, as it works on any car that reports a battery percentage. If your `Total Battery Capacity` looks wrong, set a [battery capacity override](https://github.com/townsmcp/mg-saic-ha#battery-capacity-override) and this will follow it.
- **From the car's own energy figures** — shown alongside for comparison, and omitted when it doesn't look trustworthy.

Also in the attributes: start and end battery percentage, percentage added, how long the charge took, average power, and the session's timestamps. A `mg_saic_charge_completed` event fires at the end of each charge carrying the same data, so you can notify or log on it.

**Worth knowing:** this is energy measured at the battery, so it will read lower than your wall meter, Zappi or similar — those also pay for charger and cable losses. Expect a gap of roughly 5–10%.

Some deliberate limits: charges that add less than 0.5% are ignored (that's the small rebound the pack reports after a drive rather than a real charge), and a session left open more than 48 hours is dropped rather than reported as a nonsense number. Crucially, a charging-data dropout is never mistaken for the end of a charge — on some cars the charging endpoint goes quiet the instant a session finishes, which would otherwise log a phantom charge every time.

## Upgrading

No action needed. The Last Charge Energy sensor appears after a restart and populates once it has seen a complete charge from start to finish — so it will read `Unknown` until your next charge finishes.
12 changes: 10 additions & 2 deletions custom_components/mg_saic/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class Feature(str, Enum):
# Data retrieval
STATUS = "status" # get_vehicle_status
STATE_OF_CHARGE = "state_of_charge" # SOC from status or charging data
STATE_OF_CHARGE_NON_BEV = "state_of_charge_non_bev" # PHEV/HEV SOC: basicVehicleStatus.extendedData1 is battery SOC, not fuel
CHARGING_DATA = "charging_data" # get_charging_info (incl. SOC)
ALARM_MESSAGES = "alarm_messages" # get_alarm_messages / set_alarm_switches / message poller

Expand Down Expand Up @@ -87,14 +88,21 @@ class Feature(str, Enum):

# Features implemented AND confirmed on a real vehicle by the India TAP
# client (John Lazarus, mg-ismart-india-ha). MG India reports BEV state of
# charge in the ordinary vehicle-status payload, but separate charging data
# and charging controls are deliberately absent.
# charge in the ordinary vehicle-status payload, and charging telemetry in a
# separate frame; charging controls are deliberately absent.
# ALARM_MESSAGES is absent because the TAP protocol has no message-list
# endpoint — the account message poller must not run for India accounts.
# STATE_OF_CHARGE_NON_BEV is absent because India repurposes
# basicVehicleStatus.extendedData1 to carry fuel level: on a BEV that field is
# the SOC, but on a PHEV/HEV it is litres of petrol, so a non-BEV must not read
# SOC from it even though CHARGING_DATA is now supported.
INDIA_FEATURES: frozenset[Feature] = frozenset(
{
Feature.STATUS,
Feature.STATE_OF_CHARGE,
# Charging telemetry decoded from the 63-byte TAP frame: voltage,
# current, power and charge state (get_charging_info in india.py).
Feature.CHARGING_DATA,
Feature.LOCK,
Feature.TAILGATE,
Feature.WINDOWS,
Expand Down
Loading