10000 Add https parameter to device class factory by sdb9696 · Pull Request #1184 · python-kasa/python-kasa · GitHub
[go: up one dir, main page]

Skip to content

Add https parameter to device class factory #1184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions kasa/cli/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ async def print_discovered(dev: Device):
echo(f"{infostr} {dev.alias}")

async def print_unsupported(unsupported_exception: UnsupportedDeviceError):
if res := unsupported_exception.discovery_result:
echo(f"{res.get('ip'):<15} UNSUPPORTED DEVICE")
if host := unsupported_exception.host:
echo(f"{host:<15} UNSUPPORTED DEVICE")

echo(f"{'HOST':<15} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} {'ALIAS'}")
return await _discover(ctx, print_discovered, print_unsupported, do_echo=False)
Expand Down
18 changes: 12 additions & 6 deletions kasa/device_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ async def connect(*, host: str | None = None, config: DeviceConfig) -> Device:
if (protocol := get_protocol(config=config)) is None:
raise UnsupportedDeviceError(
f"Unsupported device for {config.host}: "
+ f"{config.connection_type.device_family.value}"
+ f"{config.connection_type.device_family.value}",
host=config.host,
)

try:
Expand Down Expand Up @@ -110,7 +111,7 @@ def _perf_log(has_params, perf_type):
_perf_log(True, "update")
return device
elif device_class := get_device_class_from_family(
config.connection_type.device_family.value
config.connection_type.device_family.value, https=config.connection_type.https
):
device = device_class(host=config.host, protocol=protocol)
await device.update()
Expand All @@ -119,7 +120,8 @@ def _perf_log(has_params, perf_type):
else:
raise UnsupportedDeviceError(
f"Unsupported device for {config.host}: "
+ f"{config.connection_type.device_family.value}"
+ f"{config.connection_type.device_family.value}",
host=config.host,
)


Expand Down Expand Up @@ -164,22 +166,26 @@ def get_device_class_from_sys_info(sysinfo: dict[str, Any]) -> type[IotDevice]:
return TYPE_TO_CLASS[_get_device_type_from_sys_info(sysinfo)]


