diff --git a/custom_components/doorking_1812ap/api.py b/custom_components/doorking_1812ap/api.py index 1c50d61..32bd61c 100644 --- a/custom_components/doorking_1812ap/api.py +++ b/custom_components/doorking_1812ap/api.py @@ -2,12 +2,15 @@ from __future__ import annotations +import asyncio import socket from typing import Any import aiohttp import async_timeout +from .const import LOGGER + class Doorking1812APApiClientError(Exception): """Exception to indicate a general API error.""" @@ -24,9 +27,20 @@ def _verify_response_or_raise(response: aiohttp.ClientResponse) -> None: response.raise_for_status() +UNEXPECTED_DATA_ERROR = "Unexpected data received: {}" + + +def _raise_unexpected_data_error(data: bytes) -> None: + """Raise a ValueError for unexpected data.""" + error_message = UNEXPECTED_DATA_ERROR.format(data) + raise ValueError(error_message) + + class Doorking1812APApiClient: """Sample API Client.""" + PORT = 1030 + def __init__( self, ip_address: str, @@ -36,21 +50,92 @@ def __init__( self._ip_address = ip_address self._session = session + async def connect_to_server( + self, + ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]: + """Connect to the Doorking 1812AP.""" + retries = 5 + delay = 1 + for attempt in range(1, retries + 1): + try: + reader, writer = await asyncio.open_connection( + self._ip_address, self.PORT + ) + except (TimeoutError, OSError, socket.gaierror) as e: + LOGGER.debug(f"Attempt {attempt} failed: {e}") + if attempt < retries: + LOGGER.debug(f"Retrying in {delay} seconds...") + await asyncio.sleep(delay) + else: + LOGGER.debug("All retries failed.") + msg = f"Timeout connecting to server - {e}" + raise Doorking1812APApiClientCommunicationError( + msg, + ) from e + else: + LOGGER.debug( + f"Connected to {self._ip_address}:{self.PORT} on attempt {attempt}" + ) + return reader, writer + raise Exception # noqa: TRY002, unreachable + async def async_get_data(self) -> Any: - """Get data from the API.""" - return await self._api_wrapper( - method="get", - url="https://jsonplaceholder.typicode.com/posts/1", - ) - - async def async_set_title(self, value: str) -> Any: - """Get data from the API.""" - return await self._api_wrapper( - method="patch", - url="https://jsonplaceholder.typicode.com/posts/1", - data={"title": value}, - headers={"Content-type": "application/json; charset=UTF-8"}, - ) + """Get open status from gate controller.""" + try: + LOGGER.debug("getting state") + state = {} + reader, writer = await self.connect_to_server() + # This byte sequence requests status + message = b"\x01\x10\x03" + writer.write(message) + await writer.drain() + + # Read 7 bytes of data from the server + data = await reader.readexactly(7) + + writer.close() + await writer.wait_closed() + + if data == b"\x10\x10\x07\x80\x00\x80\x00": + LOGGER.debug("gate is open") + state["open"] = True + return state + + if data == b"\x10\x10\x07\x00\x00\x00\x00": + state["open"] = False + LOGGER.debug("gate is closed") + return state + + LOGGER.debug("gate is unknown") + _raise_unexpected_data_error(data) + + except Exception as exception: # pylint: disable=broad-except + msg = f"Something really wrong happened! - {exception}" + raise Doorking1812APApiClientError( + msg, + ) from exception + + async def async_open_gate(self, *, close_gate: bool = False) -> Any: + """Open or close gate.""" + try: + _, writer = await self.connect_to_server() + data = b"\x01\x11\x05\x00\x80" if close_gate else b"\x01\x11\x05\x01\x80" + + LOGGER.debug(f"set gate open/close: {close_gate}") + + # We spam the message 3 times just in case + for _ in range(3): + writer.write(data) + await writer.drain() + + writer.close() + await writer.wait_closed() + + except Exception as exception: # pylint: disable=broad-except + msg = f"Something really wrong happened! - {exception}" + raise Doorking1812APApiClientError( + msg, + ) from exception async def _api_wrapper( self, diff --git a/custom_components/doorking_1812ap/config_flow.py b/custom_components/doorking_1812ap/config_flow.py index d522135..4e7fc41 100644 --- a/custom_components/doorking_1812ap/config_flow.py +++ b/custom_components/doorking_1812ap/config_flow.py @@ -29,7 +29,7 @@ async def async_step_user( _errors = {} if user_input is not None: try: - await self._test_credentials( + await self._test_connection( ip_address=user_input[CONF_IP_ADDRESS], ) except Doorking1812APApiClientCommunicationError as exception: @@ -61,10 +61,14 @@ async def async_step_user( errors=_errors, ) - async def _test_credentials(self, ip_address: str) -> None: + async def _test_connection(self, ip_address: str) -> None: """Validate credentials.""" client = Doorking1812APApiClient( ip_address=ip_address, session=async_create_clientsession(self.hass), ) - await client.async_get_data() + + # connect to server + # try to read gate status + + await client.async_get_state() diff --git a/custom_components/doorking_1812ap/coordinator.py b/custom_components/doorking_1812ap/coordinator.py index b384eb0..dff1167 100644 --- a/custom_components/doorking_1812ap/coordinator.py +++ b/custom_components/doorking_1812ap/coordinator.py @@ -5,7 +5,6 @@ from datetime import timedelta from typing import TYPE_CHECKING, Any -from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .api import Doorking1812APApiClientError diff --git a/custom_components/doorking_1812ap/switch.py b/custom_components/doorking_1812ap/switch.py index 8d4fcd6..dfeec8f 100644 --- a/custom_components/doorking_1812ap/switch.py +++ b/custom_components/doorking_1812ap/switch.py @@ -18,8 +18,8 @@ ENTITY_DESCRIPTIONS = ( SwitchEntityDescription( key="doorking_1812ap", - name="Doorking 1812AP", - icon="mdi:format-quote-close", + name="Doorking 1812AP Gate Controller", + icon="mdi:gate", ), ) @@ -52,16 +52,18 @@ def __init__( self.entity_description = entity_description @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if the switch is on.""" - return self.coordinator.data.get("title", "") == "foo" + return self.coordinator.data.get("open", "") async def async_turn_on(self, **_: Any) -> None: """Turn on the switch.""" - await self.coordinator.config_entry.runtime_data.client.async_set_title("bar") + await self.coordinator.config_entry.runtime_data.client.async_open_gate() await self.coordinator.async_request_refresh() async def async_turn_off(self, **_: Any) -> None: """Turn off the switch.""" - await self.coordinator.config_entry.runtime_data.client.async_set_title("foo") + await self.coordinator.config_entry.runtime_data.client.async_open_gate( + close_gate=True + ) await self.coordinator.async_request_refresh()