Skip to content
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
6 changes: 3 additions & 3 deletions SUPPORTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
### Plugs

- **P100**
- Hardware: 1.0.0 / Firmware: 1.1.3
- Hardware: 1.0.0 / Firmware: 1.3.7
- Hardware: 1.0.0 / Firmware: 1.4.0
- Hardware: 1.0.0 (US) / Firmware: 1.1.3
- Hardware: 1.0.0 (US) / Firmware: 1.3.7
- Hardware: 1.0.0 (US) / Firmware: 1.4.0
- **P110**
- Hardware: 1.0 (EU) / Firmware: 1.0.7
- Hardware: 1.0 (EU) / Firmware: 1.2.3
Expand Down
58 changes: 26 additions & 32 deletions devtools/dump_devinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from collections import defaultdict, namedtuple
from pathlib import Path
from pprint import pprint
from typing import Any

import asyncclick as click

Expand All @@ -40,12 +41,13 @@
from kasa.deviceconfig import DeviceEncryptionType, DeviceFamily
from kasa.discover import DiscoveryResult
from kasa.exceptions import SmartErrorCode
from kasa.protocols import IotProtocol
from kasa.protocols.smartcameraprotocol import (
SmartCameraProtocol,
_ChildCameraProtocolWrapper,
)
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
from kasa.smart import SmartChildDevice
from kasa.smart import SmartChildDevice, SmartDevice
from kasa.smartcamera import SmartCamera

Call = namedtuple("Call", "module method")
Expand Down Expand Up @@ -389,7 +391,9 @@ async def cli(
)


