mirror of
https://github.com/python-kasa/python-kasa.git
synced 2026-02-27 21:29:57 +00:00
New Wi-Fi handling for SMARTCAM devices (#1639)
Some checks failed
CI / Perform Lint Checks (3.13) (push) Has been cancelled
CI / Python 3.11 on macos-latest (push) Has been cancelled
CI / Python 3.12 on macos-latest (push) Has been cancelled
CI / Python 3.13 on macos-latest (push) Has been cancelled
CI / Python 3.11 on ubuntu-latest (push) Has been cancelled
CI / Python 3.12 on ubuntu-latest (push) Has been cancelled
CI / Python 3.13 on ubuntu-latest (push) Has been cancelled
CI / Python 3.11 on windows-latest (push) Has been cancelled
CI / Python 3.12 on windows-latest (push) Has been cancelled
CI / Python 3.13 on windows-latest (push) Has been cancelled
CodeQL Checks / Analyze (python) (push) Has been cancelled
Some checks failed
CI / Perform Lint Checks (3.13) (push) Has been cancelled
CI / Python 3.11 on macos-latest (push) Has been cancelled
CI / Python 3.12 on macos-latest (push) Has been cancelled
CI / Python 3.13 on macos-latest (push) Has been cancelled
CI / Python 3.11 on ubuntu-latest (push) Has been cancelled
CI / Python 3.12 on ubuntu-latest (push) Has been cancelled
CI / Python 3.13 on ubuntu-latest (push) Has been cancelled
CI / Python 3.11 on windows-latest (push) Has been cancelled
CI / Python 3.12 on windows-latest (push) Has been cancelled
CI / Python 3.13 on windows-latest (push) Has been cancelled
CodeQL Checks / Analyze (python) (push) Has been cancelled
Updated scanning and joining Wi-Fi for SMARTCAM devices that may use a newer connection process.
This commit is contained in:
@@ -256,6 +256,52 @@ class FakeSmartCamTransport(BaseTransport):
|
||||
method = request_dict["method"]
|
||||
|
||||
info = self.info
|
||||
if method == "connectAp":
|
||||
if self.verbatim:
|
||||
return {"error_code": -1}
|
||||
return {"result": {}, "error_code": 0}
|
||||
if method == "scanApList":
|
||||
if method in info:
|
||||
result = self._get_method_from_info(method, request_dict.get("params"))
|
||||
if not self.verbatim:
|
||||
scan = (
|
||||
result.get("result", {}).get("onboarding", {}).get("scan", {})
|
||||
)
|
||||
ap_list = scan.get("ap_list")
|
||||
if isinstance(ap_list, list) and not any(
|
||||
ap.get("ssid") == "FOOBAR" for ap in ap_list
|
||||
):
|
||||
ap_list.append(
|
||||
{
|
||||
"ssid": "FOOBAR",
|
||||
"auth": 3,
|
||||
"encryption": 3,
|
||||
"rssi": -40,
|
||||
"bssid": "00:00:00:00:00:00",
|
||||
}
|
||||
)
|
||||
return result
|
||||
if self.verbatim:
|
||||
return {"error_code": -1}
|
||||
return {
|
||||
"result": {
|
||||
"onboarding": {
|
||||
"scan": {
|
||||
"publicKey": "",
|
||||
"ap_list": [
|
||||
{
|
||||
"ssid": "FOOBAR",
|
||||
"auth": 3,
|
||||
"encryption": 3,
|
||||
"rssi": -40,
|
||||
"bssid": "00:00:00:00:00:00",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"error_code": 0,
|
||||
}
|
||||
if method == "controlChild":
|
||||
return await self._handle_control_child(
|
||||
request_dict["params"]["childControl"]
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
|
||||
from kasa import Device, DeviceType, Module
|
||||
from kasa.exceptions import AuthenticationError, DeviceError, KasaException
|
||||
from kasa.smartcam import SmartCamDevice
|
||||
|
||||
from ..conftest import device_smartcam, hub_smartcam
|
||||
|
||||
@@ -34,7 +38,7 @@ async def test_state(dev: Device):
|
||||
|
||||
|
||||
@device_smartcam
|
||||
async def test_alias(dev):
|
||||
async def test_alias(dev: Device):
|
||||
test_alias = "TEST1234"
|
||||
original = dev.alias
|
||||
|
||||
@@ -49,7 +53,7 @@ async def test_alias(dev):
|
||||
|
||||
|
||||
@hub_smartcam
|
||||
async def test_hub(dev):
|
||||
async def test_hub(dev: Device):
|
||||
assert dev.children
|
||||
for child in dev.children:
|
||||
assert child.modules
|
||||
@@ -60,6 +64,95 @@ async def test_hub(dev):
|
||||
assert child.device_id
|
||||
|
||||
|
||||
@device_smartcam
|
||||
async def test_wifi_scan(dev: SmartCamDevice):
|
||||
fake_scan_data = {
|
||||
"scanApList": {
|
||||
"onboarding": {
|
||||
"scan": {
|
||||
"publicKey": base64.b64encode(b"fakekey").decode(),
|
||||
"ap_list": [
|
||||
{
|
||||
"ssid": "TestSSID",
|
||||
"auth": "WPA2",
|
||||
"encryption": "AES",
|
||||
"rssi": -40,
|
||||
"bssid": "00:11:22:33:44:55",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
with patch.object(dev, "_query_helper", AsyncMock(return_value=fake_scan_data)):
|
||||
networks = await dev.wifi_scan()
|
||||
assert len(networks) == 1
|
||||
net = networks[0]
|
||||
assert net.ssid == "TestSSID"
|
||||
assert net.auth == "WPA2"
|
||||
assert net.encryption == "AES"
|
||||
assert net.rssi == -40
|
||||
assert net.bssid == "00:11:22:33:44:55"
|
||||
assert dev._public_key == base64.b64encode(b"fakekey").decode()
|
||||
|
||||
|
||||
@device_smartcam
|
||||
async def test_wifi_join_success_and_errors(dev: SmartCamDevice):
|
||||
dev._networks = [
|
||||
type(
|
||||
"WifiNetwork",
|
||||
(),
|
||||
{
|
||||
"ssid": "TestSSID",
|
||||
"auth": "WPA2",
|
||||
"encryption": "AES",
|
||||
"rssi": -40,
|
||||
"bssid": "00:11:22:33:44:55",
|
||||
},
|
||||
)()
|
||||
]
|
||||
with patch.object(type(dev), "credentials", new_callable=PropertyMock) as cred_mock:
|
||||
cred_mock.return_value = object()
|
||||
with patch.object(dev.protocol, "query", AsyncMock(return_value={})):
|
||||
result = await dev.wifi_join("TestSSID", "password123")
|
||||
assert isinstance(result, dict)
|
||||
cred_mock.return_value = None
|
||||
with pytest.raises(AuthenticationError):
|
||||
await dev.wifi_join("TestSSID", "password123")
|
||||
cred_mock.return_value = object()
|
||||
dev._networks = []
|
||||
with (
|
||||
patch.object(dev, "wifi_scan", AsyncMock(return_value=[])),
|
||||
pytest.raises(DeviceError),
|
||||
):
|
||||
await dev.wifi_join("TestSSID", "password123")
|
||||
dev._networks = [
|
||||
type(
|
||||
"WifiNetwork",
|
||||
(),
|
||||
{
|
||||
"ssid": "TestSSID",
|
||||
"auth": "WPA2",
|
||||
"encryption": "AES",
|
||||
"rssi": -40,
|
||||
"bssid": "00:11:22:33:44:55",
|
||||
},
|
||||
)()
|
||||
]
|
||||
with (
|
||||
patch.object(
|
||||
dev.protocol, "query", AsyncMock(side_effect=DeviceError("fail"))
|
||||
),
|
||||
pytest.raises(DeviceError),
|
||||
):
|
||||
await dev.wifi_join("TestSSID", "password123")
|
||||
with patch.object(
|
||||
dev.protocol, "query", AsyncMock(side_effect=KasaException("fail"))
|
||||
):
|
||||
result = await dev.wifi_join("TestSSID", "password123")
|
||||
assert result == {}
|
||||
|
||||
|
||||
@device_smartcam
|
||||
async def test_device_time(dev: Device, freezer: FrozenDateTimeFactory):
|
||||
"""Test a child device gets the time from it's parent module."""
|
||||
@@ -69,3 +162,36 @@ async def test_device_time(dev: Device, freezer: FrozenDateTimeFactory):
|
||||
await module.set_time(fallback_time)
|
||||
await dev.update()
|
||||
assert dev.time == fallback_time
|
||||
|
||||
|
||||
@device_smartcam
|
||||
async def test_wifi_join_typeerror_on_non_rsa_key(dev: SmartCamDevice):
|
||||
dev._networks = [
|
||||
type(
|
||||
"WifiNetwork",
|
||||
(),
|
||||
{
|
||||
"ssid": "TestSSID",
|
||||
"auth": "WPA2",
|
||||
"encryption": "AES",
|
||||
"rssi": -40,
|
||||
"bssid": "00:11:22:33:44:55",
|
||||
},
|
||||
)()
|
||||
]
|
||||
with patch.object(type(dev), "credentials", new_callable=PropertyMock) as cred_mock:
|
||||
cred_mock.return_value = object()
|
||||
with (
|
||||
patch(
|
||||
"cryptography.hazmat.primitives.serialization.load_der_public_key",
|
||||
return_value=object(),
|
||||
),
|
||||
patch(
|
||||
"kasa.smartcam.smartcamdevice.RSAPublicKey",
|
||||
new=type("FakeRSA", (), {}),
|
||||
),
|
||||
pytest.raises(
|
||||
TypeError, match="Loaded public key is not an RSA public key"
|
||||
),
|
||||
):
|
||||
await dev.wifi_join("TestSSID", "password123")
|
||||
|
||||
@@ -49,10 +49,13 @@ from kasa.smart import SmartDevice
|
||||
from kasa.smartcam import SmartCamDevice
|
||||
|
||||
from .conftest import (
|
||||
device_iot,
|
||||
device_smart,
|
||||
device_smartcam,
|
||||
get_device_for_fixture_protocol,
|
||||
handle_turn_on,
|
||||
new_discovery,
|
||||
parametrize_combine,
|
||||
turn_on,
|
||||
)
|
||||
|
||||
@@ -359,12 +362,47 @@ async def test_wifi_scan(dev, runner):
|
||||
assert re.search(r"Found [\d]+ wifi networks!", res.output)
|
||||
|
||||
|
||||
@device_smart
|
||||
@parametrize_combine([device_smart, device_iot])
|
||||
async def test_wifi_join(dev, mocker, runner):
|
||||
update = mocker.patch.object(dev, "update")
|
||||
res = await runner.invoke(
|
||||
wifi,
|
||||
["join", "FOOBAR", "--keytype", "wpa_psk", "--password", "foobar"],
|
||||
["join", "FOOBAR", "--keytype", "3", "--password", "foobar"],
|
||||
obj=dev,
|
||||
)
|
||||
|
||||
# Make sure that update was not called for wifi
|
||||
with pytest.raises(AssertionError):
|
||||
update.assert_called()
|
||||
|
||||
assert res.exit_code == 0
|
||||
assert "Asking the device to connect to FOOBAR" in res.output
|
||||
|
||||
|
||||
@parametrize_combine([device_smart, device_iot])
|
||||
async def test_wifi_join_missing_keytype(dev, mocker, runner):
|
||||
"""Test that missing keytype raises KasaException and CLI echoes the message."""
|
||||
update = mocker.patch.object(dev, "update")
|
||||
res = await runner.invoke(
|
||||
wifi,
|
||||
["join", "FOOBAR", "--password", "foobar"],
|
||||
obj=dev,
|
||||
)
|
||||
|
||||
# Make sure that update was not called for wifi
|
||||
with pytest.raises(AssertionError):
|
||||
update.assert_called()
|
||||
|
||||
assert res.exit_code == 0
|
||||
assert "KeyType is required for this device." in res.output
|
||||
|
||||
|
||||
@device_smartcam
|
||||
async def test_wifi_join_smartcam(dev, mocker, runner):
|
||||
update = mocker.patch.object(dev, "update")
|
||||
res = await runner.invoke(
|
||||
wifi,
|
||||
["join", "FOOBAR", "--password", "foobar"],
|
||||
obj=dev,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user