diff --git a/openevsehttp/commands.py b/openevsehttp/commands.py index b006d48..465afef 100644 --- a/openevsehttp/commands.py +++ b/openevsehttp/commands.py @@ -667,3 +667,23 @@ async def toggle_shaper(self) -> None: new_state = not bool(shaper_active) await self.set_shaper(new_state) + + async def set_mqtt_vehicle_range_miles(self, enable: bool = True) -> None: + """Set mqtt_vehicle_range_miles configuration setting. + + Dynamically changing this setting will affect future evaluations of + the vehicle_range_with_unit property. + """ + if not isinstance(enable, bool): + raise TypeError("Value must be a boolean.") + + url = f"{self.url}config" + data = {"mqtt_vehicle_range_miles": enable} + + _LOGGER.debug("Setting mqtt_vehicle_range_miles to %s", enable) + response = await self.process_request(url=url, method="post", data=data) + response = self._normalize_response(response) + msg = response.get("msg") if isinstance(response, Mapping) else None + if msg not in SUCCESS_ANSWERS: + _LOGGER.error("Problem issuing command: %s", response) + raise UnknownError diff --git a/openevsehttp/properties.py b/openevsehttp/properties.py index e77437d..7636764 100644 --- a/openevsehttp/properties.py +++ b/openevsehttp/properties.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import warnings from collections.abc import Mapping from datetime import datetime, timedelta, timezone from typing import Any, cast @@ -458,11 +459,33 @@ def vehicle_soc(self) -> int | None: @property def vehicle_range(self) -> int | None: """Return battery range.""" + warnings.warn( + "vehicle_range is deprecated, use vehicle_range_with_unit instead", + DeprecationWarning, + stacklevel=2, + ) return cast( "int | None", self._status.get("vehicle_range", self._status.get("battery_range", None)), ) + @property + def vehicle_range_with_unit(self) -> tuple[int, str] | None: + """Return battery range and its unit.""" + value = cast( + "int | None", + self._status.get("vehicle_range", self._status.get("battery_range", None)), + ) + if value is None: + return None + unit = "miles" if self.mqtt_vehicle_range_miles else "km" + return (value, unit) + + @property + def mqtt_vehicle_range_miles(self) -> bool: + """Return True if mqtt vehicle range is in miles, False if km.""" + return bool(self._config.get("mqtt_vehicle_range_miles", False)) + @property def vehicle_eta(self) -> datetime | None: """Return time to full charge.""" diff --git a/tests/test_commands.py b/tests/test_commands.py index 3f776eb..b614094 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1334,3 +1334,36 @@ async def test_update_firmware_bytes_empty(test_charger, caplog): with pytest.raises(ValueError): await test_charger.update_firmware(firmware_bytes=b"") assert "Empty firmware bytes provided" in caplog.text + + +async def test_set_mqtt_vehicle_range_miles(test_charger_new, mock_aioclient, caplog): + """Test set_mqtt_vehicle_range_miles command.""" + await test_charger_new.update() + mock_aioclient.post( + TEST_URL_CONFIG, + status=200, + body='{"msg": "OK"}', + ) + with caplog.at_level(logging.DEBUG): + await test_charger_new.set_mqtt_vehicle_range_miles(True) + assert "Setting mqtt_vehicle_range_miles to True" in caplog.text + + mock_aioclient.post( + TEST_URL_CONFIG, + status=200, + body='{"msg": "OK"}', + ) + with caplog.at_level(logging.DEBUG): + await test_charger_new.set_mqtt_vehicle_range_miles(False) + assert "Setting mqtt_vehicle_range_miles to False" in caplog.text + + with pytest.raises(TypeError, match=r"Value must be a boolean\."): + await test_charger_new.set_mqtt_vehicle_range_miles("invalid") + + mock_aioclient.post( + TEST_URL_CONFIG, + status=200, + body='{"msg": "error"}', + ) + with pytest.raises(UnknownError): + await test_charger_new.set_mqtt_vehicle_range_miles(True) diff --git a/tests/test_properties.py b/tests/test_properties.py index 6bfb5e5..cfd4023 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -159,6 +159,8 @@ ("test_charger_v2", "vehicle_soc", None), ("test_charger", "vehicle_range", 468), ("test_charger_v2", "vehicle_range", None), + ("test_charger", "vehicle_range_with_unit", (468, "km")), + ("test_charger_v2", "vehicle_range_with_unit", None), # shaper ("test_charger", "shaper_active", True), ("test_charger_v2", "shaper_active", None), @@ -207,7 +209,11 @@ async def test_simple_properties(fixture, prop, expected, request): with pytest.raises(expected): _ = getattr(charger, prop) else: - assert getattr(charger, prop) == expected + if prop == "vehicle_range": + with pytest.deprecated_call(): + assert getattr(charger, prop) == expected + else: + assert getattr(charger, prop) == expected await charger.ws_disconnect() @@ -490,3 +496,23 @@ async def test_wifi_firmware_none(): charger = OpenEVSE(SERVER_URL) charger._config = {} assert charger.wifi_firmware is None + + +async def test_mqtt_vehicle_range_miles(): + """Test mqtt_vehicle_range_miles property and vehicle_range_with_unit unit selection.""" + charger = OpenEVSE(SERVER_URL) + # Default is False + assert charger.mqtt_vehicle_range_miles is False + + charger._config = {"mqtt_vehicle_range_miles": True} + assert charger.mqtt_vehicle_range_miles is True + + charger._status = {"vehicle_range": 150} + with pytest.deprecated_call(): + assert charger.vehicle_range == 150 + assert charger.vehicle_range_with_unit == (150, "miles") + + charger._config = {"mqtt_vehicle_range_miles": False} + with pytest.deprecated_call(): + assert charger.vehicle_range == 150 + assert charger.vehicle_range_with_unit == (150, "km")