The dashboard started with gauges and histories of equal weight, so a power failure and its effect were hard to find. The first rows now show the charge, the power source, the time remaining, and the errors of each device. Below them, one band shows each period on battery power, and one chart shows the charge of the three devices.
The charge history adds the pack current of the JBD and the River 2 on a right axis, as dashed lines in the color of each device. The charge limits apply to the charge lines only.
A time-remaining panel with no estimate now names the state. The JBD panel shows "On mains" while the pack does not discharge. The River 2 panel shows "Idle" while the device reports 5999 minutes for the two directions.
The River 2 errors panel names the module that reports a code and shows the code in hexadecimal. The solar controller reports 4096 with no solar input, and the panel counts that value as zero.
The server read each Bluetooth device every 60 seconds, and each read is one scan and one connection. An adapter that fails after many connections had no way to get fewer of them.
--bluetooth-interval sets the wait after a good Bluetooth read, and the default stays at 60 seconds. Clients receive the last read during the wait. A failed read still tries again after 60 seconds, or after the interval for a shorter one.
tools/generate_grafana_ups.py writes grafana-ups.json, which shows the JBD network UPS, the River 2 desktop UPS, and the River 3 NAS UPS in three columns. Git ignores the generated file.
Each EcoFlow column shows one time-remaining panel in hours, for the direction in use. The River 2 histories add pack current and remaining capacity. The River 3 reports neither, so its histories add battery power and charge.
The polling threads are daemon threads, so a stop ended the server in the middle of a Bluetooth read. The read got no disconnect, and the adapter kept an open connection or a scan in progress.
The server now sets the stop event at exit, closes its socket, and waits at most 20 seconds for the polling threads. A Bluetooth read that sees the stop event cancels its current step, disconnects, and returns. A read that waits for the adapter does not connect after a stop.
The server handled one client at a time, so a client that connected and sent nothing blocked every other client. The request handling is now in handle_client, and the main loop starts one thread for each connection.
The server closes a connection that stays silent for 5 seconds, so its thread ends.
Put each condition before its statement. Split long sentences. Use one term for each concept. Replace the description of possible checklist changes with a statement of the present behavior.
A Bluetooth collection listened for a fixed 10 seconds and returned the data that arrived. If a required report was late, the snapshot missed shared measurements without notice.
Each decoder now tracks the reports behind the shared measurements. A collection listens for 10 seconds, then until those reports arrive, for at most 30 seconds in total. The new ef_missing_measurements field names the reports that did not arrive. The server log shows one line each time that list changes.
The diagnostic tool now uses the River 2 heartbeat list from the decoder.
A failed JBD read retried forever inside a client request and blocked every other device and client. The server now starts one polling thread for each device, and a request returns the last snapshot. Only the first request after startup waits, until each device completes one read attempt.
One lock makes sure that only one Bluetooth collection runs at a time. A serial BMS retries a failed read after 1 second and keeps its last snapshot for 30 seconds. An error in a collection does not end the polling of that device.
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.
Use standard project metadata, dependency groups, and the uv build backend. The new lock preserves the previous dependency versions. Documentation and services use uv, and services leave their shared environment unchanged at startup.
Remove the entry point for the empty USB HID prototype. Exclude local prototypes, captures, and the external Type 7 lookup table from version control and distributions.
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.
server.py
- --device is now repeatable (-d ups1:/dev/ttyUSB0 -d ups2:/dev/ttyUSB1). Bare paths (/dev/ttyUSB0) auto-name from the last path component (ttyUSB0).
- Maintains {name: {ser, data, timestamp}} per UPS — each device has independent data freshness.
- GET response is now {ups_name: JBDUPS}. Accepts optional ups key in the request to return only one.
client.py
- read_data() gains ups=None parameter — pass a name to filter server-side, or omit for all.
- Always returns {ups_name: JBDUPS}.
influxdb.py
- influxdb_create_snapshot() iterates {name: JBDUPS} and tags every InfluxDB point with ups=name.
- influxdb_export() / bmspy-influxdb gain --ups to export only a specific UPS.
__init__.py
- bmspy CLI gains --ups to display only a named UPS.
- Displays each UPS under a === name === header.