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.
This commit is contained in:
tim
2026-10-10 09:03:33 +02:00
parent 452a3bc207
commit f36bb2510c
16 changed files with 39 additions and 28 deletions
+7 -1
View File
@@ -44,6 +44,10 @@ class BMSInfoField:
type BMSField = BMSScalarField | BMSMultiField | BMSInfoField type BMSField = BMSScalarField | BMSMultiField | BMSInfoField
# Corrected field spellings: old name to current name. An older server can
# still send the old name during an upgrade.
RENAMED_FIELDS = {"bms_temperature_celcius": "bms_temperature_celsius"}
def _field_from_dict(d: dict[str, Any]) -> BMSField: def _field_from_dict(d: dict[str, Any]) -> BMSField:
"""Reconstruct a BMSField from its JSON-serialized dict.""" """Reconstruct a BMSField from its JSON-serialized dict."""
@@ -69,7 +73,9 @@ class UPS(ABC):
@classmethod @classmethod
def from_dict(cls, data: dict[str, Any]) -> "UPS": def from_dict(cls, data: dict[str, Any]) -> "UPS":
"""Reconstruct a UPS snapshot from a JSON-decoded field dict.""" """Reconstruct a UPS snapshot from a JSON-decoded field dict."""
return _UPSSnapshot({name: _field_from_dict(v) for name, v in data.items()}) return _UPSSnapshot({
RENAMED_FIELDS.get(name, name): _field_from_dict(v) for name, v in data.items()
})
class _UPSSnapshot(UPS): class _UPSSnapshot(UPS):
+1 -1
View File
@@ -353,7 +353,7 @@ class River2Decoder:
else: else:
charge_ratio(values["soc"], priority=1) charge_ratio(values["soc"], priority=1)
temp = values["temp"] temp = values["temp"]
snapshot.bms_temperature_celcius = BMSMultiField( snapshot.bms_temperature_celsius = BMSMultiField(
help="Battery Temperature", label="sensor", help="Battery Temperature", label="sensor",
raw_values={1: temp}, values={1: str(temp)}, units="°C", raw_values={1: temp}, values={1: str(temp)}, units="°C",
) )
+1 -1
View File
@@ -305,7 +305,7 @@ class River3Decoder:
for source, priority in (("cms_batt_temp", 1), ("bms_max_cell_temp", 2)): for source, priority in (("cms_batt_temp", 1), ("bms_max_cell_temp", 2)):
temp = values.get(source) temp = values.get(source)
if temp is not None and priority >= self._river3_temp_priority: if temp is not None and priority >= self._river3_temp_priority:
snapshot.bms_temperature_celcius = BMSMultiField( snapshot.bms_temperature_celsius = BMSMultiField(
help="Battery Temperature", label="sensor", help="Battery Temperature", label="sensor",
raw_values={1: temp}, values={1: str(temp)}, units="°C", raw_values={1: temp}, values={1: str(temp)}, units="°C",
) )
+1 -1
View File
@@ -23,7 +23,7 @@ class EcoFlowSnapshot(UPS):
ef_packets_received: BMSScalarField | None = None ef_packets_received: BMSScalarField | None = None
bms_capacity_charge_ratio: BMSScalarField | None = None bms_capacity_charge_ratio: BMSScalarField | None = None
bms_temperature_celcius: BMSMultiField | None = None bms_temperature_celsius: BMSMultiField | None = None
bms_charge_is_charging: BMSScalarField | None = None bms_charge_is_charging: BMSScalarField | None = None
bms_charge_is_discharging: BMSScalarField | None = None bms_charge_is_discharging: BMSScalarField | None = None
ef_input_power_watts: BMSScalarField | None = None ef_input_power_watts: BMSScalarField | None = None
+1 -1
View File
@@ -295,7 +295,7 @@ def parse_03_response(response: bytearray, debug: int = 0) -> JBDBMS | bool:
result.bms_temperature_sensor_num = BMSScalarField( result.bms_temperature_sensor_num = BMSScalarField(
help="Temperature Sensors", raw_value=ntc_num, value="{0}".format(ntc_num) help="Temperature Sensors", raw_value=ntc_num, value="{0}".format(ntc_num)
) )
result.bms_temperature_celcius = BMSMultiField( result.bms_temperature_celsius = BMSMultiField(
help="Temperature", help="Temperature",
label="sensor", label="sensor",
raw_values={i + 1: temp for i, temp in enumerate(temperatures)}, raw_values={i + 1: temp for i, temp in enumerate(temperatures)},
+1 -1
View File
@@ -33,7 +33,7 @@ class JBDBMS(UPS):
bms_cell_number: BMSScalarField | None = None bms_cell_number: BMSScalarField | None = None
bms_cells_balancing: BMSMultiField | None = None bms_cells_balancing: BMSMultiField | None = None
bms_temperature_sensor_num: BMSScalarField | None = None bms_temperature_sensor_num: BMSScalarField | None = None
bms_temperature_celcius: BMSMultiField | None = None bms_temperature_celsius: BMSMultiField | None = None
bms_voltage_cells_volts: BMSMultiField | None = None bms_voltage_cells_volts: BMSMultiField | None = None
def items(self) -> Iterator[tuple[str, BMSField]]: def items(self) -> Iterator[tuple[str, BMSField]]:
+1 -1
View File
@@ -19,7 +19,7 @@ rules for interpreting the device reports.
|---|---| |---|---|
| `ef_input_power_watts` | Total device input power, W. | | `ef_input_power_watts` | Total device input power, W. |
| `ef_output_power_watts` | Total device output 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_temperature_celsius` | Battery temperature at sensor 1, °C. Versions before this rename wrote `bms_temperature_celcius`. |
| `bms_capacity_charge_ratio` | Battery charge as a ratio from 0 to 1. | | `bms_capacity_charge_ratio` | Battery charge as a ratio from 0 to 1. |
| `ef_remaining_time_minutes` | General remaining-time estimate, minutes. | | `ef_remaining_time_minutes` | General remaining-time estimate, minutes. |
| `ef_charge_remaining_time_minutes` | Charging-time estimate from the energy management module, minutes. | | `ef_charge_remaining_time_minutes` | Charging-time estimate from the energy management module, minutes. |
+1 -1
View File
@@ -68,7 +68,7 @@ def populated_jbdbms() -> JBDBMS:
bms.bms_manufacture_date = BMSInfoField( bms.bms_manufacture_date = BMSInfoField(
help="Date of Manufacture", info="2023-01-15" help="Date of Manufacture", info="2023-01-15"
) )
bms.bms_temperature_celcius = BMSMultiField( bms.bms_temperature_celsius = BMSMultiField(
help="Temperature", help="Temperature",
label="sensor", label="sensor",
raw_values={1: 25.0}, raw_values={1: 25.0},
+1 -1
View File
@@ -103,7 +103,7 @@ def test_legacy_numeric_decoder_entry_points(model, packets):
@pytest.mark.parametrize("decoder_type,high_packet,low_packet,field,high,low", [ @pytest.mark.parametrize("decoder_type,high_packet,low_packet,field,high,low", [
(ecoflow_river2.River2Decoder, EMS_PACKET, PD_PACKET, "bms_capacity_charge_ratio", 0.875, 0.47), (ecoflow_river2.River2Decoder, EMS_PACKET, PD_PACKET, "bms_capacity_charge_ratio", 0.875, 0.47),
(ecoflow_river3.River3Decoder, CELL_TEMP_PACKET, COMPOSITE_TEMP_PACKET, "bms_temperature_celcius", 29, 20), (ecoflow_river3.River3Decoder, CELL_TEMP_PACKET, COMPOSITE_TEMP_PACKET, "bms_temperature_celsius", 29, 20),
], ids=["river2_soc", "river3_temperature"]) ], ids=["river2_soc", "river3_temperature"])
def test_measurement_priorities_are_per_instance_and_reset_per_connection( def test_measurement_priorities_are_per_instance_and_reset_per_connection(
decoder_type, high_packet, low_packet, field, high, low, decoder_type, high_packet, low_packet, field, high, low,
+5
View File
@@ -145,6 +145,11 @@ class TestUPS:
assert isinstance(items["bms_temps"], BMSMultiField) assert isinstance(items["bms_temps"], BMSMultiField)
assert items["bms_temps"].raw_values == {1: 25.0} assert items["bms_temps"].raw_values == {1: 25.0}
def test_from_dict_corrects_renamed_field(self):
field = {"help": "Temperature", "label": "sensor", "raw_values": {1: 25.0}, "values": {1: "25.00"}}
ups = UPS.from_dict({"bms_temperature_celcius": field})
assert [name for name, _ in ups.items()] == ["bms_temperature_celsius"]
def test_bool_true_when_populated(self): def test_bool_true_when_populated(self):
assert bool(self._snapshot()) is True assert bool(self._snapshot()) is True
+2 -2
View File
@@ -18,7 +18,7 @@ MOCK_RESPONSE = {
"help": "Date of Manufacture", "help": "Date of Manufacture",
"info": "2023-01-15", "info": "2023-01-15",
}, },
"bms_temperature_celcius": { "bms_temperature_celsius": {
"help": "Temperature", "help": "Temperature",
"label": "sensor", "label": "sensor",
"raw_values": {1: 25.0}, "raw_values": {1: 25.0},
@@ -70,7 +70,7 @@ class TestReadData:
def test_multi_field_deserialized(self): def test_multi_field_deserialized(self):
result = self._call() result = self._call()
items = dict(result["myups"].items()) items = dict(result["myups"].items())
f = items["bms_temperature_celcius"] f = items["bms_temperature_celsius"]
assert isinstance(f, BMSMultiField) assert isinstance(f, BMSMultiField)
assert f.raw_values == {1: 25.0} assert f.raw_values == {1: 25.0}
assert f.label == "sensor" assert f.label == "sensor"
+3 -3
View File
@@ -217,8 +217,8 @@ def test_snapshot_contains_power_temperature_and_times():
assert snap.ef_input_power_watts.units == "W" assert snap.ef_input_power_watts.units == "W"
assert snap.ef_output_power_watts.raw_value == 42 assert snap.ef_output_power_watts.raw_value == 42
assert snap.bms_capacity_charge_ratio.raw_value == 0.7325 assert snap.bms_capacity_charge_ratio.raw_value == 0.7325
assert snap.bms_temperature_celcius.raw_values == {1: 29} assert snap.bms_temperature_celsius.raw_values == {1: 29}
assert snap.bms_temperature_celcius.units == "°C" assert snap.bms_temperature_celsius.units == "°C"
assert snap.ef_remaining_time_minutes.raw_value == 87 assert snap.ef_remaining_time_minutes.raw_value == 87
assert snap.ef_charge_remaining_time_minutes.raw_value == 87 assert snap.ef_charge_remaining_time_minutes.raw_value == 87
assert snap.ef_discharge_remaining_time_minutes.raw_value == 240 assert snap.ef_discharge_remaining_time_minutes.raw_value == 240
@@ -261,7 +261,7 @@ def test_unavailable_times_clear_previous_values_and_zero_is_valid():
def test_short_bms_has_temperature_without_inventing_charge_status(): def test_short_bms_has_temperature_without_inventing_charge_status():
dev = device() dev = device()
dev._record(heartbeat(3, bms_payload()[:21], 0x32)) dev._record(heartbeat(3, bms_payload()[:21], 0x32))
assert dev.snapshot.bms_temperature_celcius.raw_values == {1: 29} assert dev.snapshot.bms_temperature_celsius.raw_values == {1: 29}
assert dev.snapshot.bms_capacity_charge_ratio.raw_value == 0.73 assert dev.snapshot.bms_capacity_charge_ratio.raw_value == 0.73
assert dev.snapshot.bms_charge_is_charging is None assert dev.snapshot.bms_charge_is_charging is None
+1 -1
View File
@@ -95,7 +95,7 @@ def test_river2_bypass_burst_without_inverter_finishes_immediately(script, monke
snap = dev.snapshot snap = dev.snapshot
assert snap.ef_packets_received.raw_value == 5 assert snap.ef_packets_received.raw_value == 5
assert snap.bms_capacity_charge_ratio.raw_value == pytest.approx(0.8775) assert snap.bms_capacity_charge_ratio.raw_value == pytest.approx(0.8775)
assert snap.bms_temperature_celcius.raw_values == {1: 28} assert snap.bms_temperature_celsius.raw_values == {1: 28}
assert snap.bms_charge_is_charging.raw_value is False assert snap.bms_charge_is_charging.raw_value is False
assert snap.bms_charge_is_discharging.raw_value is False assert snap.bms_charge_is_discharging.raw_value is False
assert snap.ef_input_power_watts.raw_value == snap.ef_output_power_watts.raw_value == 35 assert snap.ef_input_power_watts.raw_value == snap.ef_output_power_watts.raw_value == 35
+5 -5
View File
@@ -111,7 +111,7 @@ def test_river3_snapshot_and_serialization(prefix):
snap = dev.snapshot snap = dev.snapshot
assert dev._pkt_version == 3 assert dev._pkt_version == 3
assert snap.bms_capacity_charge_ratio.raw_value == pytest.approx(0.88918) assert snap.bms_capacity_charge_ratio.raw_value == pytest.approx(0.88918)
assert snap.bms_temperature_celcius.raw_values == {1: 29} assert snap.bms_temperature_celsius.raw_values == {1: 29}
assert snap.ef_input_power_watts.raw_value == 244 assert snap.ef_input_power_watts.raw_value == 244
assert snap.ef_output_power_watts.raw_value == 39 assert snap.ef_output_power_watts.raw_value == 39
assert snap.bms_charge_is_charging.raw_value is True assert snap.bms_charge_is_charging.raw_value is True
@@ -142,7 +142,7 @@ def test_partial_updates_keep_missing_values_and_preserve_explicit_zero():
assert dev.snapshot.bms_charge_is_charging.raw_value is False assert dev.snapshot.bms_charge_is_charging.raw_value is False
assert dev.snapshot.bms_charge_is_discharging.raw_value is False assert dev.snapshot.bms_charge_is_discharging.raw_value is False
assert dev.snapshot.ef_remaining_time_minutes is None assert dev.snapshot.ef_remaining_time_minutes is None
assert dev.snapshot.bms_temperature_celcius.raw_values == {1: -5} assert dev.snapshot.bms_temperature_celsius.raw_values == {1: -5}
assert dev.snapshot.ef_output_power_watts.raw_value == 44 assert dev.snapshot.ef_output_power_watts.raw_value == 44
@@ -150,9 +150,9 @@ def test_cell_temperature_remains_preferred_across_partial_updates():
dev = device() dev = device()
dev._record(report(integer(259, 29))) dev._record(report(integer(259, 29)))
dev._record(report(integer(102, 26))) dev._record(report(integer(102, 26)))
assert dev.snapshot.bms_temperature_celcius.raw_values == {1: 29} assert dev.snapshot.bms_temperature_celsius.raw_values == {1: 29}
dev._record(report(integer(259, 30))) dev._record(report(integer(259, 30)))
assert dev.snapshot.bms_temperature_celcius.raw_values == {1: 30} assert dev.snapshot.bms_temperature_celsius.raw_values == {1: 30}
def test_remaining_time_tracks_battery_direction_and_missing_estimates(): def test_remaining_time_tracks_battery_direction_and_missing_estimates():
@@ -184,7 +184,7 @@ def test_no_load_sample_matches_display_hours_without_changing_minute_units():
dev._record(report(integer(363, 1))) dev._record(report(integer(363, 1)))
snap = dev.snapshot snap = dev.snapshot
assert snap.bms_capacity_charge_ratio.raw_value == pytest.approx(0.89) assert snap.bms_capacity_charge_ratio.raw_value == pytest.approx(0.89)
assert snap.bms_temperature_celcius.raw_values == {1: 29} assert snap.bms_temperature_celsius.raw_values == {1: 29}
assert snap.ef_input_power_watts.raw_value == 0 assert snap.ef_input_power_watts.raw_value == 0
assert snap.ef_output_power_watts.raw_value == 0 assert snap.ef_output_power_watts.raw_value == 0
assert snap.bms_charge_is_charging.raw_value is False assert snap.bms_charge_is_charging.raw_value is False
+7 -7
View File
@@ -153,13 +153,13 @@ class TestJBDBMS:
assert "bms_voltage_total_volts" in keys assert "bms_voltage_total_volts" in keys
assert "bms_current_amps" in keys assert "bms_current_amps" in keys
assert "bms_manufacture_date" in keys assert "bms_manufacture_date" in keys
assert "bms_temperature_celcius" in keys assert "bms_temperature_celsius" in keys
def test_items_yields_correct_types(self, populated_jbdbms): def test_items_yields_correct_types(self, populated_jbdbms):
d = dict(populated_jbdbms.items()) d = dict(populated_jbdbms.items())
assert isinstance(d["bms_voltage_total_volts"], BMSScalarField) assert isinstance(d["bms_voltage_total_volts"], BMSScalarField)
assert isinstance(d["bms_manufacture_date"], BMSInfoField) assert isinstance(d["bms_manufacture_date"], BMSInfoField)
assert isinstance(d["bms_temperature_celcius"], BMSMultiField) assert isinstance(d["bms_temperature_celsius"], BMSMultiField)
def test_is_ups_subclass(self): def test_is_ups_subclass(self):
assert isinstance(JBDBMS(), UPS) assert isinstance(JBDBMS(), UPS)
@@ -209,8 +209,8 @@ class TestParse03Response:
def test_temperature(self, valid_03_response): def test_temperature(self, valid_03_response):
result = parse_03_response(valid_03_response) result = parse_03_response(valid_03_response)
assert result.bms_temperature_celcius.raw_values[1] == pytest.approx(25.0) assert result.bms_temperature_celsius.raw_values[1] == pytest.approx(25.0)
assert result.bms_temperature_celcius.units == "°C" assert result.bms_temperature_celsius.units == "°C"
def test_mosfet_charging(self, valid_03_response): def test_mosfet_charging(self, valid_03_response):
result = parse_03_response(valid_03_response) result = parse_03_response(valid_03_response)
@@ -379,9 +379,9 @@ class TestParse03ProtectionBits:
_recompute_checksum(response) _recompute_checksum(response)
result = parse_03_response(response) result = parse_03_response(response)
assert isinstance(result, JBDBMS) assert isinstance(result, JBDBMS)
assert len(result.bms_temperature_celcius.raw_values) == 2 assert len(result.bms_temperature_celsius.raw_values) == 2
assert result.bms_temperature_celcius.raw_values[1] == pytest.approx(25.0) assert result.bms_temperature_celsius.raw_values[1] == pytest.approx(25.0)
assert result.bms_temperature_celcius.raw_values[2] == pytest.approx(19.2) assert result.bms_temperature_celsius.raw_values[2] == pytest.approx(19.2)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+1 -1
View File
@@ -72,7 +72,7 @@ MODEL_PROFILES = {
# RuntimePropertyUpload (0x16) is not confirmed for River 3. # RuntimePropertyUpload (0x16) is not confirmed for River 3.
"expected_fields": { "expected_fields": {
"ef_pkt_02_fe_15", "bms_capacity_charge_ratio", "ef_pkt_02_fe_15", "bms_capacity_charge_ratio",
"bms_temperature_celcius", "ef_input_power_watts", "bms_temperature_celsius", "ef_input_power_watts",
"ef_output_power_watts", "bms_charge_is_charging", "ef_output_power_watts", "bms_charge_is_charging",
}, },
}, },