diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index 541d128d..7316809a 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -40,7 +40,7 @@ from kasa.device_factory import get_protocol from kasa.deviceconfig import DeviceEncryptionType, DeviceFamily from kasa.discover import DiscoveryResult from kasa.exceptions import SmartErrorCode -from kasa.experimental.smartcameraprotocol import ( +from kasa.protocols.smartcameraprotocol import ( SmartCameraProtocol, _ChildCameraProtocolWrapper, ) diff --git a/kasa/device_factory.py b/kasa/device_factory.py index 887f4b68..d32f73a0 100755 --- a/kasa/device_factory.py +++ b/kasa/device_factory.py @@ -10,9 +10,6 @@ from .device import Device from .device_type import DeviceType from .deviceconfig import DeviceConfig from .exceptions import KasaException, UnsupportedDeviceError -from .experimental.smartcamera import SmartCamera -from .experimental.smartcameraprotocol import SmartCameraProtocol -from .experimental.sslaestransport import SslAesTransport from .iot import ( IotBulb, IotDevice, @@ -27,7 +24,9 @@ from .protocols import ( IotProtocol, SmartProtocol, ) +from .protocols.smartcameraprotocol import SmartCameraProtocol from .smart import SmartDevice +from .smartcamera.smartcamera import SmartCamera from .transports import ( AesTransport, BaseTransport, @@ -35,6 +34,7 @@ from .transports import ( KlapTransportV2, XorTransport, ) +from .transports.sslaestransport import SslAesTransport _LOGGER = logging.getLogger(__name__) @@ -217,12 +217,9 @@ def get_protocol( "IOT.KLAP": (IotProtocol, KlapTransport), "SMART.AES": (SmartProtocol, AesTransport), "SMART.KLAP": (SmartProtocol, KlapTransportV2), + "SMART.AES.HTTPS": (SmartCameraProtocol, SslAesTransport), } if not (prot_tran_cls := supported_device_protocols.get(protocol_transport_key)): - from .experimental import Experimental - - if Experimental.enabled() and protocol_transport_key == "SMART.AES.HTTPS": - prot_tran_cls = (SmartCameraProtocol, SslAesTransport) - else: - return None - return prot_tran_cls[0](transport=prot_tran_cls[1](config=config)) + return None + protocol_cls, transport_cls = prot_tran_cls + return protocol_cls(transport=transport_cls(config=config)) diff --git a/kasa/module.py b/kasa/module.py index c4e9f9a1..edd26477 100644 --- a/kasa/module.py +++ b/kasa/module.py @@ -55,9 +55,9 @@ from .modulemapping import ModuleName if TYPE_CHECKING: from . import interfaces from .device import Device - from .experimental import modules as experimental from .iot import modules as iot from .smart import modules as smart + from .smartcamera import modules as smartcamera _LOGGER = logging.getLogger(__name__) @@ -133,7 +133,7 @@ class Module(ABC): TriggerLogs: Final[ModuleName[smart.TriggerLogs]] = ModuleName("TriggerLogs") # SMARTCAMERA only modules - Camera: Final[ModuleName[experimental.Camera]] = ModuleName("Camera") + Camera: Final[ModuleName[smartcamera.Camera]] = ModuleName("Camera") def __init__(self, device: Device, module: str) -> None: self._device = device diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/protocols/smartcameraprotocol.py similarity index 99% rename from kasa/experimental/smartcameraprotocol.py rename to kasa/protocols/smartcameraprotocol.py index 4b9489ae..57f78d40 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/protocols/smartcameraprotocol.py @@ -14,12 +14,12 @@ from ..exceptions import ( _RetryableError, ) from ..json import dumps as json_dumps -from ..protocols import SmartProtocol -from .sslaestransport import ( +from ..transports.sslaestransport import ( SMART_AUTHENTICATION_ERRORS, SMART_RETRYABLE_ERRORS, SmartErrorCode, ) +from . import SmartProtocol _LOGGER = logging.getLogger(__name__) diff --git a/kasa/smartcamera/__init__.py b/kasa/smartcamera/__init__.py new file mode 100644 index 00000000..0d6052ea --- /dev/null +++ b/kasa/smartcamera/__init__.py @@ -0,0 +1,5 @@ +"""Package for supporting tapo-branded cameras.""" + +from .smartcamera import SmartCamera + +__all__ = ["SmartCamera"] diff --git a/kasa/experimental/modules/__init__.py b/kasa/smartcamera/modules/__init__.py similarity index 100% rename from kasa/experimental/modules/__init__.py rename to kasa/smartcamera/modules/__init__.py diff --git a/kasa/experimental/modules/camera.py b/kasa/smartcamera/modules/camera.py similarity index 100% rename from kasa/experimental/modules/camera.py rename to kasa/smartcamera/modules/camera.py diff --git a/kasa/experimental/modules/childdevice.py b/kasa/smartcamera/modules/childdevice.py similarity index 100% rename from kasa/experimental/modules/childdevice.py rename to kasa/smartcamera/modules/childdevice.py diff --git a/kasa/experimental/modules/device.py b/kasa/smartcamera/modules/device.py similarity index 100% rename from kasa/experimental/modules/device.py rename to kasa/smartcamera/modules/device.py diff --git a/kasa/experimental/modules/led.py b/kasa/smartcamera/modules/led.py similarity index 100% rename from kasa/experimental/modules/led.py rename to kasa/smartcamera/modules/led.py diff --git a/kasa/experimental/modules/time.py b/kasa/smartcamera/modules/time.py similarity index 100% rename from kasa/experimental/modules/time.py rename to kasa/smartcamera/modules/time.py diff --git a/kasa/experimental/smartcamera.py b/kasa/smartcamera/smartcamera.py similarity index 98% rename from kasa/experimental/smartcamera.py rename to kasa/smartcamera/smartcamera.py index feadf87b..d94d35d3 100644 --- a/kasa/experimental/smartcamera.py +++ b/kasa/smartcamera/smartcamera.py @@ -7,11 +7,10 @@ from typing import Any from ..device_type import DeviceType from ..module import Module +from ..protocols.smartcameraprotocol import _ChildCameraProtocolWrapper from ..smart import SmartChildDevice, SmartDevice -from .modules.childdevice import ChildDevice -from .modules.device import DeviceModule +from .modules import ChildDevice, DeviceModule from .smartcameramodule import SmartCameraModule -from .smartcameraprotocol import _ChildCameraProtocolWrapper _LOGGER = logging.getLogger(__name__) diff --git a/kasa/experimental/smartcameramodule.py b/kasa/smartcamera/smartcameramodule.py similarity index 100% rename from kasa/experimental/smartcameramodule.py rename to kasa/smartcamera/smartcameramodule.py diff --git a/kasa/experimental/sslaestransport.py b/kasa/transports/sslaestransport.py similarity index 99% rename from kasa/experimental/sslaestransport.py rename to kasa/transports/sslaestransport.py index 3dff225a..4f3cd4cc 100644 --- a/kasa/experimental/sslaestransport.py +++ b/kasa/transports/sslaestransport.py @@ -27,7 +27,7 @@ from ..exceptions import ( from ..httpclient import HttpClient from ..json import dumps as json_dumps from ..json import loads as json_loads -from ..transports import AesEncyptionSession, BaseTransport +from . import AesEncyptionSession, BaseTransport _LOGGER = logging.getLogger(__name__) diff --git a/tests/device_fixtures.py b/tests/device_fixtures.py index 4d335d5c..359d7164 100644 --- a/tests/device_fixtures.py +++ b/tests/device_fixtures.py @@ -11,9 +11,9 @@ from kasa import ( DeviceType, Discover, ) -from kasa.experimental.smartcamera import SmartCamera from kasa.iot import IotBulb, IotDimmer, IotLightStrip, IotPlug, IotStrip, IotWallSwitch from kasa.smart import SmartDevice +from kasa.smartcamera.smartcamera import SmartCamera from .fakeprotocol_iot import FakeIotProtocol from .fakeprotocol_smart import FakeSmartProtocol diff --git a/tests/discovery_fixtures.py b/tests/discovery_fixtures.py index e69a8b73..e272cef8 100644 --- a/tests/discovery_fixtures.py +++ b/tests/discovery_fixtures.py @@ -10,6 +10,7 @@ from kasa.transports.xortransport import XorEncryption from .fakeprotocol_iot import FakeIotProtocol from .fakeprotocol_smart import FakeSmartProtocol, FakeSmartTransport +from .fakeprotocol_smartcamera import FakeSmartCameraProtocol from .fixtureinfo import FixtureInfo, filter_fixtures, idgenerator DISCOVERY_MOCK_IP = "127.0.0.123" @@ -126,12 +127,14 @@ def create_discovery_mock(ip: str, fixture_data: dict): if "discovery_result" in fixture_data: discovery_data = {"result": fixture_data["discovery_result"].copy()} - device_type = fixture_data["discovery_result"]["device_type"] - encrypt_type = fixture_data["discovery_result"]["mgt_encrypt_schm"][ - "encrypt_type" - ] - login_version = fixture_data["discovery_result"]["mgt_encrypt_schm"].get("lv") - https = fixture_data["discovery_result"]["mgt_encrypt_schm"]["is_support_https"] + discovery_result = fixture_data["discovery_result"] + device_type = discovery_result["device_type"] + encrypt_type = discovery_result["mgt_encrypt_schm"].get( + "encrypt_type", discovery_result.get("encrypt_info", {}).get("sym_schm") + ) + + login_version = discovery_result["mgt_encrypt_schm"].get("lv") + https = discovery_result["mgt_encrypt_schm"]["is_support_https"] dm = _DiscoveryMock( ip, 80, @@ -172,7 +175,9 @@ def patch_discovery(fixture_infos: dict[str, FixtureInfo], mocker): } protos = { ip: FakeSmartProtocol(fixture_info.data, fixture_info.name) - if "SMART" in fixture_info.protocol + if fixture_info.protocol in {"SMART", "SMART.CHILD"} + else FakeSmartCameraProtocol(fixture_info.data, fixture_info.name) + if fixture_info.protocol in {"SMARTCAMERA", "SMARTCAMERA.CHILD"} else FakeIotProtocol(fixture_info.data, fixture_info.name) for ip, fixture_info in fixture_infos.items() } @@ -197,7 +202,9 @@ def patch_discovery(fixture_infos: dict[str, FixtureInfo], mocker): # update the protos for any host testing or the test overriding the first ip protos[host] = ( FakeSmartProtocol(fixture_info.data, fixture_info.name) - if "SMART" in fixture_info.protocol + if fixture_info.protocol in {"SMART", "SMART.CHILD"} + else FakeSmartCameraProtocol(fixture_info.data, fixture_info.name) + if fixture_info.protocol in {"SMARTCAMERA", "SMARTCAMERA.CHILD"} else FakeIotProtocol(fixture_info.data, fixture_info.name) ) port = ( diff --git a/tests/fakeprotocol_smartcamera.py b/tests/fakeprotocol_smartcamera.py index 7ff0bab2..d6751f7d 100644 --- a/tests/fakeprotocol_smartcamera.py +++ b/tests/fakeprotocol_smartcamera.py @@ -4,7 +4,7 @@ import copy from json import loads as json_loads from kasa import Credentials, DeviceConfig, SmartProtocol -from kasa.experimental.smartcameraprotocol import SmartCameraProtocol +from kasa.protocols.smartcameraprotocol import SmartCameraProtocol from kasa.transports.basetransport import BaseTransport from .fakeprotocol_smart import FakeSmartTransport @@ -136,6 +136,12 @@ class FakeSmartCameraTransport(BaseTransport): "basic", "zone_id", ], + ("led", "config", "enabled"): [ + "getLedStatus", + "led", + "config", + "enabled", + ], } async def _send_request(self, request_dict: dict): diff --git a/tests/fixtureinfo.py b/tests/fixtureinfo.py index cb75b423..7d7607ef 100644 --- a/tests/fixtureinfo.py +++ b/tests/fixtureinfo.py @@ -8,8 +8,8 @@ from typing import Iterable, NamedTuple from kasa.device_factory import _get_device_type_from_sys_info from kasa.device_type import DeviceType -from kasa.experimental.smartcamera import SmartCamera from kasa.smart.smartdevice import SmartDevice +from kasa.smartcamera.smartcamera import SmartCamera class FixtureInfo(NamedTuple): @@ -179,7 +179,7 @@ def filter_fixtures( filtered = [] if protocol_filter is None: - protocol_filter = {"IOT", "SMART"} + protocol_filter = {"IOT", "SMART", "SMARTCAMERA"} for fixture_data in fixture_list: if data_root_filter and data_root_filter not in fixture_data.data: continue diff --git a/tests/test_cli.py b/tests/test_cli.py index 24fc916f..1ab0dc31 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -43,6 +43,7 @@ from kasa.cli.wifi import wifi from kasa.discover import Discover, DiscoveryResult from kasa.iot import IotDevice from kasa.smart import SmartDevice +from kasa.smartcamera import SmartCamera from .conftest import ( device_smart, @@ -178,6 +179,9 @@ async def test_state(dev, turn_on, runner): @turn_on async def test_toggle(dev, turn_on, runner): + if isinstance(dev, SmartCamera) and dev.device_type == DeviceType.Hub: + pytest.skip(reason="Hub cannot toggle state") + await handle_turn_on(dev, turn_on) await dev.update() assert dev.is_on == turn_on @@ -208,7 +212,9 @@ async def test_raw_command(dev, mocker, runner): update = mocker.patch.object(dev, "update") from kasa.smart import SmartDevice - if isinstance(dev, SmartDevice): + if isinstance(dev, SmartCamera): + params = ["na", "getDeviceInfo"] + elif isinstance(dev, SmartDevice): params = ["na", "get_device_info"] else: params = ["system", "get_sysinfo"] diff --git a/tests/test_device_factory.py b/tests/test_device_factory.py index 0042d6e2..4f71888b 100644 --- a/tests/test_device_factory.py +++ b/tests/test_device_factory.py @@ -19,6 +19,7 @@ from kasa import ( ) from kasa.device_factory import ( Device, + SmartCamera, SmartDevice, _get_device_type_from_sys_info, connect, @@ -177,7 +178,9 @@ async def test_connect_http_client(discovery_mock, mocker): async def test_device_types(dev: Device): await dev.update() - if isinstance(dev, SmartDevice): + if isinstance(dev, SmartCamera): + res = SmartCamera._get_device_type_from_sysinfo(dev.sys_info) + elif isinstance(dev, SmartDevice): assert dev._discovery_info device_type = cast(str, dev._discovery_info["result"]["device_type"]) res = SmartDevice._get_device_type_from_components( diff --git a/tests/test_sslaestransport.py b/tests/test_sslaestransport.py index 2e13f153..0d8fac9c 100644 --- a/tests/test_sslaestransport.py +++ b/tests/test_sslaestransport.py @@ -18,13 +18,13 @@ from kasa.exceptions import ( KasaException, SmartErrorCode, ) -from kasa.experimental.sslaestransport import ( +from kasa.httpclient import HttpClient +from kasa.transports.aestransport import AesEncyptionSession +from kasa.transports.sslaestransport import ( SslAesTransport, TransportState, _sha256_hash, ) -from kasa.httpclient import HttpClient -from kasa.transports.aestransport import AesEncyptionSession # Transport tests are not designed for real devices pytestmark = [pytest.mark.requires_dummy]