diff --git a/openevsehttp/__main__.py b/openevsehttp/__main__.py index 9fee51c..1c426f9 100644 --- a/openevsehttp/__main__.py +++ b/openevsehttp/__main__.py @@ -84,11 +84,18 @@ class OpenEVSE: """Represent an OpenEVSE charger.""" - def __init__(self, host: str, user: str = "", pwd: str = "") -> None: + def __init__( + self, + host: str, + user: str = "", + pwd: str = "", + session: aiohttp.ClientSession | None = None, + ) -> None: """Connect to an OpenEVSE charger equipped with wifi or ethernet.""" self._user = user self._pwd = pwd self.url = f"http://{host}/" + self._session = session self._status: dict = {} self._config: dict = {} self._override = None @@ -98,6 +105,13 @@ def __init__(self, host: str, user: str = "", pwd: str = "") -> None: self._loop = None self.tasks = None + @property + def session(self) -> aiohttp.ClientSession: + """Return the aiohttp session, creating one if needed.""" + if self._session is None: + self._session = aiohttp.ClientSession() + return self._session + async def process_request( self, url: str, @@ -113,61 +127,57 @@ async def process_request( if self._user and self._pwd: auth = aiohttp.BasicAuth(self._user, self._pwd) - async with aiohttp.ClientSession() as session: - http_method = getattr(session, method) - _LOGGER.debug( - "Connecting to %s with data: %s rapi: %s using method %s", + http_method = getattr(self.session, method) + _LOGGER.debug( + "Connecting to %s with data: %s rapi: %s using method %s", + url, + data, + rapi, + method, + ) + try: + async with http_method( url, - data, - rapi, - method, - ) - try: - async with http_method( - url, - data=rapi, - json=data, - auth=auth, - ) as resp: - try: - message = await resp.text() - except UnicodeDecodeError: - _LOGGER.debug("Decoding error") - message = await resp.read() - message = message.decode(errors="replace") - - try: - message = json.loads(message) - except ValueError: - _LOGGER.warning("Non JSON response: %s", message) - - if resp.status == 400: - index = "" - if "msg" in message.keys(): - index = "msg" - elif "error" in message.keys(): - index = "error" - _LOGGER.error("Error 400: %s", message[index]) - raise ParseJSONError - if resp.status == 401: - _LOGGER.error("Authentication error: %s", message) - raise AuthenticationError - if resp.status in [404, 405, 500]: - _LOGGER.warning("%s", message) - - if method == "post" and "config_version" in message: - await self.update() - return message - - except (TimeoutError, ServerTimeoutError) as err: - _LOGGER.error("%s: %s", ERROR_TIMEOUT, url) - raise err - except ContentTypeError as err: - _LOGGER.error("Content error: %s", err.message) - raise err - - await session.close() - return message + data=rapi, + json=data, + auth=auth, + ) as resp: + try: + message = await resp.text() + except UnicodeDecodeError: + _LOGGER.debug("Decoding error") + message = await resp.read() + message = message.decode(errors="replace") + + try: + message = json.loads(message) + except ValueError: + _LOGGER.warning("Non JSON response: %s", message) + + if resp.status == 400: + index = "" + if "msg" in message.keys(): + index = "msg" + elif "error" in message.keys(): + index = "error" + _LOGGER.error("Error 400: %s", message[index]) + raise ParseJSONError + if resp.status == 401: + _LOGGER.error("Authentication error: %s", message) + raise AuthenticationError + if resp.status in [404, 405, 500]: + _LOGGER.warning("%s", message) + + if method == "post" and "config_version" in message: + await self.update() + return message + + except (TimeoutError, ServerTimeoutError) as err: + _LOGGER.error("%s: %s", ERROR_TIMEOUT, url) + raise err + except ContentTypeError as err: + _LOGGER.error("Content error: %s", err.message) + raise err async def send_command(self, command: str) -> tuple: """Send a RAPI command to the charger and parses the response.""" @@ -204,7 +214,11 @@ async def update(self) -> None: if not self.websocket: # Start Websocket listening self.websocket = OpenEVSEWebsocket( - self.url, self._update_status, self._user, self._pwd + self.url, + self._update_status, + self._user, + self._pwd, + session=self.session, ) async def test_and_get(self) -> dict: @@ -573,23 +587,22 @@ async def firmware_check(self) -> dict | None: return None try: - async with aiohttp.ClientSession() as session: - http_method = getattr(session, method) - _LOGGER.debug( - "Connecting to %s using method %s", - url, - method, - ) - async with http_method(url) as resp: - if resp.status != 200: - return None - message = await resp.text() - message = json.loads(message) - response = {} - response["latest_version"] = message["tag_name"] - response["release_notes"] = message["body"] - response["release_url"] = message["html_url"] - return response + http_method = getattr(self.session, method) + _LOGGER.debug( + "Connecting to %s using method %s", + url, + method, + ) + async with http_method(url) as resp: + if resp.status != 200: + return None + message = await resp.text() + message = json.loads(message) + response = {} + response["latest_version"] = message["tag_name"] + response["release_notes"] = message["body"] + response["release_url"] = message["html_url"] + return response except (TimeoutError, ServerTimeoutError): _LOGGER.error("%s: %s", ERROR_TIMEOUT, url) diff --git a/openevsehttp/websocket.py b/openevsehttp/websocket.py index 3fc83ae..5bfaaa1 100644 --- a/openevsehttp/websocket.py +++ b/openevsehttp/websocket.py @@ -31,9 +31,10 @@ def __init__( callback, user=None, password=None, + session=None, ): """Initialize a OpenEVSEWebsocket instance.""" - self.session = aiohttp.ClientSession() + self._session = session self.uri = self._get_uri(server) self._user = user self._password = password @@ -45,6 +46,13 @@ def __init__( self._ping = None self._pong = None + @property + def session(self): + """Return the aiohttp session, creating one if needed.""" + if self._session is None: + self._session = aiohttp.ClientSession() + return self._session + @property def state(self): """Return the current state.""" @@ -159,7 +167,8 @@ async def listen(self): async def close(self): """Close the listening websocket.""" await self._set_state(STATE_STOPPED) - await self.session.close() + if self._client: + await self._client.close() async def keepalive(self): """Send ping requests to websocket.""" diff --git a/tests/conftest.py b/tests/conftest.py index 7108967..5b270ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ """Provide common pytest fixtures.""" +import aiohttp import pytest +import pytest_asyncio from aioresponses import aioresponses import openevsehttp.__main__ as main @@ -13,8 +15,21 @@ TEST_TLD = "openevse.test.tld" -@pytest.fixture(name="test_charger_auth") -def test_charger_auth(mock_aioclient): +@pytest_asyncio.fixture( + params=[None, "external"], + ids=["internal_session", "external_session"], +) +async def session(request): + """Provide a session fixture that yields None (internal) or external session.""" + if request.param == "external": + async with aiohttp.ClientSession() as session: + yield session + else: + yield None + + +@pytest_asyncio.fixture +async def test_charger_auth(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, @@ -32,11 +47,11 @@ def test_charger_auth(mock_aioclient): body=load_fixture("v4_json/status.json"), repeat=True, ) - return main.OpenEVSE(TEST_TLD, user="testuser", pwd="fakepassword") + return main.OpenEVSE(TEST_TLD, user="testuser", pwd="fakepassword", session=session) -@pytest.fixture(name="test_charger_auth_err") -def test_charger_auth_err(mock_aioclient): +@pytest_asyncio.fixture +async def test_charger_auth_err(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, @@ -46,11 +61,11 @@ def test_charger_auth_err(mock_aioclient): TEST_URL_CONFIG, status=401, ) - return main.OpenEVSE(TEST_TLD, user="testuser", pwd="fakepassword") + return main.OpenEVSE(TEST_TLD, user="testuser", pwd="fakepassword", session=session) -@pytest.fixture(name="test_charger") -def test_charger(mock_aioclient): +@pytest_asyncio.fixture +async def test_charger(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, @@ -68,21 +83,21 @@ def test_charger(mock_aioclient): body=load_fixture("v4_json/status.json"), repeat=True, ) - return main.OpenEVSE(TEST_TLD) + return main.OpenEVSE(TEST_TLD, session=session) -@pytest.fixture(name="test_charger_timeout") -def test_charger_timeout(mock_aioclient): +@pytest_asyncio.fixture +async def test_charger_timeout(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, exception=TimeoutError, ) - return main.OpenEVSE(TEST_TLD) + return main.OpenEVSE(TEST_TLD, session=session) -@pytest.fixture(name="test_charger_dev") -def test_charger_dev(mock_aioclient): +@pytest_asyncio.fixture +async def test_charger_dev(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, @@ -100,11 +115,11 @@ def test_charger_dev(mock_aioclient): body=load_fixture("v4_json/status.json"), repeat=True, ) - return main.OpenEVSE(TEST_TLD) + return main.OpenEVSE(TEST_TLD, session=session) -@pytest.fixture(name="test_charger_new") -def test_charger_new(mock_aioclient): +@pytest_asyncio.fixture +async def test_charger_new(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, @@ -122,11 +137,11 @@ def test_charger_new(mock_aioclient): body=load_fixture("v4_json/status-new.json"), repeat=True, ) - return main.OpenEVSE(TEST_TLD) + return main.OpenEVSE(TEST_TLD, session=session) -@pytest.fixture(name="test_charger_broken") -def test_charger_broken(mock_aioclient): +@pytest_asyncio.fixture +async def test_charger_broken(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, @@ -138,11 +153,11 @@ def test_charger_broken(mock_aioclient): status=200, body=load_fixture("v4_json/config-broken.json"), ) - return main.OpenEVSE(TEST_TLD) + return main.OpenEVSE(TEST_TLD, session=session) -@pytest.fixture(name="test_charger_broken_semver") -def test_charger_broken_semver(mock_aioclient): +@pytest_asyncio.fixture +async def test_charger_broken_semver(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, @@ -154,11 +169,11 @@ def test_charger_broken_semver(mock_aioclient): status=200, body=load_fixture("v4_json/config-broken-semver.json"), ) - return main.OpenEVSE(TEST_TLD) + return main.OpenEVSE(TEST_TLD, session=session) -@pytest.fixture(name="test_charger_unknown_semver") -def test_charger_unknown_semver(mock_aioclient): +@pytest_asyncio.fixture +async def test_charger_unknown_semver(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, @@ -170,11 +185,11 @@ def test_charger_unknown_semver(mock_aioclient): status=200, body=load_fixture("v4_json/config-unknown-semver.json"), ) - return main.OpenEVSE(TEST_TLD) + return main.OpenEVSE(TEST_TLD, session=session) -@pytest.fixture(name="test_charger_modified_ver") -def test_charger_modified_ver(mock_aioclient): +@pytest_asyncio.fixture +async def test_charger_modified_ver(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, @@ -186,11 +201,11 @@ def test_charger_modified_ver(mock_aioclient): status=200, body=load_fixture("v4_json/config-extra-version.json"), ) - return main.OpenEVSE(TEST_TLD) + return main.OpenEVSE(TEST_TLD, session=session) -@pytest.fixture(name="test_charger_v2") -def test_charger_v2(mock_aioclient): +@pytest_asyncio.fixture +async def test_charger_v2(mock_aioclient, session): """Load the charger data.""" mock_aioclient.get( TEST_URL_STATUS, @@ -202,7 +217,116 @@ def test_charger_v2(mock_aioclient): status=200, body=load_fixture("v2_json/config.json"), ) - return main.OpenEVSE(TEST_TLD) + return main.OpenEVSE(TEST_TLD, session=session) + + +@pytest_asyncio.fixture +async def charger(request, mock_aioclient, session): + """Fixture to create different charger types based on parameter. + + Use with indirect parametrization: + @pytest.mark.parametrize("charger", ["test_charger", "test_charger_v2"], indirect=True) + """ + charger_type = request.param + + if charger_type == "test_charger": + mock_aioclient.get( + TEST_URL_STATUS, + status=200, + body=load_fixture("v4_json/status.json"), + ) + mock_aioclient.get( + TEST_URL_CONFIG, + status=200, + body=load_fixture("v4_json/config.json"), + ) + mock_aioclient.get( + TEST_URL_WS, + status=200, + body=load_fixture("v4_json/status.json"), + repeat=True, + ) + return main.OpenEVSE(TEST_TLD, session=session) + + elif charger_type == "test_charger_v2": + mock_aioclient.get( + TEST_URL_STATUS, + status=200, + body=load_fixture("v2_json/status.json"), + ) + mock_aioclient.get( + TEST_URL_CONFIG, + status=200, + body=load_fixture("v2_json/config.json"), + ) + return main.OpenEVSE(TEST_TLD, session=session) + + elif charger_type == "test_charger_new": + mock_aioclient.get( + TEST_URL_STATUS, + status=200, + body=load_fixture("v4_json/status-new.json"), + ) + mock_aioclient.get( + TEST_URL_CONFIG, + status=200, + body=load_fixture("v4_json/config-new.json"), + ) + mock_aioclient.get( + TEST_URL_WS, + status=200, + body=load_fixture("v4_json/status-new.json"), + repeat=True, + ) + return main.OpenEVSE(TEST_TLD, session=session) + + elif charger_type == "test_charger_broken": + mock_aioclient.get( + TEST_URL_STATUS, + status=200, + body=load_fixture("v4_json/status-broken.json"), + ) + mock_aioclient.get( + TEST_URL_CONFIG, + status=200, + body=load_fixture("v4_json/config-broken.json"), + ) + return main.OpenEVSE(TEST_TLD, session=session) + + elif charger_type == "test_charger_dev": + mock_aioclient.get( + TEST_URL_STATUS, + status=200, + body=load_fixture("v4_json/status.json"), + ) + mock_aioclient.get( + TEST_URL_CONFIG, + status=200, + body=load_fixture("v4_json/config-dev.json"), + ) + mock_aioclient.get( + TEST_URL_WS, + status=200, + body=load_fixture("v4_json/status.json"), + repeat=True, + ) + return main.OpenEVSE(TEST_TLD, session=session) + + elif charger_type == "test_charger_broken_semver": + mock_aioclient.get( + TEST_URL_STATUS, + status=200, + body=load_fixture("v4_json/status.json"), + ) + mock_aioclient.get( + TEST_URL_CONFIG, + status=200, + body=load_fixture("v4_json/config-broken-semver.json"), + ) + return main.OpenEVSE(TEST_TLD, session=session) + + else: + raise ValueError(f"Unknown charger type: {charger_type}") @pytest.fixture diff --git a/tests/test_main.py b/tests/test_main.py index e807beb..9341e51 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -177,74 +177,12 @@ async def test_send_command_server_timeout(test_charger_auth, mock_aioclient, ca @pytest.mark.parametrize( - "fixture, expected", - [("test_charger", "sleeping"), ("test_charger_v2", "not connected")], + "charger, expected", + [("test_charger", 220), ("test_charger_v2", 220)], + indirect=["charger"], ) -async def test_get_status(fixture, expected, request): +async def test_get_ammeter_scale_factor(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.status - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", - [("test_charger", "Datanode-IoT"), ("test_charger_v2", "nsavanup_IoT")], -) -async def test_get_ssid(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.wifi_ssid - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", "7.1.3"), ("test_charger_v2", "5.0.1")] -) -async def test_get_firmware(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.openevse_firmware - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", - [("test_charger", "openevse-7b2c"), ("test_charger_v2", "openevse")], -) -async def test_get_hostname(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.hostname - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] -) -async def test_get_ammeter_offset(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - await charger.ws_disconnect() - status = charger.ammeter_offset - assert status == expected - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 220), ("test_charger_v2", 220)] -) -async def test_get_ammeter_scale_factor(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.ammeter_scale_factor assert status == expected @@ -259,11 +197,12 @@ async def test_get_ammeter_scale_factor(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 2), ("test_charger_v2", 2)] + "charger, expected", + [("test_charger", 2), ("test_charger_v2", 2)], + indirect=["charger"], ) -async def test_get_service_level(fixture, expected, request): +async def test_get_service_level(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.service_level assert status == expected @@ -271,53 +210,24 @@ async def test_get_service_level(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", - [ - ("test_charger", "4.1.2"), - ("test_charger_v2", "2.9.1"), - ("test_charger_dev", "4.1.5"), - ("test_charger_broken_semver", "master_abcd123"), - ], + "charger, expected", + [("test_charger", 240), ("test_charger_v2", 240)], + indirect=["charger"], ) -async def test_get_wifi_firmware(fixture, expected, request): +async def test_get_charging_voltage(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.wifi_firmware - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", - [("test_charger", "192.168.21.10"), ("test_charger_v2", "192.168.1.67")], -) -async def test_get_ip_address(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.ip_address - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 240), ("test_charger_v2", 240)] -) -async def test_get_charging_voltage(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.charging_voltage assert status == expected @pytest.mark.parametrize( - "fixture, expected", [("test_charger", "STA"), ("test_charger_v2", "STA")] + "charger, expected", + [("test_charger", "STA"), ("test_charger_v2", "STA")], + indirect=["charger"], ) -async def test_get_mode(fixture, expected, request): +async def test_get_mode(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.mode assert status == expected @@ -325,11 +235,12 @@ async def test_get_mode(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", False), ("test_charger_v2", False)] + "charger, expected", + [("test_charger", False), ("test_charger_v2", False)], + indirect=["charger"], ) -async def test_get_using_ethernet(fixture, expected, request): +async def test_get_using_ethernet(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.using_ethernet assert status == expected @@ -337,35 +248,12 @@ async def test_get_using_ethernet(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] -) -async def test_get_stuck_relay_trip_count(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.stuck_relay_trip_count - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] + "charger, expected", + [("test_charger", 1), ("test_charger_v2", 0)], + indirect=["charger"], ) -async def test_get_no_gnd_trip_count(fixture, expected, request): +async def test_get_gfi_trip_count(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.no_gnd_trip_count - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 1), ("test_charger_v2", 0)] -) -async def test_get_gfi_trip_count(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.gfi_trip_count assert status == expected @@ -373,11 +261,12 @@ async def test_get_gfi_trip_count(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 246), ("test_charger_v2", 8751)] + "charger, expected", + [("test_charger", 246), ("test_charger_v2", 8751)], + indirect=["charger"], ) -async def test_get_charge_time_elapsed(fixture, expected, request): +async def test_get_charge_time_elapsed(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.charge_time_elapsed assert status == expected @@ -385,11 +274,12 @@ async def test_get_charge_time_elapsed(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", -61), ("test_charger_v2", -56)] + "charger, expected", + [("test_charger", -61), ("test_charger_v2", -56)], + indirect=["charger"], ) -async def test_get_wifi_signal(fixture, expected, request): +async def test_get_wifi_signal(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.wifi_signal assert status == expected @@ -397,11 +287,12 @@ async def test_get_wifi_signal(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 32.2), ("test_charger_v2", 0)] + "charger, expected", + [("test_charger", 32.2), ("test_charger_v2", 0)], + indirect=["charger"], ) -async def test_get_charging_current(fixture, expected, request): +async def test_get_charging_current(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.charging_current assert status == expected @@ -409,11 +300,12 @@ async def test_get_charging_current(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 48), ("test_charger_v2", 25)] + "charger, expected", + [("test_charger", 48), ("test_charger_v2", 25)], + indirect=["charger"], ) -async def test_get_current_capacity(fixture, expected, request): +async def test_get_current_capacity(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.current_capacity assert status == expected @@ -421,28 +313,12 @@ async def test_get_current_capacity(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", - [ - ("test_charger", 64582), - ("test_charger_v2", 1585443), - ("test_charger_new", 20127.22817), - ], + "charger, expected", + [("test_charger", 50.3), ("test_charger_v2", 34.0)], + indirect=["charger"], ) -async def test_get_usage_total(fixture, expected, request): +async def test_get_ambient_temperature(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.usage_total - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 50.3), ("test_charger_v2", 34.0)] -) -async def test_get_ambient_temperature(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.ambient_temperature assert status == expected @@ -450,11 +326,12 @@ async def test_get_ambient_temperature(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 50.3), ("test_charger_v2", None)] + "charger, expected", + [("test_charger", 50.3), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_get_rtc_temperature(fixture, expected, request): +async def test_get_rtc_temperature(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.rtc_temperature assert status == expected @@ -462,11 +339,12 @@ async def test_get_rtc_temperature(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", None), ("test_charger_v2", None)] + "charger, expected", + [("test_charger", None), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_get_ir_temperature(fixture, expected, request): +async def test_get_ir_temperature(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.ir_temperature assert status is None @@ -474,11 +352,12 @@ async def test_get_ir_temperature(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 56.0), ("test_charger_v2", None)] + "charger, expected", + [("test_charger", 56.0), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_get_esp_temperature(fixture, expected, request): +async def test_get_esp_temperature(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.esp_temperature assert status == expected @@ -486,65 +365,12 @@ async def test_get_esp_temperature(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected_str", - [("test_charger", "2021-08-10T23:00:11Z"), ("test_charger_v2", None)], -) -async def test_get_time(fixture, expected_str, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - - result = charger.time - - if expected_str: - expected_dt = datetime(2021, 8, 10, 23, 0, 11, tzinfo=timezone.utc) - assert result == expected_dt - assert isinstance(result, datetime) - else: - assert result is None - - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "bad_value", - [ - "not-a-timestamp", - 123456789, - True, - {"some": "dict"}, - ], -) -async def test_time_parsing_errors(test_charger, bad_value): - """Test that ValueError and AttributeError are caught and return None.""" - test_charger._status["time"] = bad_value - result = test_charger.time - assert result is None - - -@pytest.mark.parametrize( - "fixture, expected", - [ - ("test_charger", 275.71), - ("test_charger_v2", 7003.41), - ("test_charger_new", 0), - ], -) -async def test_get_usage_session(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.usage_session - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", None), ("test_charger_v2", "4.0.1")] + "charger, expected", + [("test_charger", None), ("test_charger_v2", "4.0.1")], + indirect=["charger"], ) -async def test_get_protocol_version(fixture, expected, request): +async def test_get_protocol_version(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.protocol_version assert status == expected @@ -552,11 +378,12 @@ async def test_get_protocol_version(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 6), ("test_charger_v2", 6)] + "charger, expected", + [("test_charger", 6), ("test_charger_v2", 6)], + indirect=["charger"], ) -async def test_get_min_amps(fixture, expected, request): +async def test_get_min_amps(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.min_amps assert status == expected @@ -564,11 +391,12 @@ async def test_get_min_amps(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 48), ("test_charger_v2", 48)] + "charger, expected", + [("test_charger", 48), ("test_charger_v2", 48)], + indirect=["charger"], ) -async def test_get_max_amps(fixture, expected, request): +async def test_get_max_amps(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.max_amps assert status == expected @@ -576,58 +404,12 @@ async def test_get_max_amps(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] -) -async def test_get_ota_update(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.ota_update - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize("fixture, expected", [("test_charger", 1)]) -async def test_get_vehicle(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.vehicle - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", - [("test_charger", "sleeping"), ("test_charger_v2", "not connected")], -) -async def test_get_state(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.state - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] -) -async def test_get_tempt(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.temp_check_enabled - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 1)] + "charger, expected", + [("test_charger", 0), ("test_charger_v2", 1)], + indirect=["charger"], ) -async def test_get_diodet(fixture, expected, request): +async def test_get_diodet(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.diode_check_enabled assert status == expected @@ -635,59 +417,12 @@ async def test_get_diodet(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] + "charger, expected", + [("test_charger", None), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_get_ventt(fixture, expected, request): +async def test_get_available_current(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.vent_required_enabled - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] -) -async def test_get_groundt(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.ground_check_enabled - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 0), ("test_charger_v2", 0)] -) -async def test_get_relayt(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.stuck_relay_check_enabled - assert status == expected - await charger.ws_disconnect() - - -@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 - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", None), ("test_charger_v2", None)] -) -async def test_get_available_current(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.available_current assert status == expected @@ -695,11 +430,12 @@ async def test_get_available_current(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", None), ("test_charger_v2", None)] + "charger, expected", + [("test_charger", None), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_get_smoothed_available_current(fixture, expected, request): +async def test_get_smoothed_available_current(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.smoothed_available_current assert status == expected @@ -707,24 +443,12 @@ async def test_get_smoothed_available_current(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", - [("test_charger", True), ("test_charger_v2", False), ("test_charger_new", False)], + "charger, expected", + [("test_charger", False), ("test_charger_v2", False)], + indirect=["charger"], ) -async def test_get_divert_active(fixture, expected, request): +async def test_get_manual_override(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.divert_active - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", False), ("test_charger_v2", False)] -) -async def test_get_manual_override(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.manual_override assert status == expected @@ -863,11 +587,12 @@ async def test_toggle_override_v2_err(test_charger_v2, mock_aioclient, caplog): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", "1234567890AB"), ("test_charger_v2", None)] + "charger, expected", + [("test_charger", "1234567890AB"), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_wifi_serial(fixture, expected, request): +async def test_wifi_serial(charger, expected): """Test wifi_serial reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.wifi_serial assert status == expected @@ -981,12 +706,12 @@ async def test_set_current_v2( @pytest.mark.parametrize( - "fixture, expected", - [("test_charger", 7728), ("test_charger_v2", 0), ("test_charger_broken", None)], + "charger, expected", + [("test_charger", 7728.0), ("test_charger_v2", 0)], + indirect=["charger"], ) -async def test_get_charging_power(fixture, expected, request): +async def test_get_charging_power(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.charging_power assert status == expected @@ -1212,11 +937,12 @@ async def test_evse_restart( @pytest.mark.parametrize( - "fixture, expected", [("test_charger", True), ("test_charger_v2", None)] + "charger, expected", + [("test_charger", True), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_shaper_active(fixture, expected, request): +async def test_shaper_active(charger, expected): """Test shaper_active reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.shaper_active assert status == expected @@ -1224,11 +950,12 @@ async def test_shaper_active(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 2299), ("test_charger_v2", None)] + "charger, expected", + [("test_charger", 2299), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_shaper_live_power(fixture, expected, request): +async def test_shaper_live_power(charger, expected): """Test shaper_live_power reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.shaper_live_power assert status == expected @@ -1236,24 +963,12 @@ async def test_shaper_live_power(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", - [("test_charger", 21), ("test_charger_v2", None), ("test_charger_broken", 48)], + "charger, expected", + [("test_charger", 4000), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_shaper_current_power(fixture, expected, request): - """Test shaper_available_current reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.shaper_available_current - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger", 4000), ("test_charger_v2", None)] -) -async def test_shaper_max_power(fixture, expected, request): +async def test_shaper_max_power(charger, expected): """Test shaper_max_power reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.shaper_max_power assert status == expected @@ -1261,11 +976,12 @@ async def test_shaper_max_power(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 75), ("test_charger_v2", None)] + "charger, expected", + [("test_charger", 75), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_vehicle_soc(fixture, expected, request): +async def test_vehicle_soc(charger, expected): """Test vehicle_soc reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.vehicle_soc assert status == expected @@ -1273,11 +989,12 @@ async def test_vehicle_soc(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 468), ("test_charger_v2", None)] + "charger, expected", + [("test_charger", 468), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_vehicle_range(fixture, expected, request): +async def test_vehicle_range(charger, expected): """Test vehicle_range reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.vehicle_range assert status == expected @@ -1285,12 +1002,13 @@ async def test_vehicle_range(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected_seconds", [("test_charger", 18000), ("test_charger_v2", None)] + "charger, expected_seconds", + [("test_charger", 18000), ("test_charger_v2", None)], + indirect=["charger"], ) @freeze_time("2026-01-09 12:00:00+00:00") -async def test_vehicle_eta(fixture, expected_seconds, request): +async def test_vehicle_eta(charger, expected_seconds): """Test vehicle_eta reply.""" - charger = request.getfixturevalue(fixture) await charger.update() result = charger.vehicle_eta @@ -1308,11 +1026,12 @@ async def test_vehicle_eta(fixture, expected_seconds, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", 48), ("test_charger_v2", 25)] + "charger, expected", + [("test_charger", 48), ("test_charger_v2", 25)], + indirect=["charger"], ) -async def test_max_current_soft(fixture, expected, request): +async def test_max_current_soft(charger, expected): """Test max_current_soft reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.max_current_soft assert status == expected @@ -1529,11 +1248,12 @@ async def test_set_charge_mode(test_charger, mock_aioclient, caplog): @pytest.mark.parametrize( - "fixture, expected", [("test_charger", "fast"), ("test_charger_v2", "fast")] + "charger, expected", + [("test_charger", "fast"), ("test_charger_v2", "fast")], + indirect=["charger"], ) -async def test_charge_mode(fixture, expected, request): +async def test_charge_mode(charger, expected): """Test vehicle_range reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.charge_mode assert status == expected @@ -1594,380 +1314,12 @@ async def test_set_service_level(test_charger, mock_aioclient, caplog): @pytest.mark.parametrize( - "fixture, expected", - [("test_charger", None), ("test_charger_v2", None), ("test_charger_new", False)], -) -async def test_get_has_limit(fixture, expected, request): - """Test has_limit reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.has_limit - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", - [("test_charger", None), ("test_charger_v2", None), ("test_charger_new", 0)], -) -async def test_get_total_day(fixture, expected, request): - """Test total_day reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.total_day - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", - [ - ("test_charger", None), - ("test_charger_v2", None), - ("test_charger_new", 1.567628635), - ], -) -async def test_get_total_week(fixture, expected, request): - """Test total_week reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.total_week - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", - [ - ("test_charger", None), - ("test_charger_v2", None), - ("test_charger_new", 37.21857071), - ], -) -async def test_get_total_month(fixture, expected, request): - """Test total_month reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.total_month - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", - [ - ("test_charger", None), - ("test_charger_v2", None), - ("test_charger_new", 2155.219982), - ], -) -async def test_get_total_year(fixture, expected, request): - """Test total_year reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.total_year - assert status == expected - await charger.ws_disconnect() - - -async def test_websocket_functions(test_charger, mock_aioclient, caplog): - """Test v4 Status reply.""" - mock_aioclient.get( - TEST_URL_WS, - status=200, - body=load_fixture("websocket.json"), - ) - await test_charger.update() - test_charger.ws_start() - await test_charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", - [("test_charger", 254), ("test_charger_v2", 1)], -) -async def test_get_state_raw(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.state_raw - assert status == expected - await charger.ws_disconnect() - - -@pytest.mark.parametrize( - "fixture, expected", - [ - ("test_charger", True), - ("test_charger_v2", False), - ("test_charger_broken", False), - ], -) -async def test_get_mqtt_connected(fixture, expected, request): - """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) - await charger.update() - status = charger.mqtt_connected - assert status == expected - await charger.ws_disconnect() - - -async def test_self_production(test_charger, test_charger_v2, mock_aioclient, caplog): - """Test self_production function.""" - await test_charger.update() - mock_aioclient.post( - TEST_URL_STATUS, - status=200, - body='{"grid_ie": 3000, "solar": 1000}', - repeat=True, - ) - with caplog.at_level(logging.DEBUG): - await test_charger.self_production(-3000, 1000, True, 210) - assert ( - "Posting self-production: {'grid_ie': 3000, 'voltage': 210}" in caplog.text - ) - assert ( - "Self-production response: {'grid_ie': 3000, 'solar': 1000}" in caplog.text - ) - - await test_charger.self_production(None, 1000) - assert "Posting self-production: {'solar': 1000}" in caplog.text - - await test_charger.self_production(None, None) - assert "No sensor data to send to device." in caplog.text - - with pytest.raises(UnsupportedFeature): - with caplog.at_level(logging.DEBUG): - await test_charger_v2.self_production(-3000, 1000) - assert "Feature not supported for older firmware." in caplog.text - await test_charger.ws_disconnect() - - -async def test_soc(test_charger, test_charger_v2, mock_aioclient, caplog): - """Test soc function.""" - await test_charger.update() - mock_aioclient.post( - TEST_URL_STATUS, - status=200, - body='{"battery_level": 85, "battery_range": 230, "time_to_full_charge": 1590}', - repeat=True, - ) - with caplog.at_level(logging.DEBUG): - await test_charger.soc(85, 230, 1590) - assert ( - "Posting SOC data: {'battery_level': 85, 'battery_range': 230, 'time_to_full_charge': 1590}" - in caplog.text - ) - assert ( - "SOC response: {'battery_level': 85, 'battery_range': 230, 'time_to_full_charge': 1590}" - in caplog.text - ) - - await test_charger.soc(voltage=220) - assert "Posting SOC data: {'voltage': 220}" in caplog.text - - await test_charger.soc(None) - assert "No SOC data to send to device." in caplog.text - await test_charger.ws_disconnect() - - with pytest.raises(UnsupportedFeature): - with caplog.at_level(logging.DEBUG): - await test_charger_v2.soc(50, 90, 3100) - assert "Feature not supported for older firmware." in caplog.text - await test_charger_v2.ws_disconnect() - - -async def test_set_limit( - test_charger_modified_ver, test_charger, mock_aioclient, caplog -): - """Test set limit.""" - await test_charger_modified_ver.update() - mock_aioclient.get( - TEST_URL_LIMIT, - status=200, - body='{"type": "energy", "value": 10}', - repeat=True, - ) - mock_aioclient.post( - TEST_URL_LIMIT, - status=200, - body='{"msg": "OK"}', - repeat=True, - ) - with caplog.at_level(logging.DEBUG): - await test_charger_modified_ver.set_limit("energy", 15, True) - assert ( - "Limit data: {'type': 'energy', 'value': 15, 'release': True}" - in caplog.text - ) - assert "Setting limit config on http://openevse.test.tld/limit" in caplog.text - - with pytest.raises(InvalidType): - await test_charger_modified_ver.set_limit("invalid", 15) - - with pytest.raises(UnsupportedFeature): - with caplog.at_level(logging.DEBUG): - await test_charger.set_limit("energy", 15) - assert "Feature not supported for older firmware." in caplog.text - - -async def test_get_limit( - test_charger_modified_ver, test_charger, mock_aioclient, caplog -): - """Test get limit.""" - await test_charger_modified_ver.update() - mock_aioclient.get( - TEST_URL_LIMIT, - status=200, - body='{"type": "energy", "value": 10}', - ) - with caplog.at_level(logging.DEBUG): - response = await test_charger_modified_ver.get_limit() - assert response == {"type": "energy", "value": 10} - assert "Getting limit config on http://openevse.test.tld/limit" in caplog.text - - mock_aioclient.get( - TEST_URL_LIMIT, - status=404, - body='{"msg": "No limit"}', - ) - with caplog.at_level(logging.DEBUG): - response = await test_charger_modified_ver.get_limit() - assert response == {"msg": "No limit"} - - with pytest.raises(UnsupportedFeature): - with caplog.at_level(logging.DEBUG): - await test_charger.get_limit() - assert "Feature not supported for older firmware." in caplog.text - - -async def test_clear_limit( - test_charger_modified_ver, test_charger, mock_aioclient, caplog -): - """Test clear limit.""" - await test_charger_modified_ver.update() - mock_aioclient.delete( - TEST_URL_LIMIT, - status=200, - body='{"msg": "Deleted"}', - ) - with caplog.at_level(logging.DEBUG): - response = await test_charger_modified_ver.clear_limit() - assert response == {"msg": "Deleted"} - assert "Clearing limit config on http://openevse.test.tld/limit" in caplog.text - - mock_aioclient.delete( - TEST_URL_LIMIT, - status=404, - body='{"msg": "No limit to clear"}', - ) - with caplog.at_level(logging.DEBUG): - response = await test_charger_modified_ver.clear_limit() - assert response == {"msg": "No limit to clear"} - - with pytest.raises(UnsupportedFeature): - with caplog.at_level(logging.DEBUG): - await test_charger.clear_limit() - assert "Feature not supported for older firmware." in caplog.text - - -async def test_voltage(test_charger, test_charger_v2, mock_aioclient, caplog): - """Test voltage function.""" - await test_charger.update() - mock_aioclient.post( - TEST_URL_STATUS, - status=200, - body='{"voltage": 210}', - repeat=True, - ) - with caplog.at_level(logging.DEBUG): - await test_charger.grid_voltage(210) - assert "Posting voltage: {'voltage': 210}" in caplog.text - assert "Voltage posting response: {'voltage': 210}" in caplog.text - - await test_charger.grid_voltage(None) - assert "No sensor data to send to device." in caplog.text - - with pytest.raises(UnsupportedFeature): - with caplog.at_level(logging.DEBUG): - await test_charger_v2.grid_voltage(210) - assert "Feature not supported for older firmware." in caplog.text - - -async def test_list_claims(test_charger, test_charger_v2, mock_aioclient, caplog): - """Test list_claims function.""" - await test_charger.update() - mock_aioclient.get( - TEST_URL_CLAIMS, - status=200, - body='[{"client":65540,"priority":10,"state":"disabled","auto_release":false}]', - repeat=True, - ) - with caplog.at_level(logging.DEBUG): - await test_charger.list_claims() - assert f"Getting claims on {TEST_URL_CLAIMS}" in caplog.text - - with pytest.raises(UnsupportedFeature): - with caplog.at_level(logging.DEBUG): - await test_charger_v2.list_claims() - assert "Feature not supported for older firmware." in caplog.text - - -async def test_release_claim(test_charger, test_charger_v2, mock_aioclient, caplog): - """Test release_claim function.""" - await test_charger.update() - mock_aioclient.delete( - f"{TEST_URL_CLAIMS}/20", - status=200, - body='[{"msg":"done"}]', - repeat=True, - ) - with caplog.at_level(logging.DEBUG): - await test_charger.release_claim() - assert f"Releasing claim on {TEST_URL_CLAIMS}/20" in caplog.text - - with pytest.raises(UnsupportedFeature): - with caplog.at_level(logging.DEBUG): - await test_charger_v2.release_claim() - assert "Feature not supported for older firmware." in caplog.text - - -async def test_make_claim(test_charger, test_charger_v2, mock_aioclient, caplog): - """Test make_claim function.""" - await test_charger.update() - mock_aioclient.post( - f"{TEST_URL_CLAIMS}/20", - status=200, - body='[{"msg":"done"}]', - repeat=True, - ) - with caplog.at_level(logging.DEBUG): - await test_charger.make_claim( - state="disabled", charge_current=20, max_current=20 - ) - assert ( - "Claim data: {'auto_release': True, 'state': 'disabled', 'charge_current': 20, 'max_current': 20}" - in caplog.text - ) - assert f"Setting up claim on {TEST_URL_CLAIMS}/20" in caplog.text - - with pytest.raises(ValueError): - with caplog.at_level(logging.DEBUG): - await test_charger.make_claim("invalid") - assert "Invalid claim state: invalid" in caplog.text - - with pytest.raises(UnsupportedFeature): - with caplog.at_level(logging.DEBUG): - await test_charger_v2.make_claim() - assert "Feature not supported for older firmware." in caplog.text - - -@pytest.mark.parametrize( - "fixture, expected", [("test_charger_new", 48), ("test_charger_v2", None)] + "charger, expected", + [("test_charger_new", 48), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_max_current(fixture, expected, request): +async def test_max_current(charger, expected): """Test max_current reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.max_current assert status == expected @@ -1975,11 +1327,12 @@ async def test_max_current(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger_new", 0), ("test_charger_v2", 0)] + "charger, expected", + [("test_charger_new", 0), ("test_charger_v2", 0)], + indirect=["charger"], ) -async def test_emoncms_connected(fixture, expected, request): +async def test_emoncms_connected(charger, expected): """Test emoncms_connected reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.emoncms_connected assert status == expected @@ -1987,11 +1340,12 @@ async def test_emoncms_connected(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger_new", 0), ("test_charger_v2", None)] + "charger, expected", + [("test_charger_new", 0), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_ocpp_connected(fixture, expected, request): +async def test_ocpp_connected(charger, expected): """Test ocpp_connected reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.ocpp_connected assert status == expected @@ -1999,11 +1353,12 @@ async def test_ocpp_connected(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger_new", 1208725), ("test_charger_v2", None)] + "charger, expected", + [("test_charger_new", 1208725), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_uptime(fixture, expected, request): +async def test_uptime(charger, expected): """Test uptime reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.uptime assert status == expected @@ -2011,11 +1366,12 @@ async def test_uptime(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", [("test_charger_new", 167436), ("test_charger_v2", None)] + "charger, expected", + [("test_charger_new", 167436), ("test_charger_v2", None)], + indirect=["charger"], ) -async def test_freeram(fixture, expected, request): +async def test_freeram(charger, expected): """Test freeram reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.freeram assert status == expected @@ -2023,15 +1379,15 @@ async def test_freeram(fixture, expected, request): @pytest.mark.parametrize( - "fixture, expected", + "charger, expected", [ ("test_charger_new", {"gfcicount": 1, "nogndcount": 0, "stuckcount": 0}), ("test_charger_v2", {"gfcicount": 0, "nogndcount": 0, "stuckcount": 0}), ], + indirect=["charger"], ) -async def test_checks_count(fixture, expected, request): +async def test_checks_count(charger, expected): """Test checks_count reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.checks_count assert status == expected @@ -2156,17 +1512,17 @@ async def test_async_override_state( @pytest.mark.parametrize( - "fixture, expected", + "charger, expected", [ ("test_charger", False), ("test_charger_v2", False), ("test_charger_broken", False), ("test_charger_new", True), ], + indirect=["charger"], ) -async def test_get_shaper_updated(fixture, expected, request): +async def test_get_shaper_updated(charger, expected): """Test v4 Status reply.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.shaper_updated assert status == expected @@ -2186,17 +1542,17 @@ async def test_get_status_error(test_charger_timeout, caplog): @pytest.mark.parametrize( - "fixture, expected", + "charger, expected", [ ("test_charger", "eco"), ("test_charger_v2", "fast"), ("test_charger_broken", "eco"), ("test_charger_new", "fast"), ], + indirect=["charger"], ) -async def test_divertmode(fixture, expected, request): +async def test_divertmode(charger, expected): """Test divertmode property.""" - charger = request.getfixturevalue(fixture) await charger.update() status = charger.divertmode assert status == expected @@ -2311,17 +1667,17 @@ async def test_send_command_empty_fallback(): @pytest.mark.parametrize( - "fixture, expected", + "charger, expected", [ ("test_charger", UnsupportedFeature), ("test_charger_v2", UnsupportedFeature), ("test_charger_broken", UnsupportedFeature), ("test_charger_new", 4500), ], + indirect=["charger"], ) -async def test_power(fixture, expected, request): +async def test_power(charger, expected): """Test current_power property.""" - charger = request.getfixturevalue(fixture) await charger.update() # If we expect an exception (UnsupportedFeature), we must use pytest.raises diff --git a/tests/test_main_edge_cases.py b/tests/test_main_edge_cases.py index dfdb730..fbf8767 100644 --- a/tests/test_main_edge_cases.py +++ b/tests/test_main_edge_cases.py @@ -3,6 +3,7 @@ import logging from unittest.mock import AsyncMock, MagicMock, patch +import aiohttp import pytest from openevsehttp.__main__ import OpenEVSE @@ -61,3 +62,59 @@ async def test_process_request_http_warnings(charger, caplog): await charger.process_request("http://url", method="get") # Verify the 404 response body was logged as a warning assert "{'msg': 'Not Found'}" in caplog.text + + +async def test_session_property_creates_session_lazily(charger): + """Test that session property creates an aiohttp.ClientSession when none provided.""" + assert charger._session is None + session = charger.session + assert session is not None + assert isinstance(session, aiohttp.ClientSession) + # Second access should return the same session + assert charger.session is session + await session.close() + + +async def test_session_property_returns_provided_session(): + """Test that session property returns a pre-provided session.""" + mock_session = MagicMock(spec=aiohttp.ClientSession) + charger = OpenEVSE(SERVER_URL, session=mock_session) + assert charger._session is mock_session + assert charger.session is mock_session + + +async def test_init_with_custom_session(): + """Test OpenEVSE constructor stores the provided session.""" + mock_session = MagicMock(spec=aiohttp.ClientSession) + charger = OpenEVSE(SERVER_URL, session=mock_session) + assert charger._session is mock_session + assert charger.url == f"http://{SERVER_URL}/" + + +async def test_update_passes_session_to_websocket(mock_aioclient): + """Test that update() passes the session to OpenEVSEWebsocket.""" + from tests.common import load_fixture + + mock_aioclient.get( + "http://openevse.test.tld/status", + status=200, + body=load_fixture("v4_json/status.json"), + ) + mock_aioclient.get( + "http://openevse.test.tld/config", + status=200, + body=load_fixture("v4_json/config.json"), + ) + + charger = OpenEVSE(SERVER_URL) + + with patch("openevsehttp.__main__.OpenEVSEWebsocket") as mock_ws_cls: + await charger.update() + mock_ws_cls.assert_called_once_with( + charger.url, + charger._update_status, + charger._user, + charger._pwd, + session=charger.session, + ) + await charger.session.close() diff --git a/tests/test_websocket.py b/tests/test_websocket.py index d97d7aa..a84276f 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -34,8 +34,8 @@ async def ws_client(mock_callback): client = OpenEVSEWebsocket(SERVER_URL, mock_callback) yield client # Clean up session to prevent unclosed session warnings - if client.session and not client.session.closed: - await client.session.close() + if client._session and not client._session.closed: + await client._session.close() def test_get_uri(): @@ -157,8 +157,8 @@ async def ws_client_auth(): f"http://{SERVER_URL}", callback, user="test", password="pw" ) yield client - if client.session and not client.session.closed: - await client.session.close() + if client._session and not client._session.closed: + await client._session.close() @pytest.mark.asyncio @@ -304,3 +304,64 @@ async def test_state_setter_threadsafe_fallback(ws_client): assert args[0] is mock_create_task assert ws_client._error_reason is None + + +@pytest.mark.asyncio +async def test_session_property_creates_session_lazily(mock_callback): + """Test that session property creates an aiohttp.ClientSession when none provided.""" + client = OpenEVSEWebsocket(SERVER_URL, mock_callback) + assert client._session is None + session = client.session + assert session is not None + assert isinstance(session, aiohttp.ClientSession) + # Second access should return the same instance + assert client.session is session + await session.close() + + +@pytest.mark.asyncio +async def test_session_property_returns_provided_session(mock_callback): + """Test that session property returns a pre-provided session.""" + mock_session = MagicMock(spec=aiohttp.ClientSession) + client = OpenEVSEWebsocket(SERVER_URL, mock_callback, session=mock_session) + assert client._session is mock_session + assert client.session is mock_session + + +@pytest.mark.asyncio +async def test_init_stores_provided_session(mock_callback): + """Test that __init__ stores the provided session without creating a new one.""" + mock_session = MagicMock(spec=aiohttp.ClientSession) + client = OpenEVSEWebsocket(SERVER_URL, mock_callback, session=mock_session) + assert client._session is mock_session + + +@pytest.mark.asyncio +async def test_init_no_session_defers_creation(mock_callback): + """Test that __init__ without session sets _session to None.""" + client = OpenEVSEWebsocket(SERVER_URL, mock_callback) + assert client._session is None + + +@pytest.mark.asyncio +async def test_close_closes_client(mock_callback): + """Test that close() closes _client instead of session.""" + client = OpenEVSEWebsocket(SERVER_URL, mock_callback) + mock_ws_client = AsyncMock() + client._client = mock_ws_client + + await client.close() + + assert client.state == STATE_STOPPED + mock_ws_client.close.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_close_without_client(mock_callback): + """Test that close() works when _client is None.""" + client = OpenEVSEWebsocket(SERVER_URL, mock_callback) + client._client = None + + await client.close() + + assert client.state == STATE_STOPPED