diff --git a/bmspy/classes.py b/bmspy/classes.py index 73637ac..ba3ce08 100644 --- a/bmspy/classes.py +++ b/bmspy/classes.py @@ -44,6 +44,10 @@ class 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: """Reconstruct a BMSField from its JSON-serialized dict.""" @@ -69,7 +73,9 @@ class UPS(ABC): @classmethod def from_dict(cls, data: dict[str, Any]) -> "UPS": """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): diff --git a/bmspy/ecoflow_river2.py b/bmspy/ecoflow_river2.py index 0fa40a5..f6f4e95 100644 --- a/bmspy/ecoflow_river2.py +++ b/bmspy/ecoflow_river2.py @@ -353,7 +353,7 @@ class River2Decoder: else: charge_ratio(values["soc"], priority=1) temp = values["temp"] - snapshot.bms_temperature_celcius = BMSMultiField( + snapshot.bms_temperature_celsius = BMSMultiField( help="Battery Temperature", label="sensor", raw_values={1: temp}, values={1: str(temp)}, units="°C", ) diff --git a/bmspy/ecoflow_river3.py b/bmspy/ecoflow_river3.py index 1893c48..e4f9d34 100644 --- a/bmspy/ecoflow_river3.py +++ b/bmspy/ecoflow_river3.py @@ -305,7 +305,7 @@ class River3Decoder: for source, priority in (("cms_batt_temp", 1), ("bms_max_cell_temp", 2)): temp = values.get(source) 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", raw_values={1: temp}, values={1: str(temp)}, units="°C", ) diff --git a/bmspy/ecoflow_snapshot.py b/bmspy/ecoflow_snapshot.py index 10dbf89..a7bfc0b 100644 --- a/bmspy/ecoflow_snapshot.py +++ b/bmspy/ecoflow_snapshot.py @@ -23,7 +23,7 @@ class EcoFlowSnapshot(UPS): ef_packets_received: 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_discharging: BMSScalarField | None = None ef_input_power_watts: BMSScalarField | None = None diff --git a/bmspy/jbd_proto.py b/bmspy/jbd_proto.py index c17534f..d9825a5 100644 --- a/bmspy/jbd_proto.py +++ b/bmspy/jbd_proto.py @@ -295,7 +295,7 @@ def parse_03_response(response: bytearray, debug: int = 0) -> JBDBMS | bool: result.bms_temperature_sensor_num = BMSScalarField( help="Temperature Sensors", raw_value=ntc_num, value="{0}".format(ntc_num) ) - result.bms_temperature_celcius = BMSMultiField( + result.bms_temperature_celsius = BMSMultiField( help="Temperature", label="sensor", raw_values={i + 1: temp for i, temp in enumerate(temperatures)}, diff --git a/bmspy/jbd_snapshot.py b/bmspy/jbd_snapshot.py index 3c090cf..0a4ae91 100644 --- a/bmspy/jbd_snapshot.py +++ b/bmspy/jbd_snapshot.py @@ -33,7 +33,7 @@ class JBDBMS(UPS): bms_cell_number: BMSScalarField | None = None bms_cells_balancing: BMSMultiField | 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 def items(self) -> Iterator[tuple[str, BMSField]]: diff --git a/docs/ecoflow-river2-fields.md b/docs/ecoflow-river2-fields.md index 47dd2f0..407ae09 100644 --- a/docs/ecoflow-river2-fields.md +++ b/docs/ecoflow-river2-fields.md @@ -19,7 +19,7 @@ rules for interpreting the device reports. |---|---| | `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_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. | | `ef_remaining_time_minutes` | General remaining-time estimate, minutes. | | `ef_charge_remaining_time_minutes` | Charging-time estimate from the energy management module, minutes. | diff --git a/tests/conftest.py b/tests/conftest.py index b43f871..bc6f8eb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,7 +68,7 @@ def populated_jbdbms() -> JBDBMS: bms.bms_manufacture_date = BMSInfoField( help="Date of Manufacture", info="2023-01-15" ) - bms.bms_temperature_celcius = BMSMultiField( + bms.bms_temperature_celsius = BMSMultiField( help="Temperature", label="sensor", raw_values={1: 25.0}, diff --git a/tests/test_backend_modules.py b/tests/test_backend_modules.py index 86b6443..b4d81f1 100644 --- a/tests/test_backend_modules.py +++ b/tests/test_backend_modules.py @@ -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", [ (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"]) def test_measurement_priorities_are_per_instance_and_reset_per_connection( decoder_type, high_packet, low_packet, field, high, low, diff --git a/tests/test_classes.py b/tests/test_classes.py index 2e1c3ae..d81404b 100644 --- a/tests/test_classes.py +++ b/tests/test_classes.py @@ -145,6 +145,11 @@ class TestUPS: assert isinstance(items["bms_temps"], BMSMultiField) 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): assert bool(self._snapshot()) is True diff --git a/tests/test_client.py b/tests/test_client.py index 0993636..e74ea0a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -18,7 +18,7 @@ MOCK_RESPONSE = { "help": "Date of Manufacture", "info": "2023-01-15", }, - "bms_temperature_celcius": { + "bms_temperature_celsius": { "help": "Temperature", "label": "sensor", "raw_values": {1: 25.0}, @@ -70,7 +70,7 @@ class TestReadData: def test_multi_field_deserialized(self): result = self._call() items = dict(result["myups"].items()) - f = items["bms_temperature_celcius"] + f = items["bms_temperature_celsius"] assert isinstance(f, BMSMultiField) assert f.raw_values == {1: 25.0} assert f.label == "sensor" diff --git a/tests/test_ecoflow_bt.py b/tests/test_ecoflow_bt.py index 683aa10..8b1af12 100644 --- a/tests/test_ecoflow_bt.py +++ b/tests/test_ecoflow_bt.py @@ -217,8 +217,8 @@ def test_snapshot_contains_power_temperature_and_times(): assert snap.ef_input_power_watts.units == "W" assert snap.ef_output_power_watts.raw_value == 42 assert snap.bms_capacity_charge_ratio.raw_value == 0.7325 - assert snap.bms_temperature_celcius.raw_values == {1: 29} - assert snap.bms_temperature_celcius.units == "°C" + assert snap.bms_temperature_celsius.raw_values == {1: 29} + assert snap.bms_temperature_celsius.units == "°C" assert snap.ef_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 @@ -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(): dev = device() 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_charge_is_charging is None diff --git a/tests/test_ecoflow_diagnostics_river2.py b/tests/test_ecoflow_diagnostics_river2.py index e3cb289..92e8914 100644 --- a/tests/test_ecoflow_diagnostics_river2.py +++ b/tests/test_ecoflow_diagnostics_river2.py @@ -95,7 +95,7 @@ def test_river2_bypass_burst_without_inverter_finishes_immediately(script, monke snap = dev.snapshot assert snap.ef_packets_received.raw_value == 5 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_discharging.raw_value is False assert snap.ef_input_power_watts.raw_value == snap.ef_output_power_watts.raw_value == 35 diff --git a/tests/test_ecoflow_river3.py b/tests/test_ecoflow_river3.py index 271e078..9ec1a87 100644 --- a/tests/test_ecoflow_river3.py +++ b/tests/test_ecoflow_river3.py @@ -111,7 +111,7 @@ def test_river3_snapshot_and_serialization(prefix): snap = dev.snapshot assert dev._pkt_version == 3 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_output_power_watts.raw_value == 39 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_discharging.raw_value is False 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 @@ -150,9 +150,9 @@ def test_cell_temperature_remains_preferred_across_partial_updates(): dev = device() dev._record(report(integer(259, 29))) 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))) - 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(): @@ -184,7 +184,7 @@ def test_no_load_sample_matches_display_hours_without_changing_minute_units(): dev._record(report(integer(363, 1))) snap = dev.snapshot 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_output_power_watts.raw_value == 0 assert snap.bms_charge_is_charging.raw_value is False diff --git a/tests/test_jbd_bms.py b/tests/test_jbd_bms.py index 65d5cb1..9896d9b 100644 --- a/tests/test_jbd_bms.py +++ b/tests/test_jbd_bms.py @@ -153,13 +153,13 @@ class TestJBDBMS: assert "bms_voltage_total_volts" in keys assert "bms_current_amps" 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): d = dict(populated_jbdbms.items()) assert isinstance(d["bms_voltage_total_volts"], BMSScalarField) 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): assert isinstance(JBDBMS(), UPS) @@ -209,8 +209,8 @@ class TestParse03Response: def test_temperature(self, 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_celcius.units == "°C" + assert result.bms_temperature_celsius.raw_values[1] == pytest.approx(25.0) + assert result.bms_temperature_celsius.units == "°C" def test_mosfet_charging(self, valid_03_response): result = parse_03_response(valid_03_response) @@ -379,9 +379,9 @@ class TestParse03ProtectionBits: _recompute_checksum(response) result = parse_03_response(response) assert isinstance(result, JBDBMS) - assert len(result.bms_temperature_celcius.raw_values) == 2 - assert result.bms_temperature_celcius.raw_values[1] == pytest.approx(25.0) - assert result.bms_temperature_celcius.raw_values[2] == pytest.approx(19.2) + assert len(result.bms_temperature_celsius.raw_values) == 2 + assert result.bms_temperature_celsius.raw_values[1] == pytest.approx(25.0) + assert result.bms_temperature_celsius.raw_values[2] == pytest.approx(19.2) # --------------------------------------------------------------------------- diff --git a/tools/ecoflow-bt.py b/tools/ecoflow-bt.py index 017305b..fd47078 100644 --- a/tools/ecoflow-bt.py +++ b/tools/ecoflow-bt.py @@ -72,7 +72,7 @@ MODEL_PROFILES = { # RuntimePropertyUpload (0x16) is not confirmed for River 3. "expected_fields": { "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", }, },