62 Commits
Author SHA1 Message Date
tim 99f3d41970 Show the present state of each device first in the Grafana dashboard
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.
2026-10-13 12:48:03 +02:00
tim 99bb321e3b Add a server option for the Bluetooth read interval
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.
2026-10-10 18:03:09 +02:00
tim e32d3a8e22 Add a Grafana dashboard generator for the three UPS devices
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.
2026-10-10 11:56:29 +02:00
tim 0c179d29e5 Disconnect from Bluetooth devices before the server stops
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.
2026-10-10 11:47:16 +02:00
tim dc1fb95ada Log each failed Bluetooth read with its reason
collect_data printed a failure only with -v, and gave no reason for a failed connection, a failed authentication, or a read with no telemetry. A device that was out of reach showed only as missing data.

Each failed read now gives one log line with the reason, at every debug level. The device object keeps the reason for a failed connection in last_error.
2026-10-10 11:35:43 +02:00
tim 97880b717c Only start the server after any bluetooth devices are available 2026-10-10 11:24:20 +02:00
tim 6b636cde81 Report a device that sends no data for 5 minutes
If the server had no data for the device, bmspy-ups slept and read again with no report. A battery that stopped answering during a power failure gave no email and no shutdown.

bmspy-ups now sends one email after 5 minutes with no data, and one more when the data returns. A new email for a loss needs another 5 minutes with no data after that return. The same rule applies to the wait at startup.
2026-10-10 11:05:16 +02:00
tim ec4d824153 Name the device in the bmspy-ups service file
bmspy-ups needs --device if the server reads more than one device. The service file now starts it with --device network.
2026-10-10 10:59:35 +02:00
tim ca641fdda8 Send service output to the journal at once
Set PYTHONUNBUFFERED=1 in the three service files. Python buffers its output if it does not write to a terminal, so log lines reached the journal late.
2026-10-10 10:59:35 +02:00
tim 98fd08e39b Report a power return at any charge level
The charge tests and the power tests were one if/elif chain. If mains power returned while the charge was below the warning threshold, the loop did not report it, did not cancel the shutdown, and did not clear the flags for the next power failure.

The power tests now run first, on their own. The charge tests apply only while the battery discharges for two consecutive readings, so a low battery on mains power starts no shutdown. A device that reports no battery current is judged by its charge alone.
2026-10-10 10:59:35 +02:00
tim 3bc86b85a2 Compare the newest readings in bmspy-ups
The loop compared the current reading with history[1], history[2], and history[3]. The history holds the oldest reading first, so these were three of the oldest readings. A plain power failure sent no email, and the shutdown started 4 minutes late. The loop now uses the three readings before the current one.

Four tests used reading sequences that matched the old comparison. They now use plain sequences.
2026-10-10 10:59:35 +02:00
tim 9ab1d909ed Raise a clear client error for a refused request
socket_comms now raises DaemonError with the server message if the reply has the status ERROR. Before, the reply reached code that expects device data, and that code raised KeyError.
2026-10-10 10:59:35 +02:00
tim 0d5a080425 Read each request in full
read_request called recv once for the length and once for the body. A stream socket can deliver a request in several pieces, and the server then rejected that request.

The server now uses the read loop of the client, which moves to utilities as recv_exact. The server refuses a request above 64 KiB.
2026-10-10 10:59:34 +02:00
tim f17d1d82c2 Answer each client in its own thread
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.
2026-10-10 10:59:34 +02:00
tim 6ee9bbaca8 Keep the server running if a reply fails
If a client disconnected before its reply, send_response raised OSError and the server ended. The request loop now logs the error and waits for the next client. An error from accept() still ends the server.
2026-10-10 10:59:34 +02:00
tim 0abc8536f0 Answer an invalid request with an error reply
An unknown command ended the server loop, so one bad request stopped the service for every client. The server now sends {"status": "ERROR", "message": ...} and continues. A request with no command, or one that is not a JSON object, gets the same reply.

The server tests stopped the server with an unknown command. They now stop it through accept().
2026-10-10 10:59:34 +02:00
tim bf2d96140b Test a failed JBD read
collect_data returns False if requestMessage returns False or None, for the first request and for the second one. Before the earlier fix, these values raised TypeError and ended the server.
2026-10-10 10:59:34 +02:00
tim ef7a6b5f92 Simplify wording in the documentation
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.
2026-10-10 10:12:35 +02:00
tim 82def5737f Select the bmspy-ups device from the configured devices
bmspy-ups selected its device from the devices that had data at startup, and exited with status 0 if the selection failed. One failed first read ended the monitor or made it select the wrong device.

The server has a new DEVICES command that lists the configured names, and client.list_devices calls it. bmspy-ups selects from that list and then waits for the first data of its device. A wrong or missing --device now exits with status 2. The service file does not restart on that status, because a restart cannot correct it.

