-
Notifications
You must be signed in to change notification settings - Fork 6
Implement minimums for WebSocket API #426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -5,20 +5,13 @@ | |||||||||||||||||||
import asyncio | ||||||||||||||||||||
import logging | ||||||||||||||||||||
import ssl | ||||||||||||||||||||
from collections.abc import Callable, Coroutine | ||||||||||||||||||||
from collections.abc import Coroutine | ||||||||||||||||||||
from http import HTTPStatus | ||||||||||||||||||||
from typing import Any, TypeVar | ||||||||||||||||||||
from typing import Any, Callable, TypeVar | ||||||||||||||||||||
|
||||||||||||||||||||
import aiohttp | ||||||||||||||||||||
import async_timeout | ||||||||||||||||||||
import backoff | ||||||||||||||||||||
from aiohttp.client import ( | ||||||||||||||||||||
ClientError, | ||||||||||||||||||||
ClientResponseError, | ||||||||||||||||||||
ClientSession, | ||||||||||||||||||||
ClientTimeout, | ||||||||||||||||||||
TCPConnector, | ||||||||||||||||||||
) | ||||||||||||||||||||
from aiohttp.hdrs import METH_DELETE, METH_GET, METH_POST, METH_PUT | ||||||||||||||||||||
|
||||||||||||||||||||
from homewizard_energy.errors import ( | ||||||||||||||||||||
DisabledError, | ||||||||||||||||||||
|
@@ -30,6 +23,7 @@ | |||||||||||||||||||
|
||||||||||||||||||||
from .cacert import CACERT | ||||||||||||||||||||
from .models import Device, Measurement, System, SystemUpdate | ||||||||||||||||||||
from .websocket import Websocket | ||||||||||||||||||||
|
||||||||||||||||||||
_LOGGER = logging.getLogger(__name__) | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -54,9 +48,10 @@ | |||||||||||||||||||
class HomeWizardEnergyV2: | ||||||||||||||||||||
"""Communicate with a HomeWizard Energy device.""" | ||||||||||||||||||||
|
||||||||||||||||||||
_clientsession: ClientSession | None = None | ||||||||||||||||||||
_clientsession: aiohttp.ClientSession | None = None | ||||||||||||||||||||
_close_clientsession: bool = False | ||||||||||||||||||||
_request_timeout: int = 10 | ||||||||||||||||||||
_websocket: Websocket | None = None | ||||||||||||||||||||
|
||||||||||||||||||||
def __init__( | ||||||||||||||||||||
self, | ||||||||||||||||||||
|
@@ -89,6 +84,16 @@ | |||||||||||||||||||
""" | ||||||||||||||||||||
return self._host | ||||||||||||||||||||
|
||||||||||||||||||||
@property | ||||||||||||||||||||
def websocket(self) -> Websocket: | ||||||||||||||||||||
"""Return the websocket object. | ||||||||||||||||||||
|
||||||||||||||||||||
Create a new websocket object if it does not exist. | ||||||||||||||||||||
""" | ||||||||||||||||||||
if self._websocket is None: | ||||||||||||||||||||
self._websocket = Websocket(self) | ||||||||||||||||||||
return self._websocket | ||||||||||||||||||||
|
||||||||||||||||||||
@authorized_method | ||||||||||||||||||||
async def device(self) -> Device: | ||||||||||||||||||||
"""Return the device object.""" | ||||||||||||||||||||
|
@@ -115,7 +120,7 @@ | |||||||||||||||||||
if update is not None: | ||||||||||||||||||||
data = update.as_dict() | ||||||||||||||||||||
status, response = await self._request( | ||||||||||||||||||||
"/api/system", method=METH_PUT, data=data | ||||||||||||||||||||
"/api/system", method=aiohttp.hdrs.METH_PUT, data=data | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
else: | ||||||||||||||||||||
|
@@ -133,7 +138,7 @@ | |||||||||||||||||||
self, | ||||||||||||||||||||
) -> bool: | ||||||||||||||||||||
"""Send identify request.""" | ||||||||||||||||||||
await self._request("/api/system/identify", method=METH_PUT) | ||||||||||||||||||||
await self._request("/api/system/identify", method=aiohttp.hdrs.METH_PUT) | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add unit tests for the The Would you like assistance in creating unit tests for the 🧰 Tools🪛 GitHub Check: codecov/patch[warning] 141-141: homewizard_energy/v2/init.py#L141 |
||||||||||||||||||||
return True | ||||||||||||||||||||
|
||||||||||||||||||||
async def get_token( | ||||||||||||||||||||
|
@@ -142,7 +147,7 @@ | |||||||||||||||||||
) -> str: | ||||||||||||||||||||
"""Get authorization token from device.""" | ||||||||||||||||||||
status, response = await self._request( | ||||||||||||||||||||
"/api/user", method=METH_POST, data={"name": f"local/{name}"} | ||||||||||||||||||||
"/api/user", method=aiohttp.hdrs.METH_POST, data={"name": f"local/{name}"} | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
if status == HTTPStatus.FORBIDDEN: | ||||||||||||||||||||
|
@@ -168,7 +173,7 @@ | |||||||||||||||||||
"""Delete authorization token from device.""" | ||||||||||||||||||||
status, response = await self._request( | ||||||||||||||||||||
"/api/user", | ||||||||||||||||||||
method=METH_DELETE, | ||||||||||||||||||||
method=aiohttp.hdrs.METH_DELETE, | ||||||||||||||||||||
data={"name": name} if name is not None else None, | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -180,11 +185,34 @@ | |||||||||||||||||||
if name is None: | ||||||||||||||||||||
self._token = None | ||||||||||||||||||||
|
||||||||||||||||||||
async def _get_clientsession(self) -> ClientSession: | ||||||||||||||||||||
@property | ||||||||||||||||||||
def token(self) -> str | None: | ||||||||||||||||||||
"""Return the token of the device. | ||||||||||||||||||||
|
||||||||||||||||||||
Returns: | ||||||||||||||||||||
token: The used token | ||||||||||||||||||||
|
||||||||||||||||||||
""" | ||||||||||||||||||||
return self._token | ||||||||||||||||||||
|
||||||||||||||||||||
@property | ||||||||||||||||||||
def request_timeout(self) -> int: | ||||||||||||||||||||
"""Return the request timeout of the device. | ||||||||||||||||||||
|
||||||||||||||||||||
Returns: | ||||||||||||||||||||
request_timeout: The used request timeout | ||||||||||||||||||||
|
||||||||||||||||||||
""" | ||||||||||||||||||||
return self._request_timeout | ||||||||||||||||||||
|
||||||||||||||||||||
async def get_clientsession(self) -> aiohttp.ClientSession: | ||||||||||||||||||||
""" | ||||||||||||||||||||
Get a clientsession that is tuned for communication with the HomeWizard Energy Device | ||||||||||||||||||||
""" | ||||||||||||||||||||
|
||||||||||||||||||||
if self._clientsession is not None: | ||||||||||||||||||||
return self._clientsession | ||||||||||||||||||||
Comment on lines
+213
to
+214
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure thread safety in lazy initialization The lazy initialization of Apply this diff to address the issue: + import asyncio
+ _clientsession_lock = asyncio.Lock()
+ _websocket_lock = asyncio.Lock()
async def get_clientsession(self) -> aiohttp.ClientSession:
"""
Get a clientsession that is tuned for communication with the HomeWizard Energy Device
"""
+ async with self._clientsession_lock:
if self._clientsession is not None:
return self._clientsession
# proceed to initialize the clientsession
@property
def websocket(self) -> Websocket:
"""Return the websocket object.
Create a new websocket object if it does not exist.
"""
+ async def init_websocket():
+ async with self._websocket_lock:
+ if self._websocket is None:
+ self._websocket = Websocket(self)
+ return self._websocket
+ loop = asyncio.get_event_loop()
+ return loop.run_until_complete(init_websocket()) Note: Since properties cannot be asynchronous, the workaround involves running an asynchronous method in the event loop. Alternatively, document that the class is not thread-safe and should be used accordingly. Also applies to: 93-95 🧰 Tools🪛 GitHub Check: codecov/patch[warning] 214-214: homewizard_energy/v2/init.py#L214 |
||||||||||||||||||||
|
||||||||||||||||||||
def _build_ssl_context() -> ssl.SSLContext: | ||||||||||||||||||||
context = ssl.create_default_context(cadata=CACERT) | ||||||||||||||||||||
if self._identifier is not None: | ||||||||||||||||||||
|
@@ -199,26 +227,28 @@ | |||||||||||||||||||
loop = asyncio.get_running_loop() | ||||||||||||||||||||
context = await loop.run_in_executor(None, _build_ssl_context) | ||||||||||||||||||||
|
||||||||||||||||||||
connector = TCPConnector( | ||||||||||||||||||||
connector = aiohttp.TCPConnector( | ||||||||||||||||||||
enable_cleanup_closed=True, | ||||||||||||||||||||
ssl=context, | ||||||||||||||||||||
limit_per_host=1, | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
Comment on lines
+230
to
235
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Update deprecated The Apply this diff to remove the deprecated parameter: connector = aiohttp.TCPConnector(
- enable_cleanup_closed=True,
ssl=context,
limit_per_host=1,
) 📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: codecov/patch[warning] 230-230: homewizard_energy/v2/init.py#L230 |
||||||||||||||||||||
return ClientSession( | ||||||||||||||||||||
connector=connector, timeout=ClientTimeout(total=self._request_timeout) | ||||||||||||||||||||
self._clientsession = aiohttp.ClientSession( | ||||||||||||||||||||
connector=connector, | ||||||||||||||||||||
timeout=aiohttp.ClientTimeout(total=self._request_timeout), | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
return self._clientsession | ||||||||||||||||||||
|
||||||||||||||||||||
@backoff.on_exception(backoff.expo, RequestError, max_tries=5, logger=None) | ||||||||||||||||||||
async def _request( | ||||||||||||||||||||
self, path: str, method: str = METH_GET, data: object = None | ||||||||||||||||||||
self, path: str, method: str = aiohttp.hdrs.METH_GET, data: object = None | ||||||||||||||||||||
) -> Any: | ||||||||||||||||||||
"""Make a request to the API.""" | ||||||||||||||||||||
|
||||||||||||||||||||
if self._clientsession is None: | ||||||||||||||||||||
self._clientsession = await self._get_clientsession() | ||||||||||||||||||||
_clientsession = await self.get_clientsession() | ||||||||||||||||||||
|
||||||||||||||||||||
if self._clientsession.closed: | ||||||||||||||||||||
if _clientsession.closed: | ||||||||||||||||||||
# Avoid runtime errors when connection is closed. | ||||||||||||||||||||
# This solves an issue when updates were scheduled and clientsession was closed. | ||||||||||||||||||||
return None | ||||||||||||||||||||
Comment on lines
+251
to
254
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure consistent return type from The Apply this diff to raise an exception when the client session is closed: if _clientsession.closed:
# Avoid runtime errors when connection is closed.
# This solves an issue when updates were scheduled and clientsession was closed.
- return None
+ raise RequestError("Client session is closed") 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||
|
@@ -235,7 +265,7 @@ | |||||||||||||||||||
|
||||||||||||||||||||
try: | ||||||||||||||||||||
async with async_timeout.timeout(self._request_timeout): | ||||||||||||||||||||
resp = await self._clientsession.request( | ||||||||||||||||||||
resp = await _clientsession.request( | ||||||||||||||||||||
method, | ||||||||||||||||||||
url, | ||||||||||||||||||||
json=data, | ||||||||||||||||||||
|
@@ -249,7 +279,7 @@ | |||||||||||||||||||
raise RequestError( | ||||||||||||||||||||
f"Timeout occurred while connecting to the HomeWizard Energy device at {self.host}" | ||||||||||||||||||||
) from exception | ||||||||||||||||||||
except (ClientError, ClientResponseError) as exception: | ||||||||||||||||||||
except (aiohttp.ClientError, aiohttp.ClientResponseError) as exception: | ||||||||||||||||||||
raise RequestError( | ||||||||||||||||||||
f"Error occurred while communicating with the HomeWizard Energy device at {self.host}" | ||||||||||||||||||||
) from exception | ||||||||||||||||||||
|
@@ -276,6 +306,7 @@ | |||||||||||||||||||
_LOGGER.debug("Closing clientsession") | ||||||||||||||||||||
if self._clientsession is not None: | ||||||||||||||||||||
await self._clientsession.close() | ||||||||||||||||||||
self._clientsession = None | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add unit tests for the The Would you like assistance in creating unit tests for the 🧰 Tools🪛 GitHub Check: codecov/patch[warning] 309-309: homewizard_energy/v2/init.py#L309 |
||||||||||||||||||||
|
||||||||||||||||||||
async def __aenter__(self) -> HomeWizardEnergyV2: | ||||||||||||||||||||
"""Async enter. | ||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,16 @@ | ||
"""Constants for HomeWizard Energy.""" | ||
|
||
SUPPORTED_API_VERSION = "v1" | ||
from enum import StrEnum | ||
|
||
SUPPORTED_API_VERSION = "2.0.0" | ||
|
||
SUPPORTS_STATE = ["HWE-SKT"] | ||
SUPPORTS_IDENTIFY = ["HWE-SKT", "HWE-P1", "HWE-WTR"] | ||
|
||
|
||
class WebsocketTopic(StrEnum): | ||
"""Websocket topics.""" | ||
|
||
DEVICE = "device" | ||
MEASUREMENT = "measurement" | ||
SYSTEM = "system" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add unit tests for the
websocket
property initializationThe new
websocket
property introduces lazy initialization of the_websocket
instance. Currently, there are no unit tests covering this property, which could lead to undetected bugs or regressions in the future. Please consider adding unit tests to ensure proper functionality.Would you like assistance in creating unit tests for this property?
🧰 Tools
🪛 GitHub Check: codecov/patch
[warning] 94-95: homewizard_energy/v2/init.py#L94-L95
Added lines #L94 - L95 were not covered by tests