Skip to content

Commit

Permalink
Merge pull request #64 from zweckj/feature/aiohttp-migratoin
Browse files Browse the repository at this point in the history
Migrate to aiohttp
  • Loading branch information
zweckj authored Nov 29, 2024
2 parents a337131 + 2036505 commit 9c54afa
Show file tree
Hide file tree
Showing 15 changed files with 466 additions and 266 deletions.
6 changes: 3 additions & 3 deletions pylamarzocco/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Import for ease of use."""

from .client_local import LaMarzoccoLocalClient
from .client_cloud import LaMarzoccoCloudClient
from .client_bluetooth import LaMarzoccoBluetoothClient
from .lm_machine import LaMarzoccoMachine
from .client_cloud import LaMarzoccoCloudClient
from .client_local import LaMarzoccoLocalClient
from .lm_grinder import LaMarzoccoGrinder
from .lm_machine import LaMarzoccoMachine
4 changes: 1 addition & 3 deletions pylamarzocco/client_bluetooth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
SETTINGS_CHARACTERISTIC,
BoilerType,
)
from .exceptions import (
BluetoothConnectionFailed,
)
from .exceptions import BluetoothConnectionFailed

_logger = logging.getLogger(__name__)

Expand Down
53 changes: 29 additions & 24 deletions pylamarzocco/client_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from __future__ import annotations

import asyncio

from datetime import datetime
import logging
from http import HTTPMethod
import time
from datetime import datetime
from http import HTTPMethod
from typing import Any

from httpx import AsyncClient, RequestError
from aiohttp import ClientSession, ClientTimeout
from aiohttp.client_exceptions import ClientError

from .const import (
CUSTOMER_URL,
Expand All @@ -26,11 +26,12 @@
PrebrewMode,
SmartStandbyMode,
)
from .helpers import is_success
from .exceptions import AuthFail, RequestNotSuccessful
from .models import (
AccessToken,
LaMarzoccoFirmware,
LaMarzoccoDeviceInfo,
LaMarzoccoFirmware,
LaMarzoccoWakeUpSleepEntry,
)

Expand All @@ -40,13 +41,13 @@
class LaMarzoccoCloudClient:
"""La Marzocco Cloud Client."""

_client: AsyncClient
_client: ClientSession

def __init__(
self, username: str, password: str, client: AsyncClient | None = None
self, username: str, password: str, client: ClientSession | None = None
) -> None:
if client is None:
self._client = AsyncClient()
self._client = ClientSession()
else:
self._client = client
self._username = username
Expand Down Expand Up @@ -90,13 +91,13 @@ async def __async_get_token(self, data: dict[str, Any]) -> str:
headers = {"Content-Type": "application/x-www-form-urlencoded"}
try:
response = await self._client.post(TOKEN_URL, data=data, headers=headers)
except RequestError as ex:
except ClientError as ex:
raise RequestNotSuccessful(
"Error during HTTP request."
+ f"Request to endpoint {TOKEN_URL} failed with error: {ex}"
) from ex
if response.is_success:
json_response = response.json()
if is_success(response):
json_response = await response.json()
self._access_token = AccessToken(
access_token=json_response["access_token"],
refresh_token=json_response["refresh_token"],
Expand All @@ -105,12 +106,12 @@ async def __async_get_token(self, data: dict[str, Any]) -> str:
_LOGGER.debug("Got new access token: %s", json_response)
return json_response["access_token"]

if response.status_code == 401:
if response.status == 401:
raise AuthFail("Invalid username or password")

raise RequestNotSuccessful(
f"Request to endpoint {TOKEN_URL} failed with status code {response.status_code}"
+ f"response: {response.text}"
f"Request to endpoint {TOKEN_URL} failed with status code {response.status}"
+ f"response: {await response.text()}"
)

async def async_logout(self) -> None:
Expand All @@ -119,15 +120,15 @@ async def async_logout(self) -> None:
return
try:
response = await self._client.post(LOGOUT_URL, data={})
except RequestError as ex:
except ClientError as ex:
raise RequestNotSuccessful(
"Error during HTTP request."
+ f"Request to endpoint {LOGOUT_URL} failed with error: {ex}"
) from ex
if not response.is_success:
if not is_success(response):
raise RequestNotSuccessful(
f"Request to endpoint {LOGOUT_URL} failed with status code {response.status_code},"
+ "response: {response.text}"
f"Request to endpoint {LOGOUT_URL} failed with status code {response.status},"
+ "response: {await response.text()}"
)
self._access_token = None

Expand All @@ -145,22 +146,26 @@ async def _rest_api_call(

try:
response = await self._client.request(
method=method, url=url, json=data, timeout=timeout, headers=headers
method=method,
url=url,
json=data,
timeout=ClientTimeout(total=timeout),
headers=headers,
)
except RequestError as ex:
except ClientError as ex:
raise RequestNotSuccessful(
f"Error during HTTP request. Request to endpoint {url} failed with error: {ex}"
) from ex

# ensure status code indicates success
if response.is_success:
json_response = response.json()
if is_success(response):
json_response = await response.json()
_LOGGER.debug("Request to %s successful", json_response)
return json_response["data"]

raise RequestNotSuccessful(
f"Request to endpoint {response.url} failed with status code {response.status_code}"
+ f"response: {response.text}"
f"Request to endpoint {response.url} failed with status code {response.status}"
+ f"response: {await response.text()}"
)

async def get_customer_fleet(self) -> dict[str, LaMarzoccoDeviceInfo]:
Expand Down
79 changes: 29 additions & 50 deletions pylamarzocco/client_local.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
"""Interact with the local API of La Marzocco machines."""

import asyncio
import logging
from typing import Any, Callable

from httpx import AsyncClient, RequestError
from websockets.asyncio.client import connect, ClientConnection
from websockets.exceptions import (
ConnectionClosed,
InvalidHandshake,
InvalidURI,
WebSocketException,
)
from aiohttp import ClientSession, ClientWebSocketResponse
from aiohttp.client_exceptions import ClientError, InvalidURL

from .client_cloud import LaMarzoccoCloudClient
from .const import DEFAULT_PORT, WEBSOCKET_RETRY_DELAY
from .const import DEFAULT_PORT
from .exceptions import AuthFail, RequestNotSuccessful
from .helpers import is_success

_LOGGER = logging.getLogger(__name__)

Expand All @@ -28,17 +22,16 @@ def __init__(
host: str,
local_bearer: str,
local_port: int = DEFAULT_PORT,
client: AsyncClient | None = None,
client: ClientSession | None = None,
) -> None:
self._host = host
self._local_port = local_port
self._local_bearer = local_bearer

self.websocket: ClientConnection | None = None
self.terminating: bool = False
self.websocket: ClientWebSocketResponse | None = None

if client is None:
self._client = AsyncClient()
self._client = ClientSession()
else:
self._client = client

Expand All @@ -53,7 +46,7 @@ async def get_config(self) -> dict[str, Any]:

@staticmethod
async def validate_connection(
client: AsyncClient,
client: ClientSession,
host: str,
token: str,
port: int = DEFAULT_PORT,
Expand All @@ -79,7 +72,7 @@ async def validate_connection(

@staticmethod
async def _get_config(
client: AsyncClient,
client: ClientSession,
host: str,
token: str,
port: int = DEFAULT_PORT,
Expand All @@ -91,17 +84,17 @@ async def _get_config(
response = await client.get(
f"http://{host}:{port}/api/v1/config", headers=headers
)
except RequestError as ex:
except ClientError as ex:
raise RequestNotSuccessful(
f"Requesting local API failed with exception: {ex}"
) from ex
if response.is_success:
return response.json()
if response.status_code == 403:
if is_success(response):
return await response.json()
if response.status == 403:
raise AuthFail("Local API returned 403.")
raise RequestNotSuccessful(
f"Querying local API failed with statuscode: {response.status_code}"
+ f"response: {response.text}"
f"Querying local API failed with statuscode: {response.status}"
+ f"response: {await response.text()}"
)

async def websocket_connect(
Expand All @@ -112,33 +105,19 @@ async def websocket_connect(

headers = {"Authorization": f"Bearer {self._local_bearer}"}
try:
async for websocket in connect(
async with await self._client.ws_connect(
f"ws://{self._host}:{self._local_port}/api/v1/streaming",
additional_headers=headers,
):
self.websocket = websocket
try:
# Process messages received on the connection.
async for message in websocket:
if self.terminating:
return
if callback is not None:
try:
callback(message)
except Exception as ex: # pylint: disable=broad-except
_LOGGER.exception("Error during callback: %s", ex)
except ConnectionClosed:
if self.terminating:
return
_LOGGER.debug(
"Websocket disconnected, reconnecting in %s",
WEBSOCKET_RETRY_DELAY,
)
await asyncio.sleep(WEBSOCKET_RETRY_DELAY)
continue
except WebSocketException as ex:
_LOGGER.warning("Exception during websocket connection: %s", ex)
except (TimeoutError, OSError, InvalidHandshake) as ex:
_LOGGER.error("Error establishing the websocket connection: %s", ex)
except InvalidURI:
headers=headers,
) as ws:
self.websocket = ws
async for msg in ws:
_LOGGER.debug("Received websocket message: %s", msg)
if callback is not None:
try:
callback(msg.data)
except Exception as ex: # pylint: disable=broad-except
_LOGGER.exception("Error during callback: %s", ex)
except InvalidURL:
_LOGGER.error("Invalid URI passed to websocket connection: %s", self._host)
except (TimeoutError, OSError, ClientError) as ex:
_LOGGER.error("Error establishing the websocket connection: %s", ex)
9 changes: 7 additions & 2 deletions pylamarzocco/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" Helper functions for pylamarzocco. """