An older server ends on the unknown DEVICES command, so update the server first.
2026-10-10 09:03:35 +02:00
tim 161ae7ee65 Wait for the required EcoFlow reports in each collection
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.
2026-10-10 09:03:35 +02:00
tim b569bcf27b Fix email, shutdown, and option bugs in bmspy-ups
The recipient check used a regular expression that never matched, so a full address got the host name appended. The main loop passed the device name with a parameter name that read_data does not have. A canceled shutdown did not clear its flag, so a second incident did not schedule a shutdown. The recorded shutdown time used the wrong unit. The --port option stayed text and never matched the STARTTLS ports.
2026-10-10 09:03:34 +02:00
tim 953f8a6482 Read each device in a background thread
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.
2026-10-10 09:03:34 +02:00
tim d86d59c0f7 Stop the server thread at the end of each server test
The server tests left main() in a thread after each test. These threads kept their patches active and delayed the suite. Each test now sends an invalid command, which ends the server loop, and then joins the thread. The suite time drops from about 8.5 seconds to about 2 seconds.

Remove one unused line from a test.
2026-10-10 09:03:34 +02:00
tim c97f6cc176 Handle a JBD read that returns no response
collect_data called len() on each response. A port that cannot open returns False or None, so the call raised TypeError and ended the server. Treat each empty or absent response as a failed read.
2026-10-10 09:03:33 +02:00
tim aa9888800e Report EcoFlow power and temperature as floats
River 2 reported total power and temperature as integers. River 3 reported temperature as an integer. InfluxDB keeps one field type for each measurement, so it rejected these points when another device wrote floats to the same measurement.
2026-10-10 09:03:33 +02:00
tim f36bb2510c Rename bms_temperature_celcius to bms_temperature_celsius
Correct the spelling in the JBD and EcoFlow snapshots, the diagnostic tool, and the tests. UPS.from_dict maps the old name to the new name, so a client still reads data from an older server during an upgrade.

InfluxDB and Prometheus now receive this measurement under the new name.
2026-10-10 09:03:33 +02:00
tim 452a3bc207 Correct the ups_alarm field spelling
The upstream River 3 schema spells field 355 `ups_alram`. Use `ups_alarm` in the decoder, the generated schema, and the field reference. The schema generator now applies a rename map, so a regenerated schema keeps the corrected name.
2026-10-10 09:03:32 +02:00
tim 5fa6000c70 Separate JBD decoding from serial transport
Move response decoding into jbd_proto and the data container into jbd_snapshot. Preserve the existing jbd_bms imports and test the transport boundary.
2026-10-08 14:09:05 +02:00
tim 3422742204 Fix server SIGTERM handler signature 2026-10-08 14:08:20 +02:00
tim c2faeb669a 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.
2026-10-08 14:08:20 +02:00
tim bac953bf48 Replace Poetry with uv
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.
2026-10-08 14:03:09 +02:00
tim 79f9ebf0c7 Integrate EcoFlow Bluetooth devices into the server
Serial paths and collector interfaces remain compatible. Bluetooth polling uses a 60-second cache and backoff. Offline Bluetooth devices no longer retry forever inside a client request. Invalid device specifications do not echo account IDs.
2026-10-08 14:02:45 +02:00
tim 886991caf3 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.
2026-10-08 14:02:45 +02:00
tim 90388d2800 Stop Prometheus mode from falling through to the client 2026-10-07 17:44:48 +02:00
tim 75564f96f0 Fix UPS functionality for multiple devices 2026-05-02 23:25:15 +02:00
tim a74934c439 Fix prometheus functionality 2026-05-02 23:12:40 +02:00
tim 897ae5dfb7 Add unit tests 2026-05-02 23:12:29 +02:00
tim 4e2a967dcf Add docstrings 2026-05-02 23:12:29 +02:00
tim 86f2c548b3 Formatting cleanups for UPS and Prometheus functionality 2026-05-02 18:19:03 +02:00
tim deb6b2bdcc Add a new DeviceState class to capture metadata from a reading 2026-05-02 18:13:33 +02:00
tim d08e26b0db Rename JBDUPS to JBDBMS, as this is more accurate 2026-05-02 18:08:55 +02:00
tim ff578f1889 Add typing, as well as a new UPS type that should be inherited by all new models 2026-05-02 18:08:55 +02:00
tim 973dc9bc96 Fix formatting 2026-05-02 17:40:31 +02:00
tim ad58fc8bc2 Add debugger 2026-05-02 17:40:31 +02:00
tim 4f54b9be36 Extract out classes that should be common to all UPS models 2026-05-02 17:40:31 +02:00
tim f2ffc4568a Add multi-device support
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.
2026-05-02 17:40:31 +02:00
tim 827f59cf49 Update from influxdb_client to influxdb_client_3 2026-05-02 17:40:31 +02:00
tim a782e19563 Update Python minumum version to allow new dependencies
Add a manual pin to typing-extensions >= 4.13, as lower versions that might already be installed will break on Python >= 3.14
2026-05-02 17:40:31 +02:00
tim 4b13450b65 Refactor data dict into a proper JBDUPS class. 2026-05-02 17:40:31 +02:00
tim b672c9f5ae Add UPS functionality 2024-05-14 09:22:06 +02:00