Add River 2 Bluetooth telemetry and diagnostics

Decode River 2 charge, temperature, power flow, and time estimates. Preserve precise charge readings across packet reordering and omit unavailable time estimates.

Require valid PD, EMS, and BMS core heartbeats for diagnostic completion. Report optional packets separately and support raw watch output with reliable disconnect cleanup. Use synthetic regression tests and keep captures, credentials, and external keydata private.

Device details come from command-line arguments or discovery. This commit retains Poetry; the separate packaging commit migrates to uv.
This commit is contained in:
tim
2026-10-08 14:02:45 +02:00
parent 90388d2800
commit 886991caf3
16 changed files with 4093 additions and 11 deletions
+101
View File
@@ -0,0 +1,101 @@
# River 2 measurements
River 2 models with serial prefixes `R601` and `R603` use packet version 2.
Encryption is separate from the packet version. The tested River 2 uses Type 7
encryption, not Type 1. River 3 uses a separate decoder.
`bmspy/ecoflow_river2.py` contains the binary layouts, numeric decoder, and
measurement rules. `bmspy/ecoflow_bt.py` handles Bluetooth transport and
resets the decoder's source priorities before each connection.
`bmspy/ecoflow_snapshot.py` defines the shared EcoFlow snapshot.
## Normal snapshot
A snapshot contains the latest received measurements. Missing measurements stay
absent. These field names are also used by River 3, but each model has its own
rules for interpreting the device reports.
| Field | Meaning |
|---|---|
| `ef_input_power_watts` | Total device input power, W. |
| `ef_output_power_watts` | Total device output power, W. |
| `bms_temperature_celcius` | Battery temperature at sensor 1, °C. The spelling preserves the existing field name. |
| `bms_capacity_charge_ratio` | Battery charge as a ratio from 0 to 1. |
| `ef_remaining_time_minutes` | General remaining-time estimate, minutes. |
| `ef_charge_remaining_time_minutes` | Charging-time estimate from the energy management module, minutes. |
| `ef_discharge_remaining_time_minutes` | Discharging-time estimate from the energy management module, minutes. |
| `bms_charge_is_charging` | Whether battery-side power flows into the battery. |
| `bms_charge_is_discharging` | Whether battery-side power flows out of the battery. |
| `ef_charge_state` | Numeric device state code. Observed R601 values are 0 for idle, 1 for charging, and 2 for discharging. |
Input power alone does not indicate battery charging. The device can supply an
AC load without charging its battery. Do not apply River 2 state-code meanings
to River 3.
## Reported fields available to clients
Clients also receive every complete field from the five known heartbeat layouts.
A heartbeat is a periodic report from a device module.
The field names use these prefixes:
| Report | Prefix | Example |
|---|---|---|
| Device (PD) | `ef_river2_pd_` | `ef_river2_pd_usb1_watts` |
| Energy management (EMS) | `ef_river2_ems_` | `ef_river2_ems_max_charge_soc` |
| Battery management (BMS) | `ef_river2_bms_` | `ef_river2_bms_cycles` |
| Inverter (INV) | `ef_river2_inv_` | `ef_river2_inv_inv_out_vol` |
| Solar controller (MPPT) | `ef_river2_mppt_` | `ef_river2_mppt_in_watts` |
These fields appear in `EcoFlowSnapshot.items()`, socket responses, and client
snapshots. InfluxDB and Prometheus export them alongside the normal measurements.
Prometheus adds fields that first arrive after the exporter starts.
Numeric fields use `BMSScalarField.raw_value` and the units from the layout.
They retain reported state codes, zero values, and unavailable-time codes such as
`5999`. They do not replace the normal measurements or their interpretation rules.
For example, `ef_river2_pd_soc` uses percent, while `bms_capacity_charge_ratio`
uses a ratio from 0 to 1.
Byte fields use `BMSInfoField.info` with a `0x` hexadecimal string.
This includes version, reserved, and other byte fields omitted by the text decoder.
Bytes beyond the last complete field use the same format under
`<prefix>trailing_bytes`. Their meanings remain unknown.
Each field retains its latest received value for the snapshot.
Shorter firmware reports do not erase earlier trailing fields or create zero values
for missing fields. A snapshot can therefore combine values from different reports.
A rejected report leaves the earlier decoded fields unchanged and adds
`<prefix>decode_error`. The next accepted report removes that error.
The `ef_pkt_<src>_<cmdset>_<cmdid>` field always retains the latest raw payload.
## Source priority and units
Within each connection, charge percentage prefers the precise battery-management
reading, then the precise energy-management reading, then an integer reading.
Later integer packets cannot overwrite a precise reading. New readings from the
preferred source still update the snapshot.
R601 pack voltage and current use volts and signed amps. Cell voltages and current
limits use millivolts and milliamps. Unverified inverter configuration fields have
no unit labels in diagnostics.
The normal snapshot omits negative, all-ones, and `5999` time estimates. R601
samples report `5999` for inactive directions, but it can also mean an unknown
or capped estimate. Diagnostics retain these raw values. Other estimates remain
unchanged. This rule does not apply to River 3.
## Diagnostic collection
The diagnostic script waits for valid PD, EMS, and BMS reports. These are periodic
reports from the device, energy management, and battery management modules.
Empty or truncated reports do not count. Inverter (INV) and solar-controller
(MPPT) reports are optional and do not delay completion.
Raw packet keys include the source module: `ef_pkt_<src>_<cmdset>_<cmdid>`.
This keeps reports from different modules separate, even when they share a
command number. Watch mode also shows optional reports and unclassified packets.
Live R601 checks cover idle, charging, and discharging operation. Automated tests
use synthetic packets. These results do not validate River 3 operation.
See the [README](../README.md#ecoflow-bluetooth-le-devices) for setup and commands.