Add River 3 telemetry and schema-based diagnostics

Decode River 3 separately and accumulate partial reports against a field-level checklist. Keep watch mode running after completion.

Label fields and nested records from the pinned pr705 schema. Preserve unknown values and document unverified meanings and routing.

Add synthetic regressions, the River 3 field reference, schema-generation tooling, and upstream attribution.
This commit is contained in:
tim
2026-10-08 14:08:20 +02:00
parent bac953bf48
commit c2faeb669a
14 changed files with 3055 additions and 263 deletions
+387
View File
@@ -0,0 +1,387 @@
# River 3 diagnostic fields
This reference explains the fields in the reviewed R651 reports. It covers all
110 distinct top-level fields in that watch capture. It does not change the
53-field completion checklist or claim that these are all possible device fields.
The source for field numbers, names, and types is
[upstream `proto/pr705.proto`](https://github.com/rabits/ha-ef-ble/blob/511e04702c83ddcc90cdcd575a1bbeffe2f061ad/proto/pr705.proto),
specifically `DisplayPropertyUpload`. These messages arrive at source `0x02`,
command set `0xFE`, command `0x15`.
A schema describes the message structure. It does not explain every status code,
bit flag, physical connector, or unit conversion. The tables distinguish those
limits from known field names. Units shown for the current checklist follow the
current diagnostic decoder. The live tests do not independently establish every
unit or the accuracy of every measurement.
## Normal measurements and validation
Support covers serial prefixes `R651`, `R653`, `R654`, and `R655`. River 3 uses
packet version 3 and a separate decoder from River 2. Both models share the
[snapshot field names](ecoflow-river2-fields.md#normal-snapshot), not their decoding rules.
Temperature prefers the maximum cell temperature, with composite battery
temperature as a fallback. Signed battery power determines charging or discharging.
Total input power alone does not indicate charging. The general remaining-time
field selects the estimate for the active battery direction. It stays absent
while the direction is idle or unknown.
Time estimates use minutes. River 2's `5999` rule does not apply. A live R651
sample reported 4556 minutes while the display showed 75–76 hours. This supports
the time unit, not the accuracy of the prediction. Live checks cover no-load and
USB-C-load operation. Charging and further display comparisons still need validation.
Diagnostic collection combines each field's latest value across reports, not one
simultaneous measurement. Zero and explicit unavailable values count as received.
Malformed messages and required fields with an unexpected encoding do not count.
Missing fields do not erase earlier measurements. The normal snapshot omits
unusable values even when the diagnostic checklist counts their receipt.
## Reported fields available to clients
Clients receive all reported fields from `DisplayPropertyUpload`, beyond the
normal measurements and the completion checklist. The decoder knows 296 top-level
fields from the pinned schema. It exports only fields that the device sends.
Field names use `ef_river3_display_<protocol_name>`, such as
`ef_river3_display_pow_get_typec1` or `ef_river3_display_cms_batt_soh`.
They 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.
Numbers and boolean values use `BMSScalarField.raw_value`.
Enum values are named numeric codes. They retain the code in `raw_value` and the
name in `value`, when the schema defines a name.
Only fields with existing unit annotations carry units.
Do not assume that an untranslated code or an unannotated number is a physical measurement.
Strings use `BMSInfoField.info` without conversion.
Nested messages and repeated fields use JSON text in `info`.
Clients can use `json.loads(field.info)` for those groups.
This preserves named children, lists, enum codes and names, and explicit empty groups.
Missing children do not become zero values.
Statistics retain their reported numbers without an assumed energy unit.
Unknown fields use `ef_river3_display_field_<number>`.
Their `info` contains a JSON list with each occurrence's `wire_type` and `raw`
hexadecimal encoding. Integer encodings also include their unsigned `value`.
The decoder does not guess the meaning of unknown fields.
Separate reports update only the fields that they contain.
A reported nested group replaces its earlier group, while absent top-level fields
retain their earlier values. Each snapshot can therefore combine several reports.
Malformed fields, invalid strings, and non-finite numbers reject the extended report
without changing earlier extended fields. `ef_river3_display_decode_error` records
the reason until the next accepted report. The normal measurement decoder keeps
its separate validation rules.
Raw packet fields remain available as `ef_pkt_<src>_<cmdset>_<cmdid>`.
They retain the latest payload, including rejected and unknown reports.
Runtime routing at `0x02/0xFE/0x16` remains unconfirmed.
Those reports stay raw-only in client snapshots, although the diagnostic tool can
show a tentative schema interpretation. They do not supply normal measurements.
## How to read the reports
Protobuf is the binary message format used by River 3. Each field has a numeric
identifier. Fields can arrive in separate messages with the same source and
command numbers. Missing fields are not zero values.
The `bms_` prefix identifies battery-management data. The `cms_` prefix identifies
device-level battery data. Similar fields belong to different reporting layers.
They do not necessarily describe separate batteries or independent measurements.
SOC means state of charge, the reported charge percentage. SOH means state of
health, a reported battery health estimate. Neither is a direct capacity test.
AC means alternating current. DC means direct current. PV identifies the solar/DC
input in these messages. MPPT identifies the solar-input charging controller.
A boolean field is a yes/no flag. A raw state code is a number whose individual
meanings remain unverified. In particular, River 2 and River 3 do not share a
verified charge-state code mapping.
The `measurements` and `configuration` names below are local checklist groups.
They are not message types or exact boundaries between the device's reports.
Several configuration fields arrive in the first report.
## Current measurement and status checklist: 31 fields
Every row below is required by the current diagnostic checklist. Receipt does
not guarantee a usable exported measurement. Zero and explicit unavailable values
still establish that a field arrived.
| Field | Protocol name | Information represented |
|---|---|---|
| 1 | `errcode` | Overall device error code. Individual code meanings remain unverified. |
| 3 | `pow_in_sum_w` | Total device input power, W. This is not necessarily battery charging power. |
| 4 | `pow_out_sum_w` | Total device output power, W. |
| 9 | `pow_get_qcusb1` | Power at USB-A/QC channel 1, W. |
| 10 | `pow_get_qcusb2` | Power at USB-A/QC channel 2, W. |
| 11 | `pow_get_typec1` | Signed power at USB-C channel 1, W. A discharge sample reported a negative value. |
| 12 | `pow_get_typec2` | Power at USB-C channel 2, W. A reported slot does not prove that a physical port exists. |
| 37 | `pow_get_12v` | Power on the 12 V DC channel, W. |
| 54 | `pow_get_ac_in` | AC input power, W. |
| 148 | `pv_chg_type` | Solar/DC charging mode code. The value mapping remains unverified. |
| 158 | `pow_get_bms` | Signed battery power, W. Positive means charging and negative means discharging in the current mapping. |
| 212 | `dev_sleep_state` | Device sleep-state code. The value mapping remains unverified. |
| 242 | `bms_batt_soc` | Battery-management charge estimate, %. |
| 243 | `bms_batt_soh` | Battery-management health estimate, %. |
| 248 | `bms_design_cap` | Battery design capacity. The current diagnostic label is mAh. |
| 254 | `bms_dsg_rem_time` | Battery-management estimate of remaining discharge time, minutes. |
| 255 | `bms_chg_rem_time` | Battery-management estimate of remaining charge time, minutes. |
| 258 | `bms_min_cell_temp` | Minimum battery-cell temperature, degrees Celsius. |
| 259 | `bms_max_cell_temp` | Maximum battery-cell temperature, degrees Celsius. This supplies the normal snapshot temperature when present. |
| 260 | `bms_min_mos_temp` | Minimum temperature at battery switching transistors, degrees Celsius. This is not a cell temperature. |
| 261 | `bms_max_mos_temp` | Maximum temperature at battery switching transistors, degrees Celsius. |
| 262 | `cms_batt_soc` | Device-level charge estimate, %. This supplies the normal snapshot charge ratio. |
| 263 | `cms_batt_soh` | Device-level battery health estimate, %. |
| 268 | `cms_dsg_rem_time` | Device-level estimate of remaining discharge time, minutes. |
| 269 | `cms_chg_rem_time` | Device-level estimate of remaining charge time, minutes. |
| 275 | `cms_bms_run_state` | Battery-system operating-state code. The value mapping remains unverified. |
| 281 | `bms_chg_dsg_state` | Battery-management charge/discharge state code. It is not decoded using River 2 meanings. |
| 282 | `cms_chg_dsg_state` | Device-level charge/discharge state code. Exported separately from power-based direction. |
| 355 | `ups_alram` | Alarm flag associated with uninterruptible power supply operation. The protocol spelling is retained. Exact trigger conditions remain unverified. |
| 361 | `pow_get_pv` | Solar/DC input power, W. |
| 368 | `pow_get_ac_out` | AC output power, W. |
Individual channel power can be signed even when aggregate output power is
positive. Do not apply one sign convention to every power field. The current
snapshot uses battery power, not an untranslated state code, to select direction.
The two time estimates can both be present while only one direction is active.
Their presence does not mean simultaneous charging and discharging. A value of
`4294967295` is treated as unavailable for the normal River 3 time measurements.
## Current configuration checklist: 22 fields
These values describe settings, limits, and connection state. A configured power
limit is not a measurement of current power. A charger flag alone does not prove
that the battery is receiving power.
| Field | Protocol name | Information represented |
|---|---|---|
| 7 | `energy_backup_en` | Whether the energy-backup feature is enabled. |
| 8 | `energy_backup_start_soc` | Charge threshold for the energy-backup feature, %. Exact transition behavior remains unverified. |
| 17 | `dev_standby_time` | Device standby timeout setting, seconds in the current decoder. |
| 18 | `screen_off_time` | Display-off timeout setting, seconds in the current decoder. |
| 19 | `ac_standby_time` | AC standby timeout setting, seconds in the current decoder. |
| 25 | `xboost_en` | Whether X-Boost is enabled. This is a setting, not proof that it is active under a load. |
| 33 | `flow_info_12v` | Encoded flow/status information for the 12 V channel. The bits remain unverified. |
| 74 | `dc_out_open` | DC output enabled flag. |
| 195 | `en_beep` | Audible signal enabled flag. |
| 202 | `plug_in_info_ac_charger_flag` | AC charger flag. Its exact relationship to cable presence and active charging remains unverified. |
| 209 | `plug_in_info_ac_in_chg_pow_max` | Reported AC input charging-power limit, W. |
| 211 | `ac_out_freq` | AC output frequency setting, Hz. This is separate from runtime frequency measurements. |
| 270 | `cms_max_chg_soc` | Maximum charge setting, %. |
| 271 | `cms_min_dsg_soc` | Minimum charge setting for discharge, %. |
| 356 | `plug_in_info_pv_dc_amp_max` | Solar/DC input current-limit setting, A in the current decoder. |
| 358 | `low_power_alarm` | Low-battery alarm setting, % in the current decoder. The special meaning of zero remains unverified. |
| 360 | `flow_info_pv` | Encoded flow/status information for the solar/DC input. The bits remain unverified. |
| 364 | `plug_in_info_pv_charger_flag` | Solar/DC charger flag. It is not a power measurement. |
| 365 | `plug_in_info_pv_chg_amp_max` | Reported maximum solar/DC charging current, A in the current decoder. |
| 366 | `plug_in_info_pv_chg_vol_max` | Reported maximum solar/DC charging voltage, V in the current decoder. |
| 367 | `flow_info_ac_out` | Encoded AC output flow/status information. The bits remain unverified. |
| 458 | `plug_in_info_ac_in_chg_hal_pow_max` | Hardware-layer AC charging-power limit, W. Its exact relationship to field 209 needs device testing. |
## Information supplied by the third report: 20 fields
The reviewed capture contained three distinct report payloads, each repeated
68 times. The third payload contained the following 20 additional fields.
None belongs to the current 53-field checklist.
DCP and DCP2 are connector/interface slot names in the schema. Their physical
mapping on R651 remains unverified. Zero or empty values for a slot do not prove
that the device has another physical connector.
PCS identifies power-conversion equipment in the fan field. PD identifies the
main device controller here, not a USB charging port. LLC identifies a
power-converter module in the schema.
| Field | Protocol name | Information represented | Value in the reviewed capture |
|---|---|---|---|
| 30 | `pcs_fan_level` | Power-conversion fan level. Not a verified speed in revolutions per minute or a percentage. | 0 |
| 83 | `plug_in_info_dcp2_in_flag` | DCP2 input/connection flag. Exact physical meaning remains unverified. | false |
| 84 | `plug_in_info_dcp2_dsg_chg_type` | DCP2 charge/discharge mode code. | 0 |
| 85 | `plug_in_info_dcp2_charger_flag` | DCP2 charger flag. | false |
| 86 | `plug_in_info_dcp2_type` | DCP2 connected-device/interface type code. | 0 |
| 87 | `plug_in_info_dcp2_detail` | DCP2 type/detail code. | 0 |
| 88 | `plug_in_info_dcp2_sn` | DCP2 associated serial-number string. Treat populated values as private. | Empty string |
| 89 | `plug_in_info_dcp2_run_state` | DCP2 operating-state code. | 0 |
| 90 | `plug_in_info_dcp2_firm_ver` | DCP2 firmware-version number. Version packing remains unverified. | 0 |
| 91 | `plug_in_info_dcp2_resv` | DCP2 reserved information. | Empty message |
| 140 | `bms_err_code` | Battery-management error code. Specific nonzero codes remain unverified. | 0 |
| 141 | `err_code_record_list` | List of error records, each with optional error code and timestamp. Timestamp units and epoch remain unverified. | Ten empty records |
| 213 | `pd_err_code` | Main device-controller error code. | 0 |
| 215 | `mppt_err_code` | Solar/DC charging-controller error code. | 0 |
| 232 | `llc_inv_err_code` | Power-converter/inverter error code. | 0 |
| 432 | `plug_in_info_dcp_resv` | DCP reserved information. | Empty message |
| 434 | `plug_in_info_dcp_firm_ver` | DCP firmware-version number. Version packing remains unverified. | 0 |
| 436 | `plug_in_info_dcp_run_state` | DCP operating-state code. | 0 |
| 438 | `plug_in_info_dcp_err_code` | DCP error code. | 0 |
| 439 | `plug_in_info_dcp2_err_code` | DCP2 error code. | 0 |
The decoder now names all 20 fields using the pinned schema. Earlier versions
named only fields 30, 140, 213, 215, and 232. The 53-field completion checklist
still does not require this report.
Field 141 contains a nested list, not one error number. Its outer field 1 is a
repeated `ErrcodeRecordItem`. Each item can contain error code in field 1 and
timestamp in field 2. The ten empty entries in this capture contain neither
value. They do not represent ten detected faults.
Fields 91 and 432 use `ResvInfo`, a message containing repeated unsigned integers
in field 1. Both messages are empty in this capture. Reserved information has no
verified operational meaning.
## Other fields already present in the first two reports: 37 fields
These fields appeared in the earlier short run as well as the watch capture.
They are outside the current checklist. Earlier versions printed numeric labels
for these fields. The decoder now uses the schema names below.
Together, these 37 fields, the 53 required fields, and the third report's 20
fields account for all 110 observed top-level fields. This is a capture
inventory, not a universal River 3 field list.
| Field | Protocol name | Information represented |
|---|---|---|
| 13 | `flow_info_qcusb1` | USB-A/QC channel 1 flow/status code. Bit meanings remain unverified. |
| 14 | `flow_info_qcusb2` | USB-A/QC channel 2 flow/status code. |
| 15 | `flow_info_typec1` | USB-C channel 1 flow/status code. |
| 16 | `flow_info_typec2` | USB-C channel 2 flow/status code. |
| 22 | `ac_always_on_flag` | AC always-on feature flag. |
| 23 | `ac_always_on_mini_soc` | Minimum charge threshold associated with AC always-on operation. |
| 45 | `flow_info_ac2dc` | AC-to-DC conversion flow/status code. |
| 46 | `flow_info_dc2ac` | DC-to-AC conversion flow/status code. |
| 47 | `flow_info_ac_in` | AC input flow/status code. |
| 53 | `pow_get_ac` | AC-side power field. Its exact relationship to separate input/output totals needs testing. |
| 61 | `plug_in_info_ac_in_flag` | AC input connection/status code. |
| 62 | `plug_in_info_ac_in_feq` | AC input frequency field. The protocol spelling is retained. |
| 77 | `pow_get_dcp2` | DCP2 power field. Physical slot mapping and sign convention remain unverified. |
| 78 | `flow_info_dcp2_in` | DCP2 input flow/status code. |
| 79 | `flow_info_dcp2_out` | DCP2 output flow/status code. |
| 147 | `output_power_off_memory` | Setting for retaining output-state information across power-off. Exact restore behavior needs testing. |
| 152 | `flow_info_bms_dsg` | Battery discharge flow/status code. It is not a time estimate or charge percentage. |
| 153 | `flow_info_bms_chg` | Battery charge flow/status code. It is not a time estimate or charge percentage. |
| 219 | `time_task_current` | Current scheduled-task message. Empty in the reviewed capture. |
| 227 | `llc_hv_lv_flag` | Converter high/low-voltage mode indicator. Despite the observed value 230, do not treat it as a measured voltage. |
| 238 | `plug_in_info_ac_out_dsg_pow_max` | Maximum AC output power field, not current output power. |
| 285 | `time_task_conflict_flag` | Scheduled-task conflict status. |
| 286 | `time_task_change_cnt` | Scheduled-task change counter. |
| 357 | `led_mode` | Light/display mode code. Exact modes remain unverified. |
| 359 | `silence_chg_watt` | Quiet-mode charging-power setting. It is not an AC frequency field. |
| 362 | `plug_in_info_pv_flag` | Solar/DC input flag. |
| 363 | `plug_in_info_pv_type` | Solar/DC input type code. |
| 423 | `flow_info_dcp_in` | DCP input flow/status code. |
| 424 | `flow_info_dcp_out` | DCP output flow/status code. |
| 425 | `pow_get_dcp` | DCP power field. Physical slot mapping and sign convention remain unverified. |
| 426 | `plug_in_info_dcp_in_flag` | DCP input/connection flag. |
| 427 | `plug_in_info_dcp_type` | DCP connected-device/interface type code. |
| 428 | `plug_in_info_dcp_detail` | DCP type/detail code. |
| 431 | `plug_in_info_dcp_dsg_chg_type` | DCP charge/discharge mode code. |
| 433 | `plug_in_info_dcp_sn` | DCP associated serial-number string. Empty here, but populated values are private. |
| 435 | `plug_in_info_dcp_charger_flag` | DCP charger flag. |
| 463 | `display_statistics_sum` | List of accumulated statistics. This is not a list of instantaneous power readings. |
Field 463 contains repeated `StatisticsRecordItem` messages in outer field 1.
Each item uses field 1 for the statistic category and field 2 for its numeric
content. Categories 2 through 7 identify AC output, 12 V output, USB-C output,
USB-A output, AC input, and solar input energy, respectively.
Those six categories occur in the reviewed capture, along with empty entries.
The schema does not establish the scaling of the recorded energy values.
Do not label the raw numbers as Wh without further evidence. An absent content
field is also distinct from an explicitly received zero.
The earlier generic decoder displayed both empty strings and empty nested messages
as `''`. The schema-based decoder now distinguishes them. Fields 88 and 433 are
strings and retain `''`. Empty messages in fields 91, 219, and 432 print as `{}`.
## Why normal collection can omit the third report
The three observed payloads have the same route, `0x02/0xFE/0x15`. They are not
three separately requested message types. Their sizes and checklist coverage in
this capture were:
| Observed report | Payload size | Top-level fields | Current required fields supplied |
|---|---|---|---|
| First | 300 bytes | 56 | 43: all 31 measurement/status fields and 12 configuration fields |
| Second | 102 bytes | 34 | 10: the remaining configuration fields |
| Third | 80 bytes | 20 | None |
The second report completes the current checklist. The collector can therefore
exit before the third report arrives. If the third report arrives before the
next completion check, its fields still appear in the accumulated final view.
There is no separate, shorter wait period assigned to the second report.
Collection checks progress every half second. It stops when the required
information is present, rather than when a selected report count or idle period
is reached. The watch output does not timestamp each packet, so it cannot
establish the exact interval between these reports.
Adding fields 30, 140, 213, 215, and 232 to a required status group would increase
the checklist from 53 to 58 fields. With the observed report layout, collection
would then wait for the third report. It would finish on receipt rather than
wait until the full timeout. The third report can still take additional time to
arrive. No fixed arrival time is established by this capture.
Requiring those five fields would not guarantee receipt of the other 15 fields
on firmware that splits the reports differently. Requiring all 20 would need a
broader, firmware-aware policy, especially for reserved and connector fields.
Neither change is implemented by this reference. The existing 53-field checklist
and verbose missing-field output remain unchanged.
## Information that remains unresolved
The watch capture also contains eight-byte messages at `0x35/0x35/0x20`. There
are 17 such messages with seven distinct payloads. Their structure and meaning
remain unverified. They must not be assigned field meanings from the protobuf
schema merely because some bytes resemble protobuf tags.
This capture contains no confirmed `0x02/0xFE/0x16` runtime report. It therefore
does not establish runtime voltage/current fields from that route. The schema
also contains many fields that this device did not report during the capture.
Naming a field is separate from interpreting its values or exporting it as a
normal measurement. Verified names and nested structures are now decoded. State
codes, physical slot mappings, units, and error causes still need independent
evidence where this reference marks them as unverified.
## Schema coverage and maintenance
`bmspy/ecoflow_river3.py` contains the curated units, numeric decoder, and
measurement rules. `bmspy/ecoflow_proto.py` provides shared parsing and
diagnostic formatting. Its `format_*` functions return readable text, not
measurement values. The older diagnostic `decode_*` names remain aliases.
Bluetooth transport stays in `bmspy/ecoflow_bt.py`. The shared snapshot stays
in `bmspy/ecoflow_snapshot.py`.
`bmspy/ecoflow_river3_schema.py` contains generated descriptors for all 296
`DisplayPropertyUpload` fields and all 98 `RuntimePropertyUpload` fields in the
pinned upstream schema. It also includes the 15 reachable message definitions
and ten enum definitions. No network access or protobuf package is needed at runtime.
The decoder uses declared types for strings, nested messages, booleans, signed
integers, floats, enums, and repeated values. It accepts packed and unpacked
repeated integers. Known fields with an unexpected encoding retain their names
and raw bytes, with an error rather than a misleading interpreted value.
Unknown future field numbers remain numeric/raw. The eight-byte `0x35/0x35/0x20`
message is still unclassified. Full schema coverage does not establish the
meaning of that separate message or confirm runtime message routing.
The descriptor source is upstream commit
`511e04702c83ddcc90cdcd575a1bbeffe2f061ad`. Only protocol declarations are retained,
not upstream decoder code or authentication keydata.
To reproduce the descriptors, run this command from the repository root:
```bash
python tools/generate_ecoflow_river3_schema.py --ref 511e04702c83ddcc90cdcd575a1bbeffe2f061ad
```
The generator retrieves public source files through the GitHub API. Without
`--ref`, it resolves the current upstream main commit. Review the generated diff
before accepting a schema update. Unsupported reachable types cause generation
to fail rather than silently omit fields.