def get_device_class_from_family(device_type: str) -> type[Device] | None:
def get_device_class_from_family(
device_type: str, *, https: bool
) -> type[Device] | None:
"""Return the device class from the type name."""
supported_device_types: dict[str, type[Device]] = {
"SMART.TAPOPLUG": SmartDevice,
"SMART.TAPOBULB": SmartDevice,
"SMART.TAPOSWITCH": SmartDevice,
"SMART.KASAPLUG": SmartDevice,
"SMART.TAPOHUB": SmartDevice,
"SMART.TAPOHUB.HTTPS": SmartCamera,
"SMART.KASAHUB": SmartDevice,
"SMART.KASASWITCH": SmartDevice,
"SMART.IPCAMERA": SmartCamera,
"SMART.IPCAMERA.HTTPS": SmartCamera,
"IOT.SMARTPLUGSWITCH": IotPlug,
"IOT.SMARTBULB": IotBulb,
}
lookup_key = f"{device_type}{'.HTTPS' if https else ''}"
if (
cls := supported_device_types.get(device_type)
cls := supported_device_types.get(lookup_key)
) is None and device_type.startswith("SMART."):
_LOGGER.warning("Unknown SMART device with %s, using SmartDevice", device_type)
cls = SmartDevice
Expand Down
36 changes: 27 additions & 9 deletions kasa/discover.py
8000
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,11 @@ async def try_connect_all(
)
)
and (protocol := get_protocol(config))
and (device_class := get_device_class_from_family(device_family.value))
and (
device_class := get_device_class_from_family(
device_family.value, https=https
)
)
}
for protocol, config in candidates.values():
try:
Expand All @@ -591,7 +595,10 @@ def _get_device_class(info: dict) -> type[Device]:
"""Find SmartDevice subclass for device described by passed data."""
if "result" in info:
discovery_result = DiscoveryResult(**info["result"])
dev_class = get_device_class_from_family(discovery_result.device_type)
https = discovery_result.mgt_encrypt_schm.is_support_https
dev_class = get_device_class_from_family(
discovery_result.device_type, https=https
)
if not dev_class:
raise UnsupportedDeviceError(
"Unknown device type: %s" % discovery_result.device_type,
Expand Down Expand Up @@ -662,7 +669,9 @@ def _get_device_instance(
) from ex
try:
discovery_result = DiscoveryResult(**info["result"])
if discovery_result.encrypt_info:
if (
encrypt_info := discovery_result.encrypt_info
) and encrypt_info.sym_schm == "AES":
Discover._decrypt_discovery_data(discovery_result)
except ValidationError as ex:
if debug_enabled:
Expand All @@ -677,21 +686,23 @@ def _get_device_instance(
pf(data),
)
raise UnsupportedDeviceError(
f"Unable to parse discovery from device: {config.host}: {ex}"
f"Unable to parse discovery from device: {config.host}: {ex}",
host=config.host,
) from ex

type_ = discovery_result.device_type

encrypt_schm = discovery_result.mgt_encrypt_schm
try:
if not (
encrypt_type := discovery_result.mgt_encrypt_schm.encrypt_type
) and (encrypt_info := discovery_result.encrypt_info):
if not (encrypt_type := encrypt_schm.encrypt_type) and (
encrypt_info := discovery_result.encrypt_info
):
encrypt_type = encrypt_info.sym_schm
if not encrypt_type:
raise UnsupportedDeviceError(
f"Unsupported device {config.host} of type {type_} "
+ "with no encryption type",
discovery_result=discovery_result.get_dict(),
host=config.host,
)
config.connection_type = DeviceConnectionParameters.from_values(
type_,
Expand All @@ -704,12 +715,18 @@ def _get_device_instance(
f"Unsupported device {config.host} of type {type_} "
+ f"with encrypt_type {discovery_result.mgt_encrypt_schm.encrypt_type}",
discovery_result=discovery_result.get_dict(),
host=config.host,
) from ex
if (device_class := get_device_class_from_family(type_)) is None:
if (
device_class := get_device_class_from_family(
type_, https=encrypt_schm.is_support_https
)
) is None:
_LOGGER.warning("Got unsupported device type: %s", type_)
raise UnsupportedDeviceError(
f"Unsupported device {config.host} of type {type_}: {info}",
discovery_result=discovery_result.get_dict(),
host=config.host,
)
if (protocol := get_protocol(config)) is None:
_LOGGER.warning(
Expand All @@ -719,6 +736,7 @@ def _get_device_instance(
f"Unsupported encryption scheme {config.host} of "
+ f"type {config.connection_type.to_dict()}: {info}",
discovery_result=discovery_result.get_dict(),
host=config.host,
)

if debug_enabled:
Expand Down
1 change: 1 addition & 0 deletions kasa/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class UnsupportedDeviceError(KasaException):

def __init__(self, *args: Any, **kwargs: Any) -> None:
self.discovery_result = kwargs.get("discovery_result")
self.host = kwargs.get("host")
super().__init__(*args)


Expand Down
28 changes: 26 additions & 2 deletions kasa/tests/discovery_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
DISCOVERY_MOCK_IP = "127.0.0.123"


def _make_unsupported(device_family, encrypt_type):
return {
def _make_unsupported(device_family, encrypt_type, *, omit_keys=None):
if omit_keys is None:
omit_keys = {"encrypt_info": None}
result = {
"result": {
"device_id": "xx",
"owner": "xx",
Expand All @@ -33,16 +35,34 @@ def _make_unsupported(device_family, encrypt_type):
"http_port": 80,
"lv": 2,
},
"encrypt_info": {"data": "", "key": "", "sym_schm": encrypt_type},
},
"error_code": 0,
}
for key, val in omit_keys.items():
if val is None:
result["result"].pop(key)
else:
result["result"][key].pop(val)

return result


UNSUPPORTED_DEVICES = {
"unknown_device_family": _make_unsupported("SMART.TAPOXMASTREE", "AES"),
"wrong_encryption_iot": _make_unsupported("IOT.SMARTPLUGSWITCH", "AES"),
"wrong_encryption_smart": _make_unsupported("SMART.TAPOBULB", "IOT"),
"unknown_encryption": _make_unsupported("IOT.SMARTPLUGSWITCH", "FOO"),
"missing_encrypt_type": _make_unsupported(
"SMART.TAPOBULB",
"FOO",
omit_keys={"mgt_encrypt_schm": "encrypt_type", "encrypt_info": None},
),
"unable_to_parse": _make_unsupported(
"SMART.TAPOBULB",
"FOO",
omit_keys={"mgt_encrypt_schm": None},
),
}


Expand Down Expand Up @@ -90,6 +110,7 @@ class _DiscoveryMock:
query_data: dict
device_type: str
encrypt_type: str
https: bool
login_version: int | None = None
port_override: int | None = None

Expand All @@ -110,6 +131,7 @@ def _datagram(self) -> bytes:
"encrypt_type"
]
login_version = fixture_data["discovery_result"]["mgt_encrypt_schm"].get("lv")
https = fixture_data["discovery_resul F438 t"]["mgt_encrypt_schm"]["is_support_https"]
dm = _DiscoveryMock(
ip,
80,
Expand All @@ -118,6 +140,7 @@ def _datagram(self) -> bytes:
fixture_data,
device_type,
encrypt_type,
https,
login_version,
)
else:
Expand All @@ -134,6 +157,7 @@ def _datagram(self) -> bytes:
fixture_data,
device_type,
encrypt_type,
False,
login_version,
)

Expand Down
1 change: 0 additions & 1 deletion kasa/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,6 @@ async def test_discover_unsupported(unsupported_device_info, runner):
)
assert res.exit_code == 0
assert "== Unsupported device ==" in res.output
assert "== Discovery Result ==" in res.output


async def test_host_unsupported(unsupported_device_info, runner):
Expand Down
2 changes: 1 addition & 1 deletion kasa/tests/test_device_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,5 @@ async def test_device_class_from_unknown_family(caplog):
"""Verify that unknown SMART devices yield a warning and fallback to SmartDevice."""
dummy_name = "SMART.foo"
with caplog.at_level(logging.WARNING):
assert get_device_class_from_family(dummy_name) == SmartDevice
assert get_device_class_from_family(dummy_name, https=False) == SmartDevice
assert f"Unknown SMART device with {dummy_name}" in caplog.text
6 changes: 4 additions & 2 deletions kasa/tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,12 +658,14 @@ async def test_discovery_decryption():
async def test_discover_try_connect_all(discovery_mock, mocker):
"""Test that device update is called on main."""
if "result" in discovery_mock.discovery_data:
dev_class = get_device_class_from_family(discovery_mock.device_type)
dev_class = get_device_class_from_family(
discovery_mock.device_type, https=discovery_mock.https
)
cparams = DeviceConnectionParameters.from_values(
discovery_mock.device_type,
discovery_mock.encrypt_type,
discovery_mock.login_version,
False,
discovery_mock.https,
)
protocol = get_protocol(
DeviceConfig(discovery_mock.ip, connection_type=cparams)
Expand Down
Loading
0