Skip to content

Commit

Permalink
fix: can get gate state and open/close gate
Browse files Browse the repository at this point in the history
Doesn't seem to poll gate for status changes that happen elsewhere
  • Loading branch information
cameronr committed Jul 28, 2024
1 parent 750296c commit 0934cfb
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 24 deletions.
113 changes: 99 additions & 14 deletions custom_components/doorking_1812ap/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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,
Expand All @@ -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,
Expand Down
10 changes: 7 additions & 3 deletions custom_components/doorking_1812ap/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
1 change: 0 additions & 1 deletion custom_components/doorking_1812ap/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions custom_components/doorking_1812ap/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
)

Expand Down Expand Up @@ -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()

0 comments on commit 0934cfb

Please sign in to comment.