From b667b8d9c848e3a3889fc30f9e2e119d6f8a654b Mon Sep 17 00:00:00 2001 From: Nate Date: Sat, 21 Dec 2024 21:11:38 -0600 Subject: [PATCH] Remove websockets from this branch --- homeassistant_api/__init__.py | 2 - homeassistant_api/rawwebsocket.py | 182 ------------------------------ homeassistant_api/websocket.py | 73 ------------ 3 files changed, 257 deletions(-) delete mode 100644 homeassistant_api/rawwebsocket.py delete mode 100644 homeassistant_api/websocket.py diff --git a/homeassistant_api/__init__.py b/homeassistant_api/__init__.py index cf924e6..8f6ca38 100644 --- a/homeassistant_api/__init__.py +++ b/homeassistant_api/__init__.py @@ -20,11 +20,9 @@ "ParameterMissingError", "RequestError", "UnauthorizedError", - "WebSocketClient", ) from .client import Client -from .websocket import WebSocketClient from .errors import ( APIConfigurationError, EndpointNotFoundError, diff --git a/homeassistant_api/rawwebsocket.py b/homeassistant_api/rawwebsocket.py deleted file mode 100644 index 4f781ab..0000000 --- a/homeassistant_api/rawwebsocket.py +++ /dev/null @@ -1,182 +0,0 @@ -import json -import logging -import time - -# import threading - -import websockets.sync.client as ws -from typing import Any - -from homeassistant_api.errors import ReceivingError, ResponseError, UnauthorizedError - - -logger = logging.getLogger(__name__) - - -class RawWebSocketClient: - api_url: str - token: str - _conn: ws.ClientConnection - - def __init__( - self, - api_url: str, - token: str, - ) -> None: - self.api_url = api_url - self.token = token - self._conn = None - self._id_counter = 0 - self._result_responses: dict[int, dict[str, Any]] = {} # id -> response - self._event_responses: dict[int, list[dict[str, Any]]] = ( - {} - ) # id -> [response, ...] - self._ping_responses: dict[int, dict[str, float]] = {} # id -> (sent, received) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.api_url!r})" - - def __enter__(self): - self._conn = ws.connect(self.api_url) - self._conn.__enter__() - self.authentication_phase() - return self - - def __exit__(self, exc_type, exc_value, traceback): - self._conn.__exit__(exc_type, exc_value, traceback) - self._conn = None - - def _request_id(self) -> int: - """Get a unique id for a message.""" - self._id_counter += 1 - return self._id_counter - - def _send(self, data: dict[str, Any]) -> None: - """Send a message to the websocket server.""" - logger.info(f"Sending message: {data}") - self._conn.send(json.dumps(data)) - - def _recv(self) -> dict[str, Any]: - """Receive a message from the websocket server.""" - _bytes = self._conn.recv() - - # logger.info(f"Received message: {_bytes}") - - return json.loads(_bytes) - - def send(self, type: str, include_id: bool = True, **data: Any) -> int: - """ - Send a command message to the websocket server and wait for a "result" response. - - Returns the id of the message sent. - """ - if include_id: # auth messages don't have an id - data["id"] = self._request_id() - data["type"] = type - - self._send(data) - - if "id" in data: - match data["type"]: - case "event": - self._event_responses[data["id"]] = [] - case "ping": - self._ping_responses[data["id"]] = {"start": time.perf_counter_ns()} - case ( - _ - ): # anything else is one-time command that returns a "type": "result" entry - self._result_responses[data["id"]] = None - return data["id"] - return -1 # non-command messages don't have an id - - def check_success(self, data: dict[str, Any]) -> None: - """Check if a command message was successful.""" - match data: - case {"type": "result", "success": False, "error": {}}: - raise ResponseError(data["error"].pop("message"), data["error"]) - case {"type": "result", "success": True}: - # this is the expected case - pass - case {"type": "result"}: - raise ResponseError( - "Wrongly formatted response", data - ) # because "type": "result" should imply a "success" key - return data - - def handle_recv(self, data: dict[str, Any]) -> dict[str, Any]: - """Handle a received message.""" - if "id" not in data: - raise ReceivingError( - "Received a message without an id outside the auth phase." - ) - - match data: - case {"type": "pong"}: - logger.info("Received pong message") - self._ping_responses[data["id"]].update( - {"end": time.perf_counter_ns(), **data} - ) - data = self._ping_responses[data["id"]] - case {"type": "result"}: - logger.info("Received result message") - self._result_responses[data["id"]] = data - case {"type": "event"}: - logger.info("Received event message") - self._event_responses[data["id"]].append(data) - case _: - logger.warning(f"Received unknown message: {data}") - - return self.check_success(data) - - def recv(self, id: int) -> dict[str, Any]: - """Receive a response to a message from the websocket server.""" - while True: - ## have we received a message with the id we're looking for? - if self._result_responses.get(id) is not None: - return self._result_responses.pop(id) - if self._event_responses.get(id, []): - if len(self._event_responses[id]) > 1: - return self._event_responses[id].pop(0) - return self._event_responses.pop(id)[0] - if self._ping_responses.get(id, {}).get("end") is not None: - return self._ping_responses.pop(id) - - ## if not, keep receiving messages until we do - data = self._recv() - - if "id" not in data: - raise ResponseError( - "Received a message without an id outside the auth phase." - ) - - data = self.handle_recv(data) - - if data["id"] == id: ## we've found the message we're looking for - return data - - def authentication_phase(self) -> dict[str, Any]: - """Authenticate with the websocket server.""" - # Capture the first message from the server saying we need to authenticate - welcome = self._recv() - logging.debug(f"Received welcome message: {welcome}") - if welcome["type"] != "auth_required": - raise ResponseError("Unexpected response during authentication") - - # Send our authentication token - self.send("auth", access_token=self.token, include_id=False) - logging.debug("Sent auth message") - # Check the response - match (resp := self._recv())["type"]: - case "auth_ok": - return None - case "auth_invalid": - raise UnauthorizedError() - case _: - raise ResponseError( - "Unexpected response during authentication", resp["message"] - ) - - def ping_latency(self) -> float: - """Get the latency (in milliseconds) of the connection by sending a ping message.""" - pong = self.recv(self.send("ping")) - return (pong["end"] - pong["start"]) / 1_000_000 diff --git a/homeassistant_api/websocket.py b/homeassistant_api/websocket.py deleted file mode 100644 index 0fb5f5c..0000000 --- a/homeassistant_api/websocket.py +++ /dev/null @@ -1,73 +0,0 @@ -from typing import Any, cast - -from homeassistant_api.models.domains import Domain -from .rawwebsocket import RawWebSocketClient - -import urllib.parse as urlparse - -import logging - - -logger = logging.getLogger(__name__) - - -class WebSocketClient(RawWebSocketClient): - def __init__( - self, - api_url: str, - token: str, - ) -> None: - parsed = urlparse.urlparse(api_url) - - if parsed.scheme not in {"ws", "wss"}: - raise ValueError(f"Unknown scheme {parsed.scheme} in {api_url}") - super().__init__(api_url, token) - logger.info(f"WebSocketClient initialized with api_url: {api_url}") - - def get_config(self) -> dict[str, Any]: - """Get the configuration.""" - return self.recv(self.send("get_config"))["result"] - - def get_entities(self) -> list[dict[str, str]]: - """Get a list of entities.""" - - # Note: Even though it says "get_states" this is actually comparable - # to the `get_entities` method from the REST API clients. - # TODO: do the same parsing logic as in the REST API client - return self.recv(self.send("get_states")) - - def get_domains(self) -> list[str]: - """Get a list of (service) domains.""" - data = self.recv(self.send("get_services"))["result"] - domains = map( - lambda item: Domain.from_json( - {"domain": item[0], "services": item[1]}, - client=cast(WebSocketClient, self), - ), - cast(dict[str, Any], data).items(), - ) - return {domain.domain_id: domain for domain in domains} - - def trigger_service(self, domain: str, service: str, **service_data) -> None: - """Trigger a service.""" - pass - - def get_events(self) -> list[dict[str, str]]: - """Get a list of events.""" - pass - - def subscribe_event(self, event_type: str) -> None: - """Subscribe to an event.""" - pass - - def unsubscribe_event(self, event_type: str) -> None: - """Unsubscribe from an event.""" - pass - - def subscribe_trigger(self, entity_id: str) -> None: - """Subscribe to a trigger.""" - pass - - def unsubscribe_trigger(self, entity_id: str) -> None: - """Unsubscribe from a trigger.""" - pass