Skip to content

Commit d5c9953

Browse files
authored
fix: make websocket listener not automatic (#13)
* add set_charge_mode function Utilizes the HTTP API to set the charge_mode configuration. * refactor: move all aiohttp handling into separate function * fix: make websocket listener not automatic (#12) * fix tests
1 parent adbce89 commit d5c9953

File tree

6 files changed

+236
-284
lines changed

6 files changed

+236
-284
lines changed

openevsehttp/__init__.py

Lines changed: 95 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
import aiohttp # type: ignore
1010

1111
from .const import MAX_AMPS, MIN_AMPS
12-
from .exceptions import AuthenticationError, ParseJSONError
12+
from .exceptions import (
13+
AlreadyListening,
14+
AuthenticationError,
15+
MissingMethod,
16+
ParseJSONError,
17+
UnknownError,
18+
)
1319

1420
_LOGGER = logging.getLogger(__name__)
1521

@@ -116,7 +122,7 @@ async def running(self):
116122
break
117123

118124
except aiohttp.ClientResponseError as error:
119-
if error.code == 401:
125+
if error.status == 401:
120126
_LOGGER.error("Credentials rejected: %s", error)
121127
self._error_reason = ERROR_AUTH_FAILURE
122128
else:
@@ -174,70 +180,82 @@ def __init__(self, host: str, user: str = None, pwd: str = None) -> None:
174180
self.callback: Optional[Callable] = None
175181
self._loop = None
176182

177-
async def send_command(self, command: str) -> tuple | None:
178-
"""Send a RAPI command to the charger and parses the response."""
183+
async def process_request(
184+
self, url: str, method: str = None, data: Any = None
185+
) -> Any:
186+
"""Return result of processed HTTP request."""
179187
auth = None
180-
url = f"{self.url}r"
181-
data = {"json": 1, "rapi": command}
188+
if method is None:
189+
raise MissingMethod
182190

183191
if self._user and self._pwd:
184192
auth = aiohttp.BasicAuth(self._user, self._pwd)
185193

186-
_LOGGER.debug("Posting data: %s to %s", command, url)
187194
async with aiohttp.ClientSession() as session:
188-
async with session.post(url, data=data, auth=auth) as resp:
195+
http_method = getattr(session, method)
196+
async with http_method(url, data=data, auth=auth) as resp:
197+
try:
198+
message = await resp.json()
199+
except TimeoutError:
200+
_LOGGER.error("%s: %s", ERROR_TIMEOUT, url)
201+
189202
if resp.status == 400:
190-
_LOGGER.debug("JSON error: %s", await resp.text())
203+
_LOGGER.error("%s", message["msg"])
191204
raise ParseJSONError
192205
if resp.status == 401:
193-
_LOGGER.debug("Authentication error: %s", await resp.text())
206+
error = await resp.text()
207+
_LOGGER.error("Authentication error: %s", error)
194208
raise AuthenticationError
209+
if resp.status == 404:
210+
_LOGGER.error("%s", message["msg"])
211+
raise UnknownError
212+
if resp.status == 405:
213+
_LOGGER.error("%s", message["msg"])
214+
elif resp.status == 500:
215+
_LOGGER.error("%s", message["msg"])
195216

196-
value = await resp.json()
217+
return message
197218

198-
if "ret" not in value:
199-
return False, ""
200-
return value["cmd"], value["ret"]
219+
async def send_command(self, command: str) -> tuple | None:
220+
"""Send a RAPI command to the charger and parses the response."""
221+
url = f"{self.url}r"
222+
data = {"json": 1, "rapi": command}
223+
224+
_LOGGER.debug("Posting data: %s to %s", command, url)
225+
value = await self.process_request(url=url, method="post", data=data)
226+
if "ret" not in value:
227+
return False, ""
228+
return value["cmd"], value["ret"]
201229

202230
async def update(self) -> None:
203231
"""Update the values."""
204-
auth = None
205232
urls = [f"{self.url}config"]
206233

207-
if self._user and self._pwd:
208-
auth = aiohttp.BasicAuth(self._user, self._pwd)
209-
210234
if not self._ws_listening:
211235
urls = [f"{self.url}status", f"{self.url}config"]
212236

213-
async with aiohttp.ClientSession() as session:
214-
for url in urls:
215-
_LOGGER.debug("Updating data from %s", url)
216-
async with session.get(url, auth=auth) as resp:
217-
if resp.status == 401:
218-
_LOGGER.debug("Authentication error: %s", resp.text())
219-
raise AuthenticationError
220-
221-
if "/status" in url:
222-
try:
223-
self._status = await resp.json()
224-
_LOGGER.debug("Status update: %s", self._status)
225-
except TimeoutError:
226-
_LOGGER.error("%s status.", ERROR_TIMEOUT)
227-
else:
228-
try:
229-
self._config = await resp.json()
230-
_LOGGER.debug("Config update: %s", self._config)
231-
except TimeoutError:
232-
_LOGGER.error("%s config.", ERROR_TIMEOUT)
237+
for url in urls:
238+
_LOGGER.debug("Updating data from %s", url)
239+
response = await self.process_request(url, method="get")
240+
if "/status" in url:
241+
self._status = response
242+
_LOGGER.debug("Status update: %s", self._status)
243+
244+
else:
245+
self._config = response
246+
_LOGGER.debug("Config update: %s", self._config)
233247

234248
if not self.websocket:
235249
# Start Websocket listening
236250
self.websocket = OpenEVSEWebsocket(
237251
self.url, self._update_status, self._user, self._pwd
238252
)
239-
if not self._ws_listening:
240-
self._start_listening()
253+
254+
def ws_start(self):
255+
"""Start the websocket listener."""
256+
if self._ws_listening:
257+
raise AlreadyListening
258+
self._start_listening()
241259

242260
def _start_listening(self):
243261
"""Start the websocket listener."""
@@ -283,7 +301,7 @@ def _update_status(self, msgtype, data, error):
283301
self._status.update(data)
284302

285303
if self.callback is not None:
286-
self.callback()
304+
self.callback() # pylint: disable=not-callable
287305

288306
def ws_disconnect(self) -> None:
289307
"""Disconnect the websocket listener."""
@@ -297,28 +315,39 @@ def ws_state(self) -> Any:
297315
assert self.websocket
298316
return self.websocket.state
299317

318+
async def get_schedule(self) -> list:
319+
"""Return the current schedule."""
320+
url = f"{self.url}schedule"
321+
322+
_LOGGER.debug("Getting current schedule from %s", url)
323+
response = await self.process_request(url=url, method="post")
324+
return response
325+
326+
async def set_charge_mode(self, mode: str = "fast") -> None:
327+
"""Set the charge mode."""
328+
url = f"{self.url}config"
329+
330+
if mode != "fast" or mode != "eco":
331+
_LOGGER.error("Invalid value for charge_mode: %s", mode)
332+
raise ValueError
333+
334+
data = {"charge_mode": mode}
335+
336+
_LOGGER.debug("Setting charge mode to %s", mode)
337+
response = await self.process_request(
338+
url=url, method="post", data=data
339+
) # noqa: E501
340+
if response["msg"] != "done":
341+
_LOGGER.error("Problem issuing command: %s", response["msg"])
342+
raise UnknownError
343+
300344
async def get_override(self) -> None:
301345
"""Get the manual override status."""
302346
url = f"{self.url}override"
303347

304-
if self._user and self._pwd:
305-
auth = aiohttp.BasicAuth(self._user, self._pwd)
306-
307348
_LOGGER.debug("Geting data from %s", url)
308-
async with aiohttp.ClientSession() as session:
309-
async with session.get(url, auth=auth) as resp:
310-
if resp.status == 400:
311-
_LOGGER.debug("JSON error: %s", await resp.text())
312-
raise ParseJSONError
313-
if resp.status == 401:
314-
_LOGGER.debug("Authentication error: %s", await resp.text())
315-
raise AuthenticationError
316-
if resp.status == 404:
317-
error = await resp.json()
318-
_LOGGER.error("Error getting override status: %s", error["msg"])
319-
320-
value = await resp.json()
321-
return value
349+
response = await self.process_request(url=url, method="get")
350+
return response
322351

323352
async def set_override(
324353
self,
@@ -332,9 +361,6 @@ async def set_override(
332361
"""Set the manual override status."""
333362
url = f"{self.url}override"
334363

335-
if self._user and self._pwd:
336-
auth = aiohttp.BasicAuth(self._user, self._pwd)
337-
338364
if state not in ["active", "disabled"]:
339365
raise ValueError
340366

@@ -348,56 +374,26 @@ async def set_override(
348374
}
349375

350376
_LOGGER.debug("Setting override config on %s", url)
351-
async with aiohttp.ClientSession() as session:
352-
async with session.post(url, data=data, auth=auth) as resp:
353-
if resp.status == 400:
354-
_LOGGER.debug("JSON error: %s", await resp.text())
355-
raise ParseJSONError
356-
if resp.status == 401:
357-
_LOGGER.debug("Authentication error: %s", await resp.text())
358-
raise AuthenticationError
359-
360-
value = await resp.json()
361-
_LOGGER.debug("Override set response: %s", value["msg"])
362-
return value
377+
response = await self.process_request(
378+
url=url, method="post", data=data
379+
) # noqa: E501
380+
return response
363381

364382
async def toggle_override(self) -> None:
365383
"""Toggle the manual override status."""
366384
url = f"{self.url}override"
367385

368-
if self._user and self._pwd:
369-
auth = aiohttp.BasicAuth(self._user, self._pwd)
370-
371386
_LOGGER.debug("Toggling manual override %s", url)
372-
async with aiohttp.ClientSession() as session:
373-
async with session.patch(url, auth=auth) as resp:
374-
if resp.status == 400:
375-
_LOGGER.debug("JSON error: %s", await resp.text())
376-
raise ParseJSONError
377-
if resp.status == 401:
378-
_LOGGER.debug("Authentication error: %s", await resp.text())
379-
raise AuthenticationError
380-
381-
_LOGGER.debug("Toggle response: %s", resp.status)
387+
response = await self.process_request(url=url, method="patch")
388+
_LOGGER.debug("Toggle response: %s", response["msg"])
382389

383390
async def clear_override(self) -> None:
384391
"""Clear the manual override status."""
385392
url = f"{self.url}overrride"
386393

387-
if self._user and self._pwd:
388-
auth = aiohttp.BasicAuth(self._user, self._pwd)
389-
390394
_LOGGER.debug("Clearing manual overrride %s", url)
391-
async with aiohttp.ClientSession() as session:
392-
async with session.delete(url, auth=auth) as resp:
393-
if resp.status == 400:
394-
_LOGGER.debug("JSON error: %s", await resp.text())
395-
raise ParseJSONError
396-
if resp.status == 401:
397-
_LOGGER.debug("Authentication error: %s", await resp.text())
398-
raise AuthenticationError
399-
400-
_LOGGER.debug("Toggle response: %s", resp.status)
395+
response = await self.process_request(url=url, method="delete")
396+
_LOGGER.debug("Toggle response: %s", response["msg"])
401397

402398
@property
403399
def hostname(self) -> str:

openevsehttp/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@ class AuthenticationError(Exception):
77

88
class ParseJSONError(Exception):
99
"""Exception for JSON parsing errors."""
10+
11+
12+
class UnknownError(Exception):
13+
"""Exception for Unknown errors."""
14+
15+
16+
class MissingMethod(Exception):
17+
"""Exception for missing method variable."""
18+
19+
20+
class AlreadyListening(Exception):
21+
"""Exception for already listening websocket."""

requirements_test.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
pytest==6.2.4
22
pytest-cov==2.12.1
33
pytest-timeout==1.4.2
4-
pytest-aiohttp
4+
pytest-asyncio
55
requests_mock
66
aiohttp
7-
aioresponses
7+
aioresponses

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
PROJECT_DIR = Path(__file__).parent.resolve()
77
README_FILE = PROJECT_DIR / "README.md"
8-
VERSION = "0.1.10"
8+
VERSION = "0.1.11"
99

1010

1111
setup(

0 commit comments

Comments
 (0)