async def get_legacy_fixture(protocol, *, discovery_info):
async def get_legacy_fixture(
protocol: IotProtocol, *, discovery_info: dict[str, Any] | None
) -> FixtureResult:
"""Get fixture for legacy IOT style protocol."""
items = [
Call(module="system", method="get_sysinfo"),
Expand Down Expand Up @@ -422,8 +426,8 @@ async def get_legacy_fixture(protocol, *, discovery_info):
finally:
await protocol.close()

final_query = defaultdict(defaultdict)
final = defaultdict(defaultdict)
final_query: dict = defaultdict(defaultdict)
final: dict = defaultdict(defaultdict)
for succ, resp in successes:
final_query[succ.module][succ.method] = {}
final[succ.module][succ.method] = resp
Expand All @@ -433,16 +437,14 @@ async def get_legacy_fixture(protocol, *, discovery_info):
try:
final = await protocol.query(final_query)
except Exception as ex:
_echo_error(f"Unable to query all successes at once: {ex}", bold=True, fg="red")
_echo_error(f"Unable to query all successes at once: {ex}")
finally:
await protocol.close()
if discovery_info and not discovery_info.get("system"):
# Need to recreate a DiscoverResult here because we don't want the aliases
# in the fixture, we want the actual field names as returned by the device.
dr = DiscoveryResult.from_dict(protocol._discovery_info)
final["discovery_result"] = dr.dict(
by_alias=False, exclude_unset=True, exclude_none=True, exclude_defaults=True
)
dr = DiscoveryResult.from_dict(discovery_info)
final["discovery_result"] = dr.to_dict()

click.echo("Got %s successes" % len(successes))
click.echo(click.style("## device info file ##", bold=True))
Expand Down Expand Up @@ -817,23 +819,21 @@ async def get_smart_test_calls(protocol: SmartProtocol):

def get_smart_child_fixture(response):
"""Get a seperate fixture for the child device."""
info = response["get_device_info"]
hw_version = info["hw_ver"]
sw_version = info["fw_ver"]
sw_version = sw_version.split(" ", maxsplit=1)[0]
model = info["model"]
if region := info.get("specs"):
model += f"({region})"

save_filename = f"{model}_{hw_version}_{sw_version}.json"
model_info = SmartDevice._get_device_info(response, None)
hw_version = model_info.hardware_version
fw_version = model_info.firmware_version
model = model_info.long_name
if model_info.region is not None:
model = f"{model}({model_info.region})"
save_filename = f"{model}_{hw_version}_{fw_version}.json"
return FixtureResult(
filename=save_filename, folder=SMART_CHILD_FOLDER, data=response
)


async def get_smart_fixtures(
protocol: SmartProtocol, *, discovery_info=None, batch_size: int
):
protocol: SmartProtocol, *, discovery_info: dict[str, Any] | None, batch_size: int
) -> list[FixtureResult]:
"""Get fixture for new TAPO style protocol."""
if isinstance(protocol, SmartCameraProtocol):
test_calls, successes = await get_smart_camera_test_calls(protocol)
Expand Down Expand Up @@ -964,23 +964,17 @@ async def get_smart_fixtures(

if "get_device_info" in final:
# smart protocol
hw_version = final["get_device_info"]["hw_ver"]
sw_version = final["get_device_info"]["fw_ver"]
if discovery_info:
model = discovery_info["device_model"]
else:
model = final["get_device_info"]["model"] + "(XX)"
sw_version = sw_version.split(" ", maxsplit=1)[0]
model_info = SmartDevice._get_device_info(final, discovery_info)
copy_folder = SMART_FOLDER
else:
# smart camera protocol
model_info = SmartCamera._get_device_info(final, discovery_info)
model = model_info.long_name
hw_version = model_info.hardware_version
sw_version = model_info.firmare_version
if model_info.region is not None:
model = f"{model}({model_info.region})"
copy_folder = SMARTCAMERA_FOLDER
hw_version = model_info.hardware_version
sw_version = model_info.firmware_version
model = model_info.long_name
if model_info.region is not None:
model = f"{model}({model_info.region})"

save_filename = f"{model}_{hw_version}_{sw_version}.json"

Expand Down
83 changes: 18 additions & 65 deletions devtools/generate_supported.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
#!/usr/bin/env python
"""Script that checks supported devices and updates README.md and SUPPORTED.md."""

from __future__ import annotations

import json
import os
import sys
from pathlib import Path
from string import Template
from typing import NamedTuple
from typing import Any, NamedTuple

from kasa.device_factory import _get_device_type_from_sys_info
from kasa.device_type import DeviceType
from kasa.iot import IotDevice
from kasa.smart import SmartDevice
from kasa.smartcamera import SmartCamera


class SupportedVersion(NamedTuple):
"""Supported version."""

region: str
region: str | None
hw: str
fw: str
auth: bool
Expand Down Expand Up @@ -45,6 +47,7 @@ class SupportedVersion(NamedTuple):

IOT_FOLDER = "tests/fixtures/"
SMART_FOLDER = "tests/fixtures/smart/"
SMART_CHILD_FOLDER = "tests/fixtures/smart/child"
SMARTCAMERA_FOLDER = "tests/fixtures/smartcamera/"


Expand All @@ -59,9 +62,10 @@ def generate_supported(args):

supported = {"kasa": {}, "tapo": {}}

_get_iot_supported(supported)
_get_smart_supported(supported)
_get_smartcamera_supported(supported)
_get_supported_devices(supported, IOT_FOLDER, IotDevice)
_get_supported_devices(supported, SMART_FOLDER, SmartDevice)
_get_supported_devices(supported, SMART_CHILD_FOLDER, SmartDevice)
_get_supported_devices(supported, SMARTCAMERA_FOLDER, SmartCamera)

readme_updated = _update_supported_file(
README_FILENAME, _supported_summary(supported), print_diffs
Expand Down Expand Up @@ -201,49 +205,16 @@ def _supported_text(
return brands


def _get_smart_supported(supported):
for file in Path(SMART_FOLDER).glob("**/*.json"):
with file.open() as f:
fixture_data = json.load(f)

if "discovery_result" in fixture_data:
model, _, region = fixture_data["discovery_result"][
"device_model"
].partition("(")
device_type = fixture_data["discovery_result"]["device_type"]
else: # child devices of hubs do not have discovery result
model = fixture_data["get_device_info"]["model"]
region = fixture_data["get_device_info"].get("specs")
device_type = fixture_data["get_device_info"]["type"]
# P100 doesn't have region HW
region = region.replace(")", "") if region else ""

_protocol, devicetype = device_type.split(".")
brand = devicetype[:4].lower()
components = [
component["id"]
for component in fixture_data["component_nego"]["component_list"]
]
dt = SmartDevice._get_device_type_from_components(components, device_type)
supported_type = DEVICE_TYPE_TO_PRODUCT_GROUP[dt]

hw_version = fixture_data["get_device_info"]["hw_ver"]
fw_version = fixture_data["get_device_info"]["fw_ver"]
fw_version = fw_version.split(" ", maxsplit=1)[0]

stype = supported[brand].setdefault(supported_type, {})
smodel = stype.setdefault(model, [])
smodel.append(
SupportedVersion(region=region, hw=hw_version, fw=fw_version, auth=True)
)


def _get_smartcamera_supported(supported):
for file in Path(SMARTCAMERA_FOLDER).glob("**/*.json"):
def _get_supported_devices(
supported: dict[str, Any],
fixture_location: str,
device_cls: type[IotDevice | SmartDevice | SmartCamera],
):
for file in Path(fixture_location).glob("*.json"):
with file.open() as f:
fixture_data = json.load(f)

model_info = SmartCamera._get_device_info(
model_info = device_cls._get_device_info(
fixture_data, fixture_data.get("discovery_result")
)

Expand All @@ -255,30 +226,12 @@ def _get_smartcamera_supported(supported):
SupportedVersion(
region=model_info.region,
hw=model_info.hardware_version,
fw=model_info.firmare_version,
fw=model_info.firmware_version,
auth=model_info.requires_auth,
)
)


def _get_iot_supported(supported):
for file in Path(IOT_FOLDER).glob("*.json"):
with file.open() as f:
fixture_data = json.load(f)
sysinfo = fixture_data["system"]["get_sysinfo"]
dt = _get_device_type_from_sys_info(fixture_data)
supported_type = DEVICE_TYPE_TO_PRODUCT_GROUP[dt]

model, _, region = sysinfo["model"][:-1].partition("(")
auth = "discovery_result" in fixture_data
stype = supported["kasa"].setdefault(supported_type, {})
smodel = stype.setdefault(model, [])
fw = sysinfo["sw_ver"].split(" ", maxsplit=1)[0]
smodel.append(
SupportedVersion(region=region, hw=sysinfo["hw_ver"], fw=fw, auth=auth)
)


def main():
"""Entry point to module."""
generate_supported(sys.argv[1:])
Expand Down
2 changes: 1 addition & 1 deletion kasa/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class _DeviceInfo:
device_family: str
device_type: DeviceType
hardware_version: str
firmare_version: str
firmware_version: str
firmware_build: str
requires_auth: bool
region: str | None
Expand Down
30 changes: 1 addition & 29 deletions kasa/device_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,34 +128,6 @@ def _perf_log(has_params: bool, perf_type: str) -> None:
)


def _get_device_type_from_sys_info(info: dict[str, Any]) -> DeviceType:
"""Find SmartDevice subclass for device described by passed data."""
if "system" not in info or "get_sysinfo" not in info["system"]:
raise KasaException("No 'system' or 'get_sysinfo' in response")

sysinfo: dict[str, Any] = info["system"]["get_sysinfo"]
type_: str | None = sysinfo.get("type", sysinfo.get("mic_type"))
if type_ is None:
raise KasaException("Unable to find the device type field!")

if "dev_name" in sysinfo and "Dimmer" in sysinfo["dev_name"]:
return DeviceType.Dimmer

if "smartplug" in type_.lower():
if "children" in sysinfo:
return DeviceType.Strip
if (dev_name := sysinfo.get("dev_name")) and "light" in dev_name.lower():
return DeviceType.WallSwitch
return DeviceType.Plug

if "smartbulb" in type_.lower():
if "length" in sysinfo: # strips have length
return DeviceType.LightStrip

return DeviceType.Bulb
raise UnsupportedDeviceError(f"Unknown device type: {type_}")


def get_device_class_from_sys_info(sysinfo: dict[str, Any]) -> type[IotDevice]:
"""Find SmartDevice subclass for device described by passed data."""
TYPE_TO_CLASS = {
Expand All @@ -166,7 +138,7 @@ def get_device_class_from_sys_info(sysinfo: dict[str, Any]) -> type[IotDevice]:
DeviceType.WallSwitch: IotWallSwitch,
DeviceType.LightStrip: IotLightStrip,
}
return TYPE_TO_CLASS[_get_device_type_from_sys_info(sysinfo)]
return TYPE_TO_CLASS[IotDevice._get_device_type_from_sys_info(sysinfo)]


def get_device_class_from_family(
Expand Down
Loading
Loading