diff --git a/openevsehttp/__init__.py b/openevsehttp/__init__.py index 1b6b720..96304f6 100644 --- a/openevsehttp/__init__.py +++ b/openevsehttp/__init__.py @@ -4,9 +4,11 @@ import asyncio import datetime import logging +from json.decoder import JSONDecodeError from typing import Any, Callable, Optional import aiohttp # type: ignore +from awesomeversion import AwesomeVersion from .const import MAX_AMPS, MIN_AMPS from .exceptions import ( @@ -200,6 +202,8 @@ async def process_request( message = await resp.json() except TimeoutError: _LOGGER.error("%s: %s", ERROR_TIMEOUT, url) + except JSONDecodeError: + message = {"msg": resp} if resp.status == 400: _LOGGER.error("%s", message["msg"]) @@ -346,6 +350,29 @@ async def set_charge_mode(self, mode: str = "fast") -> None: _LOGGER.error("Problem issuing command: %s", response["msg"]) raise UnknownError + async def divert_mode(self, mode: str = "Normal") -> None: + """Set the divert mode to either Normal or Eco modes.""" + url = f"{self.url}divertmode" + + if mode != "Normal" or mode != "Eco": + _LOGGER.error("Invalid value for divertmode: %s", mode) + raise ValueError + + if mode == "Normal": + value = 1 + else: + value = 2 + + data = {"divertmode": value} + + _LOGGER.debug("Setting charge mode to %s", mode) + response = await self.process_request( + url=url, method="post", data=data + ) # noqa: E501 + if response["msg"] != "done": + _LOGGER.error("Problem issuing command: %s", response["msg"]) + raise UnknownError + async def get_override(self) -> None: """Get the manual override status.""" url = f"{self.url}override" @@ -386,11 +413,26 @@ async def set_override( async def toggle_override(self) -> None: """Toggle the manual override status.""" - url = f"{self.url}override" + # 3.x: use RAPI commands $FE (enable) and $FS (sleep) + # 4.x: use HTTP API call - _LOGGER.debug("Toggling manual override %s", url) - response = await self.process_request(url=url, method="patch") - _LOGGER.debug("Toggle response: %s", response["msg"]) + cutoff = AwesomeVersion("4.0.0") + current = AwesomeVersion(self._config["version"]) + + _LOGGER.debug("Detected firmware: %s", current) + + if cutoff <= current: + url = f"{self.url}override" + + _LOGGER.debug("Toggling manual override %s", url) + response = await self.process_request(url=url, method="patch") + _LOGGER.debug("Toggle response: %s", response) + else: + # Older firmware use RAPI commands + _LOGGER.debug("Toggling manual override via RAPI") + command = "$FE" if self._status["state"] == "sleeping" else "$FS" + response = await self.send_command(command) + _LOGGER.debug("Toggle response: %s", response[1]) async def clear_override(self) -> None: """Clear the manual override status.""" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c0152f4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +awesomeversion \ No newline at end of file diff --git a/requirements_lint.txt b/requirements_lint.txt index 113c9d6..2c1fe94 100644 --- a/requirements_lint.txt +++ b/requirements_lint.txt @@ -1,5 +1,6 @@ -black==21.12b0 -flake8==4.0.1 -mypy==0.931 +-r requirements.txt +black==21.7b0 +flake8==3.9.2 +mypy==0.910 pydocstyle==6.1.1 pylint==2.12.2 \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index 4cb5bfa..693a471 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,3 +1,4 @@ +-r requirements.txt pytest==6.2.5 pytest-cov==3.0.0 pytest-timeout==2.0.2 diff --git a/setup.py b/setup.py index 8dc625b..02f11fd 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ PROJECT_DIR = Path(__file__).parent.resolve() README_FILE = PROJECT_DIR / "README.md" -VERSION = "0.1.13" +VERSION = "0.1.14" setup( diff --git a/tests/conftest.py b/tests/conftest.py index 9c08faa..1657270 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,12 @@ """Provide common pytest fixtures.""" -import pytest import json +import pytest +from aioresponses import aioresponses + import openevsehttp from tests.common import load_fixture -from aioresponses import aioresponses - TEST_URL_STATUS = "http://openevse.test.tld/status" TEST_URL_CONFIG = "http://openevse.test.tld/config" TEST_URL_RAPI = "http://openevse.test.tld/r" diff --git a/tests/fixtures/v4_json/status.json b/tests/fixtures/v4_json/status.json index 03a3bad..c35bb8b 100644 --- a/tests/fixtures/v4_json/status.json +++ b/tests/fixtures/v4_json/status.json @@ -27,7 +27,7 @@ "vehicle": 1, "colour": 6, "freeram": 223464, - "divertmode": 1, + "divertmode": 0, "srssi": -61, "elapsed": 246, "wattsec": 992549, diff --git a/tests/test_init.py b/tests/test_init.py index 3a303a0..0de1e62 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,11 +1,16 @@ import asyncio import json + +import logging + import pytest + import openevsehttp pytestmark = pytest.mark.asyncio TEST_URL_RAPI = "http://openevse.test.tld/r" +TEST_URL_OVERRIDE = "http://openevse.test.tld/override" async def test_get_status_auth(test_charger_auth): @@ -508,3 +513,100 @@ async def test_get_relayt(fixture, expected, request): await charger.update() status = charger.stuck_relay_check_enabled assert status == expected + + +@pytest.mark.parametrize( + "fixture, expected", [("test_charger", "eco"), ("test_charger_v2", "normal")] +) +async def test_get_divertmode(fixture, expected, request): + """Test v4 Status reply""" + charger = request.getfixturevalue(fixture) + await charger.update() + status = charger.divertmode + assert status == expected + + +@pytest.mark.parametrize( + "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] +) +async def test_get_charge_rate(fixture, expected, request): + """Test v4 Status reply""" + charger = request.getfixturevalue(fixture) + await charger.update() + status = charger.charge_rate + assert status == expected + + +@pytest.mark.parametrize( + "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] +) +async def test_get_available_current(fixture, expected, request): + """Test v4 Status reply""" + charger = request.getfixturevalue(fixture) + await charger.update() + with pytest.raises(KeyError): + status = charger.available_current + # assert status == expected + + +@pytest.mark.parametrize( + "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] +) +async def test_get_smoothed_available_current(fixture, expected, request): + """Test v4 Status reply""" + charger = request.getfixturevalue(fixture) + await charger.update() + with pytest.raises(KeyError): + status = charger.smoothed_available_current + # assert status == expected + + +@pytest.mark.parametrize( + "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] +) +async def test_get_divert_active(fixture, expected, request): + """Test v4 Status reply""" + charger = request.getfixturevalue(fixture) + await charger.update() + with pytest.raises(KeyError): + status = charger.divert_active + # assert status == expected + + +@pytest.mark.parametrize( + "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] +) +async def test_get_manual_override(fixture, expected, request): + """Test v4 Status reply""" + charger = request.getfixturevalue(fixture) + await charger.update() + with pytest.raises(KeyError): + status = charger.manual_override + # assert status == expected + + +async def test_toggle_override(test_charger, mock_aioclient, caplog): + """Test v4 Status reply""" + await test_charger.update() + mock_aioclient.patch( + TEST_URL_OVERRIDE, + status=200, + body="OK", + ) + with caplog.at_level(logging.DEBUG): + await test_charger.toggle_override() + assert "Toggling manual override http" in caplog.text + + +async def test_toggle_override_v2(test_charger_v2, mock_aioclient, caplog): + """Test v4 Status reply""" + await test_charger_v2.update() + value = {"cmd": "OK", "ret": "$OK^20"} + mock_aioclient.post( + TEST_URL_RAPI, + status=200, + body=json.dumps(value), + ) + with caplog.at_level(logging.DEBUG): + await test_charger_v2.toggle_override() + assert "Toggling manual override via RAPI" in caplog.text