2024-06-03 09:14:10 +00:00
|
|
|
"""Configuration for connecting directly to a device without discovery.
|
|
|
|
|
|
|
|
If you are connecting to a newer KASA or TAPO device you can get the device
|
|
|
|
via discovery or connect directly with :class:`DeviceConfig`.
|
|
|
|
|
|
|
|
Discovery returns a list of discovered devices:
|
|
|
|
|
2024-06-03 18:06:54 +00:00
|
|
|
>>> from kasa import Discover, Device
|
2024-06-03 09:14:10 +00:00
|
|
|
>>> device = await Discover.discover_single(
|
|
|
|
>>> "127.0.0.3",
|
2024-06-03 18:06:54 +00:00
|
|
|
>>> username="user@example.com",
|
|
|
|
>>> password="great_password",
|
2024-06-03 09:14:10 +00:00
|
|
|
>>> )
|
|
|
|
>>> print(device.alias) # Alias is None because update() has not been called
|
|
|
|
None
|
|
|
|
|
|
|
|
>>> config_dict = device.config.to_dict()
|
|
|
|
>>> # DeviceConfig.to_dict() can be used to store for later
|
|
|
|
>>> print(config_dict)
|
|
|
|
{'host': '127.0.0.3', 'timeout': 5, 'credentials': Credentials(), 'connection_type'\
|
|
|
|
: {'device_family': 'SMART.TAPOBULB', 'encryption_type': 'KLAP', 'login_version': 2},\
|
|
|
|
'uses_http': True}
|
|
|
|
|
2024-06-03 18:06:54 +00:00
|
|
|
>>> later_device = await Device.connect(config=Device.Config.from_dict(config_dict))
|
2024-06-03 09:14:10 +00:00
|
|
|
>>> print(later_device.alias) # Alias is available as connect() calls update()
|
|
|
|
Living Room Bulb
|
2024-04-16 18:21:20 +00:00
|
|
|
|
2024-04-17 13:39:24 +00:00
|
|
|
"""
|
|
|
|
|
2024-06-03 09:14:10 +00:00
|
|
|
# Note that this module does not work with from __future__ import annotations
|
|
|
|
# due to it's use of type returned by fields() which becomes a string with the import.
|
|
|
|
# https://bugs.python.org/issue39442
|
2024-04-17 13:39:24 +00:00
|
|
|
# ruff: noqa: FA100
|
2023-12-29 19:17:15 +00:00
|
|
|
import logging
|
|
|
|
from dataclasses import asdict, dataclass, field, fields, is_dataclass
|
|
|
|
from enum import Enum
|
2024-09-10 16:24:38 +00:00
|
|
|
from typing import TYPE_CHECKING, Dict, Optional, TypedDict, Union
|
2023-12-29 19:17:15 +00:00
|
|
|
|
|
|
|
from .credentials import Credentials
|
2024-02-21 15:52:55 +00:00
|
|
|
from .exceptions import KasaException
|
2023-12-29 19:17:15 +00:00
|
|
|
|
2024-01-18 09:57:33 +00:00
|
|
|
if TYPE_CHECKING:
|
2024-01-18 17:32:26 +00:00
|
|
|
from aiohttp import ClientSession
|
2024-01-18 09:57:33 +00:00
|
|
|
|
2023-12-29 19:17:15 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2024-09-10 16:24:38 +00:00
|
|
|
class KeyPairDict(TypedDict):
|
|
|
|
"""Class to represent a public/private key pair."""
|
|
|
|
|
|
|
|
private: str
|
|
|
|
public: str
|
|
|
|
|
|
|
|
|
2024-06-03 18:06:54 +00:00
|
|
|
class DeviceEncryptionType(Enum):
|
2023-12-29 19:17:15 +00:00
|
|
|
"""Encrypt type enum."""
|
|
|
|
|
|
|
|
Klap = "KLAP"
|
|
|
|
Aes = "AES"
|
|
|
|
Xor = "XOR"
|
|
|
|
|
|
|
|
|
2024-06-03 18:06:54 +00:00
|
|
|
class DeviceFamily(Enum):
|
2023-12-29 19:17:15 +00:00
|
|
|
"""Encrypt type enum."""
|
|
|
|
|
|
|
|
IotSmartPlugSwitch = "IOT.SMARTPLUGSWITCH"
|
|
|
|
IotSmartBulb = "IOT.SMARTBULB"
|
|
|
|
SmartKasaPlug = "SMART.KASAPLUG"
|
2024-01-03 18:31:42 +00:00
|
|
|
SmartKasaSwitch = "SMART.KASASWITCH"
|
2023-12-29 19:17:15 +00:00
|
|
|
SmartTapoPlug = "SMART.TAPOPLUG"
|
|
|
|
SmartTapoBulb = "SMART.TAPOBULB"
|
2024-01-25 07:54:56 +00:00
|
|
|
SmartTapoSwitch = "SMART.TAPOSWITCH"
|
2024-02-22 22:09:38 +00:00
|
|
|
SmartTapoHub = "SMART.TAPOHUB"
|
2024-04-22 14:24:15 +00:00
|
|
|
SmartKasaHub = "SMART.KASAHUB"
|
2023-12-29 19:17:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _dataclass_from_dict(klass, in_val):
|
|
|
|
if is_dataclass(klass):
|
|
|
|
fieldtypes = {f.name: f.type for f in fields(klass)}
|
|
|
|
val = {}
|
|
|
|
for dict_key in in_val:
|
2024-01-18 17:51:50 +00:00
|
|
|
if dict_key in fieldtypes:
|
|
|
|
if hasattr(fieldtypes[dict_key], "from_dict"):
|
|
|
|
val[dict_key] = fieldtypes[dict_key].from_dict(in_val[dict_key])
|
|
|
|
else:
|
|
|
|
val[dict_key] = _dataclass_from_dict(
|
|
|
|
fieldtypes[dict_key], in_val[dict_key]
|
|
|
|
)
|
2023-12-29 19:17:15 +00:00
|
|
|
else:
|
2024-02-21 15:52:55 +00:00
|
|
|
raise KasaException(
|
2024-01-18 17:51:50 +00:00
|
|
|
f"Cannot create dataclass from dict, unknown key: {dict_key}"
|
2023-12-29 19:17:15 +00:00
|
|
|
)
|
|
|
|
return klass(**val)
|
|
|
|
else:
|
|
|
|
return in_val
|
|
|
|
|
|
|
|
|
|
|
|
def _dataclass_to_dict(in_val):
|
|
|
|
fieldtypes = {f.name: f.type for f in fields(in_val) if f.compare}
|
|
|
|
out_val = {}
|
|
|
|
for field_name in fieldtypes:
|
|
|
|
val = getattr(in_val, field_name)
|
|
|
|
if val is None:
|
|
|
|
continue
|
|
|
|
elif hasattr(val, "to_dict"):
|
|
|
|
out_val[field_name] = val.to_dict()
|
|
|
|
elif is_dataclass(fieldtypes[field_name]):
|
|
|
|
out_val[field_name] = asdict(val)
|
|
|
|
else:
|
|
|
|
out_val[field_name] = val
|
|
|
|
return out_val
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
2024-06-03 18:06:54 +00:00
|
|
|
class DeviceConnectionParameters:
|
2023-12-29 19:17:15 +00:00
|
|
|
"""Class to hold the the parameters determining connection type."""
|
|
|
|
|
2024-06-03 18:06:54 +00:00
|
|
|
device_family: DeviceFamily
|
|
|
|
encryption_type: DeviceEncryptionType
|
2024-01-03 21:46:08 +00:00
|
|
|
login_version: Optional[int] = None
|
2023-12-29 19:17:15 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_values(
|
|
|
|
device_family: str,
|
|
|
|
encryption_type: str,
|
2024-01-03 21:46:08 +00:00
|
|
|
login_version: Optional[int] = None,
|
2024-06-03 18:06:54 +00:00
|
|
|
) -> "DeviceConnectionParameters":
|
2023-12-29 19:17:15 +00:00
|
|
|
"""Return connection parameters from string values."""
|
|
|
|
try:
|
2024-06-03 18:06:54 +00:00
|
|
|
return DeviceConnectionParameters(
|
|
|
|
DeviceFamily(device_family),
|
|
|
|
DeviceEncryptionType(encryption_type),
|
2024-01-03 21:46:08 +00:00
|
|
|
login_version,
|
2023-12-29 19:17:15 +00:00
|
|
|
)
|
2024-01-03 21:46:08 +00:00
|
|
|
except (ValueError, TypeError) as ex:
|
2024-02-21 15:52:55 +00:00
|
|
|
raise KasaException(
|
2024-01-03 21:46:08 +00:00
|
|
|
f"Invalid connection parameters for {device_family}."
|
|
|
|
+ f"{encryption_type}.{login_version}"
|
2023-12-29 19:17:15 +00:00
|
|
|
) from ex
|
|
|
|
|
|
|
|
@staticmethod
|
2024-06-03 18:06:54 +00:00
|
|
|
def from_dict(connection_type_dict: Dict[str, str]) -> "DeviceConnectionParameters":
|
2023-12-29 19:17:15 +00:00
|
|
|
"""Return connection parameters from dict."""
|
|
|
|
if (
|
|
|
|
isinstance(connection_type_dict, dict)
|
|
|
|
and (device_family := connection_type_dict.get("device_family"))
|
|
|
|
and (encryption_type := connection_type_dict.get("encryption_type"))
|
|
|
|
):
|
2024-01-03 21:46:08 +00:00
|
|
|
if login_version := connection_type_dict.get("login_version"):
|
|
|
|
login_version = int(login_version) # type: ignore[assignment]
|
2024-06-03 18:06:54 +00:00
|
|
|
return DeviceConnectionParameters.from_values(
|
2024-01-03 21:46:08 +00:00
|
|
|
device_family,
|
|
|
|
encryption_type,
|
|
|
|
login_version, # type: ignore[arg-type]
|
|
|
|
)
|
2023-12-29 19:17:15 +00:00
|
|
|
|
2024-02-21 15:52:55 +00:00
|
|
|
raise KasaException(f"Invalid connection type data for {connection_type_dict}")
|
2023-12-29 19:17:15 +00:00
|
|
|
|
2024-01-03 21:46:08 +00:00
|
|
|
def to_dict(self) -> Dict[str, Union[str, int]]:
|
2023-12-29 19:17:15 +00:00
|
|
|
"""Convert connection params to dict."""
|
2024-01-03 21:46:08 +00:00
|
|
|
result: Dict[str, Union[str, int]] = {
|
2023-12-29 19:17:15 +00:00
|
|
|
"device_family": self.device_family.value,
|
|
|
|
"encryption_type": self.encryption_type.value,
|
|
|
|
}
|
2024-01-03 21:46:08 +00:00
|
|
|
if self.login_version:
|
|
|
|
result["login_version"] = self.login_version
|
2023-12-29 19:17:15 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class DeviceConfig:
|
|
|
|
"""Class to represent paramaters that determine how to connect to devices."""
|
|
|
|
|
|
|
|
DEFAULT_TIMEOUT = 5
|
2024-01-10 19:13:14 +00:00
|
|
|
#: IP address or hostname
|
2023-12-29 19:17:15 +00:00
|
|
|
host: str
|
2024-01-10 19:13:14 +00:00
|
|
|
#: Timeout for querying the device
|
2023-12-29 19:17:15 +00:00
|
|
|
timeout: Optional[int] = DEFAULT_TIMEOUT
|
2024-01-10 19:13:14 +00:00
|
|
|
#: Override the default 9999 port to support port forwarding
|
2023-12-29 19:17:15 +00:00
|
|
|
port_override: Optional[int] = None
|
2024-01-10 19:13:14 +00:00
|
|
|
#: Credentials for devices requiring authentication
|
2024-01-03 21:46:08 +00:00
|
|
|
credentials: Optional[Credentials] = None
|
2024-01-10 19:13:14 +00:00
|
|
|
#: Credentials hash for devices requiring authentication.
|
|
|
|
#: If credentials are also supplied they take precendence over credentials_hash.
|
2024-05-16 16:13:44 +00:00
|
|
|
#: Credentials hash can be retrieved from :attr:`Device.credentials_hash`
|
2024-01-03 21:46:08 +00:00
|
|
|
credentials_hash: Optional[str] = None
|
2024-01-10 19:13:14 +00:00
|
|
|
#: The protocol specific type of connection. Defaults to the legacy type.
|
2024-01-29 10:55:54 +00:00
|
|
|
batch_size: Optional[int] = None
|
|
|
|
#: The batch size for protoools supporting multiple request batches.
|
2024-06-03 18:06:54 +00:00
|
|
|
connection_type: DeviceConnectionParameters = field(
|
|
|
|
default_factory=lambda: DeviceConnectionParameters(
|
2024-09-10 16:24:38 +00:00
|
|
|
DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Xor
|
2023-12-29 19:17:15 +00:00
|
|
|
)
|
|
|
|
)
|
2024-01-10 19:13:14 +00:00
|
|
|
#: True if the device uses http. Consumers should retrieve rather than set this
|
|
|
|
#: in order to determine whether they should pass a custom http client if desired.
|
2023-12-29 19:17:15 +00:00
|
|
|
uses_http: bool = False
|
2024-01-10 19:13:14 +00:00
|
|
|
|
2023-12-29 19:17:15 +00:00
|
|
|
# compare=False will be excluded from the serialization and object comparison.
|
2024-01-10 19:13:14 +00:00
|
|
|
#: Set a custom http_client for the device to use.
|
2024-01-18 17:32:26 +00:00
|
|
|
http_client: Optional["ClientSession"] = field(default=None, compare=False)
|
2023-12-29 19:17:15 +00:00
|
|
|
|
2024-09-10 16:24:38 +00:00
|
|
|
aes_keys: Optional[KeyPairDict] = None
|
|
|
|
|
2023-12-29 19:17:15 +00:00
|
|
|
def __post_init__(self):
|
|
|
|
if self.connection_type is None:
|
2024-06-03 18:06:54 +00:00
|
|
|
self.connection_type = DeviceConnectionParameters(
|
|
|
|
DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Xor
|
2023-12-29 19:17:15 +00:00
|
|
|
)
|
|
|
|
|
2024-01-03 21:46:08 +00:00
|
|
|
def to_dict(
|
|
|
|
self,
|
|
|
|
*,
|
|
|
|
credentials_hash: Optional[str] = None,
|
|
|
|
exclude_credentials: bool = False,
|
|
|
|
) -> Dict[str, Dict[str, str]]:
|
2024-01-10 19:13:14 +00:00
|
|
|
"""Convert device config to dict."""
|
2024-01-10 19:47:30 +00:00
|
|
|
if credentials_hash is not None or exclude_credentials:
|
2024-01-03 21:46:08 +00:00
|
|
|
self.credentials = None
|
|
|
|
if credentials_hash:
|
|
|
|
self.credentials_hash = credentials_hash
|
2023-12-29 19:17:15 +00:00
|
|
|
return _dataclass_to_dict(self)
|
|
|
|
|
|
|
|
@staticmethod
|
2024-01-18 17:51:50 +00:00
|
|
|
def from_dict(config_dict: Dict[str, Dict[str, str]]) -> "DeviceConfig":
|
2024-01-10 19:13:14 +00:00
|
|
|
"""Return device config from dict."""
|
2024-01-18 17:51:50 +00:00
|
|
|
if isinstance(config_dict, dict):
|
|
|
|
return _dataclass_from_dict(DeviceConfig, config_dict)
|
2024-02-21 15:52:55 +00:00
|
|
|
raise KasaException(f"Invalid device config data: {config_dict}")
|