from typing import Any
from aiohttp import ClientResponse

from .const import (
BoilerType,
Expand All @@ -10,7 +11,6 @@
SmartStandbyMode,
WeekDay,
)

from .models import (
LaMarzoccoBoiler,
LaMarzoccoCoffeeStatistics,
Expand All @@ -20,7 +20,6 @@
LaMarzoccoWakeUpSleepEntry,
)


# def schedule_to_request(schedule: LaMarzoccoSchedule) -> LaMarzoccoCloudSchedule:
# """convert schedule to API expected input format"""

Expand Down Expand Up @@ -167,6 +166,7 @@ def parse_firmware(
def parse_webhook_statistics(statistics: dict[str, Any]) -> LaMarzoccoCoffeeStatistics:
"""Parse statistics from webhook statistics object."""

continuous = 0
group = statistics["groups"][0]
doses = group["doses"]
drink_stats: dict[PhysicalKey, int] = {}
Expand Down Expand Up @@ -209,3 +209,8 @@ def parse_wakeup_sleep_entries(
)
parsed[wake_up_sleep_entry.entry_id] = wake_up_sleep_entry
return parsed


def is_success(response: ClientResponse) -> bool:
"""Check if response is successful."""
return 200 <= response.status < 300
6 changes: 3 additions & 3 deletions pylamarzocco/lm_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

from bleak import BleakError

from .client_bluetooth import LaMarzoccoBluetoothClient
from .client_cloud import LaMarzoccoCloudClient
from .client_local import LaMarzoccoLocalClient
from .const import FirmwareType
from .exceptions import (
AuthFail,
Expand All @@ -15,9 +18,6 @@
RequestNotSuccessful,
)
from .helpers import parse_firmware
from .client_bluetooth import LaMarzoccoBluetoothClient
from .client_cloud import LaMarzoccoCloudClient
from .client_local import LaMarzoccoLocalClient
from .models import LaMarzoccoDeviceConfig, LaMarzoccoFirmware, LaMarzoccoStatistics

_LOGGER = logging.getLogger(__name__)
Expand Down
2 changes: 1 addition & 1 deletion pylamarzocco/lm_grinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from typing import Any

from .const import FirmwareType, GrinderModel, PhysicalKey
from .client_cloud import LaMarzoccoCloudClient
from .client_local import LaMarzoccoLocalClient
from .const import FirmwareType, GrinderModel, PhysicalKey
from .lm_device import LaMarzoccoDevice
from .models import LaMarzoccoGrinderConfig

Expand Down
Loading

0 comments on commit 9c54afa

Please sign in to comment.