Add klap protocol (#509)

* Add support for the new encryption protocol

This adds support for the new TP-Link discovery and encryption
protocols. It is currently incomplete - only devices without
username and password are current supported, and single device
discovery is not implemented.

Discovery should find both old and new devices. When accessing
a device by IP the --klap option can be specified on the command
line to active the new connection protocol.

sdb9696 - This commit also contains 16 later commits from Simon Wilkinson
squashed into the original

* Update klap changes 2023 to fix encryption, deal with kasa credential switching and work with new discovery changes

* Move from aiohttp to httpx

* Changes following review comments

---------

Co-authored-by: Simon Wilkinson <simon@sxw.org.uk>
This commit is contained in:
sdb9696
2023-11-20 13:17:10 +00:00
committed by GitHub
parent bde07d117f
commit 30f217b8ab
10 changed files with 1297 additions and 65 deletions

View File

@@ -14,6 +14,7 @@ import contextlib
import errno
import logging
import struct
from abc import ABC, abstractmethod
from pprint import pformat as pf
from typing import Dict, Generator, Optional, Union
@@ -21,6 +22,7 @@ from typing import Dict, Generator, Optional, Union
# async_timeout can be replaced with asyncio.timeout
from async_timeout import timeout as asyncio_timeout
from .credentials import Credentials
from .exceptions import SmartDeviceException
from .json import dumps as json_dumps
from .json import loads as json_loads
@@ -29,7 +31,31 @@ _LOGGER = logging.getLogger(__name__)
_NO_RETRY_ERRORS = {errno.EHOSTDOWN, errno.EHOSTUNREACH, errno.ECONNREFUSED}
class TPLinkSmartHomeProtocol:
class TPLinkProtocol(ABC):
"""Base class for all TP-Link Smart Home communication."""
def __init__(
self,
host: str,
*,
port: Optional[int] = None,
credentials: Optional[Credentials] = None,
) -> None:
"""Create a protocol object."""
self.host = host
self.port = port
self.credentials = credentials
@abstractmethod
async def query(self, request: Union[str, Dict], retry_count: int = 3) -> Dict:
"""Query the device for the protocol. Abstract method to be overriden."""
@abstractmethod
async def close(self) -> None:
"""Close the protocol. Abstract method to be overriden."""
class TPLinkSmartHomeProtocol(TPLinkProtocol):
"""Implementation of the TP-Link Smart Home protocol."""
INITIALIZATION_VECTOR = 171
@@ -38,11 +64,18 @@ class TPLinkSmartHomeProtocol:
BLOCK_SIZE = 4
def __init__(
self, host: str, *, port: Optional[int] = None, timeout: Optional[int] = None
self,
host: str,
*,
port: Optional[int] = None,
timeout: Optional[int] = None,
credentials: Optional[Credentials] = None,
) -> None:
"""Create a protocol object."""
self.host = host
self.port = port or TPLinkSmartHomeProtocol.DEFAULT_PORT
super().__init__(
host=host, port=port or self.DEFAULT_PORT, credentials=credentials
)
self.reader: Optional[asyncio.StreamReader] = None
self.writer: Optional[asyncio.StreamWriter] = None
self.query_lock = asyncio.Lock()