2019-11-11 21:14:34 +00:00
|
|
|
"""Implementation of the TP-Link Smart Home Protocol.
|
|
|
|
|
|
|
|
Encryption/Decryption methods based on the works of
|
|
|
|
Lubomir Stroetmann and Tobias Esser
|
|
|
|
|
|
|
|
https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/
|
|
|
|
https://github.com/softScheck/tplink-smartplug/
|
|
|
|
|
|
|
|
which are licensed under the Apache License, Version 2.0
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
"""
|
2024-04-16 18:21:20 +00:00
|
|
|
|
2024-04-17 13:39:24 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-01-23 14:44:32 +00:00
|
|
|
import base64
|
2022-04-24 17:38:42 +00:00
|
|
|
import errno
|
2024-01-26 16:57:56 +00:00
|
|
|
import hashlib
|
Refactor & add unittests for almost all functionality, add tox for running tests on py27 and py35 (#17)
* Refactor & add unittests for almost all functionality, add tox for running tests on py27 and py35
This commit adds unit tests for current api functionality.
- currently no mocking, all tests are run on the device.
- the library is now compatible with python 2.7 and python 3.5, use tox for tests
- schema checks are done with voluptuous
refactoring:
- protocol is separated into its own file, smartplug adapted to receive protocol worker as parameter.
- cleaned up the initialization routine, initialization is done on use, not on creation of smartplug
- added model and features properties, identity kept for backwards compatibility
- no more storing of local variables outside _sys_info, paves a way to handle state changes sanely (without complete reinitialization)
* Fix CI warnings, remove unused leftover code
* Rename _initialize to _fetch_sysinfo, as that's what it does.
* examples.cli: fix identify call, prettyprint sysinfo, update readme which had false format for led setting
* Add tox-travis for automated testing.
2016-12-16 22:51:56 +00:00
|
|
|
import logging
|
2019-11-11 18:39:43 +00:00
|
|
|
import struct
|
2023-11-20 13:17:10 +00:00
|
|
|
from abc import ABC, abstractmethod
|
2024-07-17 17:57:09 +00:00
|
|
|
from typing import Any, Callable, TypeVar, cast
|
Refactor & add unittests for almost all functionality, add tox for running tests on py27 and py35 (#17)
* Refactor & add unittests for almost all functionality, add tox for running tests on py27 and py35
This commit adds unit tests for current api functionality.
- currently no mocking, all tests are run on the device.
- the library is now compatible with python 2.7 and python 3.5, use tox for tests
- schema checks are done with voluptuous
refactoring:
- protocol is separated into its own file, smartplug adapted to receive protocol worker as parameter.
- cleaned up the initialization routine, initialization is done on use, not on creation of smartplug
- added model and features properties, identity kept for backwards compatibility
- no more storing of local variables outside _sys_info, paves a way to handle state changes sanely (without complete reinitialization)
* Fix CI warnings, remove unused leftover code
* Rename _initialize to _fetch_sysinfo, as that's what it does.
* examples.cli: fix identify call, prettyprint sysinfo, update readme which had false format for led setting
* Add tox-travis for automated testing.
2016-12-16 22:51:56 +00:00
|
|
|
|
2023-07-21 09:50:54 +00:00
|
|
|
# When support for cpython older than 3.11 is dropped
|
|
|
|
# async_timeout can be replaced with asyncio.timeout
|
2024-01-23 14:44:32 +00:00
|
|
|
from .credentials import Credentials
|
2023-12-29 19:17:15 +00:00
|
|
|
from .deviceconfig import DeviceConfig
|
2020-05-27 17:02:09 +00:00
|
|
|
|
Refactor & add unittests for almost all functionality, add tox for running tests on py27 and py35 (#17)
* Refactor & add unittests for almost all functionality, add tox for running tests on py27 and py35
This commit adds unit tests for current api functionality.
- currently no mocking, all tests are run on the device.
- the library is now compatible with python 2.7 and python 3.5, use tox for tests
- schema checks are done with voluptuous
refactoring:
- protocol is separated into its own file, smartplug adapted to receive protocol worker as parameter.
- cleaned up the initialization routine, initialization is done on use, not on creation of smartplug
- added model and features properties, identity kept for backwards compatibility
- no more storing of local variables outside _sys_info, paves a way to handle state changes sanely (without complete reinitialization)
* Fix CI warnings, remove unused leftover code
* Rename _initialize to _fetch_sysinfo, as that's what it does.
* examples.cli: fix identify call, prettyprint sysinfo, update readme which had false format for led setting
* Add tox-travis for automated testing.
2016-12-16 22:51:56 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2022-04-24 17:38:42 +00:00
|
|
|
_NO_RETRY_ERRORS = {errno.EHOSTDOWN, errno.EHOSTUNREACH, errno.ECONNREFUSED}
|
2024-01-09 23:51:04 +00:00
|
|
|
_UNSIGNED_INT_NETWORK_ORDER = struct.Struct(">I")
|
Refactor & add unittests for almost all functionality, add tox for running tests on py27 and py35 (#17)
* Refactor & add unittests for almost all functionality, add tox for running tests on py27 and py35
This commit adds unit tests for current api functionality.
- currently no mocking, all tests are run on the device.
- the library is now compatible with python 2.7 and python 3.5, use tox for tests
- schema checks are done with voluptuous
refactoring:
- protocol is separated into its own file, smartplug adapted to receive protocol worker as parameter.
- cleaned up the initialization routine, initialization is done on use, not on creation of smartplug
- added model and features properties, identity kept for backwards compatibility
- no more storing of local variables outside _sys_info, paves a way to handle state changes sanely (without complete reinitialization)
* Fix CI warnings, remove unused leftover code
* Rename _initialize to _fetch_sysinfo, as that's what it does.
* examples.cli: fix identify call, prettyprint sysinfo, update readme which had false format for led setting
* Add tox-travis for automated testing.
2016-12-16 22:51:56 +00:00
|
|
|
|
2024-07-17 17:57:09 +00:00
|
|
|
_T = TypeVar("_T")
|
|
|
|
|
|
|
|
|
|
|
|
def redact_data(data: _T, redactors: dict[str, Callable[[Any], Any] | None]) -> _T:
|
|
|
|
"""Redact sensitive data for logging."""
|
|
|
|
if not isinstance(data, (dict, list)):
|
|
|
|
return data
|
|
|
|
|
|
|
|
if isinstance(data, list):
|
|
|
|
return cast(_T, [redact_data(val, redactors) for val in data])
|
|
|
|
|
|
|
|
redacted = {**data}
|
|
|
|
|
|
|
|
for key, value in redacted.items():
|
|
|
|
if value is None:
|
|
|
|
continue
|
|
|
|
if isinstance(value, str) and not value:
|
|
|
|
continue
|
|
|
|
if key in redactors:
|
|
|
|
if redactor := redactors[key]:
|
|
|
|
try:
|
|
|
|
redacted[key] = redactor(value)
|
|
|
|
except: # noqa: E722
|
|
|
|
redacted[key] = "**REDACTEX**"
|
|
|
|
else:
|
|
|
|
redacted[key] = "**REDACTED**"
|
|
|
|
elif isinstance(value, dict):
|
|
|
|
redacted[key] = redact_data(value, redactors)
|
|
|
|
elif isinstance(value, list):
|
|
|
|
redacted[key] = [redact_data(item, redactors) for item in value]
|
|
|
|
|
|
|
|
return cast(_T, redacted)
|
|
|
|
|
|
|
|
|
|
|
|
def mask_mac(mac: str) -> str:
|
|
|
|
"""Return mac address with last two octects blanked."""
|
|
|
|
delim = ":" if ":" in mac else "-"
|
|
|
|
rest = delim.join(format(s, "02x") for s in bytes.fromhex("000000"))
|
|
|
|
return f"{mac[:8]}{delim}{rest}"
|
|
|
|
|
Refactor & add unittests for almost all functionality, add tox for running tests on py27 and py35 (#17)
* Refactor & add unittests for almost all functionality, add tox for running tests on py27 and py35
This commit adds unit tests for current api functionality.
- currently no mocking, all tests are run on the device.
- the library is now compatible with python 2.7 and python 3.5, use tox for tests
- schema checks are done with voluptuous
refactoring:
- protocol is separated into its own file, smartplug adapted to receive protocol worker as parameter.
- cleaned up the initialization routine, initialization is done on use, not on creation of smartplug
- added model and features properties, identity kept for backwards compatibility
- no more storing of local variables outside _sys_info, paves a way to handle state changes sanely (without complete reinitialization)
* Fix CI warnings, remove unused leftover code
* Rename _initialize to _fetch_sysinfo, as that's what it does.
* examples.cli: fix identify call, prettyprint sysinfo, update readme which had false format for led setting
* Add tox-travis for automated testing.
2016-12-16 22:51:56 +00:00
|
|
|
|
2023-12-04 18:50:05 +00:00
|
|
|
def md5(payload: bytes) -> bytes:
|
2024-01-26 16:57:56 +00:00
|
|
|
"""Return the MD5 hash of the payload."""
|
|
|
|
return hashlib.md5(payload).digest() # noqa: S324
|
2023-12-04 18:50:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
class BaseTransport(ABC):
|
|
|
|
"""Base class for all TP-Link protocol transports."""
|
|
|
|
|
2023-12-19 14:11:59 +00:00
|
|
|
DEFAULT_TIMEOUT = 5
|
|
|
|
|
2023-12-04 18:50:05 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
*,
|
2023-12-29 19:17:15 +00:00
|
|
|
config: DeviceConfig,
|
2023-12-04 18:50:05 +00:00
|
|
|
) -> None:
|
|
|
|
"""Create a protocol object."""
|
2023-12-29 19:17:15 +00:00
|
|
|
self._config = config
|
|
|
|
self._host = config.host
|
|
|
|
self._port = config.port_override or self.default_port
|
|
|
|
self._credentials = config.credentials
|
2024-01-03 21:46:08 +00:00
|
|
|
self._credentials_hash = config.credentials_hash
|
2024-01-25 17:37:19 +00:00
|
|
|
self._timeout = config.timeout or self.DEFAULT_TIMEOUT
|
2023-12-29 19:17:15 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
@abstractmethod
|
|
|
|
def default_port(self) -> int:
|
|
|
|
"""The default port for the transport."""
|
2023-12-04 18:50:05 +00:00
|
|
|
|
2024-01-03 21:46:08 +00:00
|
|
|
@property
|
|
|
|
@abstractmethod
|
2024-07-02 12:43:37 +00:00
|
|
|
def credentials_hash(self) -> str | None:
|
2024-01-03 21:46:08 +00:00
|
|
|
"""The hashed credentials used by the transport."""
|
|
|
|
|
2023-12-04 18:50:05 +00:00
|
|
|
@abstractmethod
|
2024-04-17 13:39:24 +00:00
|
|
|
async def send(self, request: str) -> dict:
|
2023-12-04 18:50:05 +00:00
|
|
|
"""Send a message to the device and return a response."""
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
async def close(self) -> None:
|
|
|
|
"""Close the transport. Abstract method to be overriden."""
|
|
|
|
|
2024-01-23 22:15:18 +00:00
|
|
|
@abstractmethod
|
|
|
|
async def reset(self) -> None:
|
|
|
|
"""Reset internal state."""
|
|
|
|
|
2023-12-04 18:50:05 +00:00
|
|
|
|
2024-01-22 15:28:30 +00:00
|
|
|
class BaseProtocol(ABC):
|
2023-11-20 13:17:10 +00:00
|
|
|
"""Base class for all TP-Link Smart Home communication."""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
*,
|
2023-12-19 14:11:59 +00:00
|
|
|
transport: BaseTransport,
|
2023-11-20 13:17:10 +00:00
|
|
|
) -> None:
|
|
|
|
"""Create a protocol object."""
|
2023-12-19 14:11:59 +00:00
|
|
|
self._transport = transport
|
|
|
|
|
|
|
|
@property
|
|
|
|
def _host(self):
|
|
|
|
return self._transport._host
|
2023-11-20 13:17:10 +00:00
|
|
|
|
2023-12-29 19:17:15 +00:00
|
|
|
@property
|
|
|
|
def config(self) -> DeviceConfig:
|
|
|
|
"""Return the connection parameters the device is using."""
|
|
|
|
return self._transport._config
|
|
|
|
|
2023-11-20 13:17:10 +00:00
|
|
|
@abstractmethod
|
2024-04-17 13:39:24 +00:00
|
|
|
async def query(self, request: str | dict, retry_count: int = 3) -> dict:
|
2023-11-20 13:17:10 +00:00
|
|
|
"""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."""
|
|
|
|
|
|
|
|
|
2024-04-17 13:39:24 +00:00
|
|
|
def get_default_credentials(tuple: tuple[str, str]) -> Credentials:
|
2024-01-23 14:44:32 +00:00
|
|
|
"""Return decoded default credentials."""
|
|
|
|
un = base64.b64decode(tuple[0].encode()).decode()
|
|
|
|
pw = base64.b64decode(tuple[1].encode()).decode()
|
|
|
|
return Credentials(un, pw)
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_CREDENTIALS = {
|
|
|
|
"KASA": ("a2FzYUB0cC1saW5rLm5ldA==", "a2FzYVNldHVw"),
|
|
|
|
"TAPO": ("dGVzdEB0cC1saW5rLm5ldA==", "dGVzdA=="),
|
2024-10-25 18:27:40 +00:00
|
|
|
"TAPOCAMERA": ("YWRtaW4=", "YWRtaW4="),
|
2024-01-23 14:44:32 +00:00
|
|
|
}
|