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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,11 @@ The following devices have been tested and confirmed as working. If your device
- **Wall Switches**: S210, S220, S500D, S505, S505D
- **Bulbs**: L510B, L510E, L530E, L630
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
- **Cameras**: C100, C210, C220, C225, C325WB, C520WS, C720, D230, TC65, TC70
- **Cameras**: C100, C210, C220, C225, C325WB, C520WS, C720, TC65, TC70
- **Doorbells and chimes**: D230
- **Vacuums**: RV20 Max Plus, RV30 Max
- **Hubs**: H100, H200
- **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315
- **Vacuums**: RV20 Max Plus, RV30 Max

<!--SUPPORTED_END-->
[^1]: Model requires authentication
Expand Down
21 changes: 12 additions & 9 deletions SUPPORTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,23 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
- Hardware: 1.0 (US) / Firmware: 1.2.8
- **C720**
- Hardware: 1.0 (US) / Firmware: 1.2.3
- **D230**
- Hardware: 1.20 (EU) / Firmware: 1.1.19
- **TC65**
- Hardware: 1.0 / Firmware: 1.3.9
- **TC70**
- Hardware: 3.0 / Firmware: 1.3.11

### Doorbells and chimes

- **D230**
- Hardware: 1.20 (EU) / Firmware: 1.1.19

### Vacuums

- **RV20 Max Plus**
- Hardware: 1.0 (EU) / Firmware: 1.0.7
- **RV30 Max**
- Hardware: 1.0 (US) / Firmware: 1.2.0

### Hubs

- **H100**
Expand Down Expand Up @@ -326,13 +336,6 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
- Hardware: 1.0 (EU) / Firmware: 1.7.0
- Hardware: 1.0 (US) / Firmware: 1.8.0

### Vacuums

- **RV20 Max Plus**
- Hardware: 1.0 (EU) / Firmware: 1.0.7
- **RV30 Max**
- Hardware: 1.0 (US) / Firmware: 1.2.0


<!--SUPPORTED_END-->
[^1]: Model requires authentication
4 changes: 3 additions & 1 deletion devtools/generate_supported.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ class SupportedVersion(NamedTuple):
DeviceType.Bulb: "Bulbs",
DeviceType.LightStrip: "Light Strips",
DeviceType.Camera: "Cameras",
DeviceType.Doorbell: "Doorbells and chimes",
DeviceType.Chime: "Doorbells and chimes",
DeviceType.Vacuum: "Vacuums",
DeviceType.Hub: "Hubs",
DeviceType.Sensor: "Hub-Connected Devices",
DeviceType.Thermostat: "Hub-Connected Devices",
DeviceType.Vacuum: "Vacuums",
}


Expand Down
6 changes: 5 additions & 1 deletion kasa/device_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ def get_device_class_from_family(
"SMART.KASAHUB": SmartDevice,
"SMART.KASASWITCH": SmartDevice,
"SMART.IPCAMERA.HTTPS": SmartCamDevice,
"SMART.TAPODOORBELL.HTTPS": SmartCamDevice,
"SMART.TAPOROBOVAC.HTTPS": SmartDevice,
"IOT.SMARTPLUGSWITCH": IotPlug,
"IOT.SMARTBULB": IotBulb,
Expand Down Expand Up @@ -194,7 +195,10 @@ def get_protocol(config: DeviceConfig, *, strict: bool = False) -> BaseProtocol
protocol_name = ctype.device_family.value.split(".")[0]
_LOGGER.debug("Finding protocol for %s", ctype.device_family)

if ctype.device_family is DeviceFamily.SmartIpCamera:
if ctype.device_family in {
DeviceFamily.SmartIpCamera,
DeviceFamily.SmartTapoDoorbell,
}:
if strict and ctype.encryption_type is not DeviceEncryptionType.Aes:
return None
return SmartCamProtocol(transport=SslAesTransport(config=config))
Expand Down
2 changes: 2 additions & 0 deletions kasa/device_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class DeviceType(Enum):
Fan = "fan"
Thermostat = "thermostat"
Vacuum = "vacuum"
Chime = "chime"
Doorbell = "doorbell"
Unknown = "unknown"

@staticmethod
Expand Down
2 changes: 2 additions & 0 deletions kasa/deviceconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class DeviceFamily(Enum):
SmartKasaHub = "SMART.KASAHUB"
SmartIpCamera = "SMART.IPCAMERA"
SmartTapoRobovac = "SMART.TAPOROBOVAC"
SmartTapoChime = "SMART.TAPOCHIME"
SmartTapoDoorbell = "SMART.TAPODOORBELL"


class _DeviceConfigBaseMixin(DataClassJSONMixin):
Expand Down
2 changes: 2 additions & 0 deletions kasa/smart/smartdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,8 @@
return DeviceType.Thermostat
if "ROBOVAC" in device_type:
return DeviceType.Vacuum
if "TAPOCHIME" in device_type:
return DeviceType.Chime

Check warning on line 889 in kasa/smart/smartdevice.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/smartdevice.py#L889

Added line #L889 was not covered by tests
_LOGGER.warning("Unknown device type, falling back to plug")
return DeviceType.Plug

Expand Down
7 changes: 2 additions & 5 deletions kasa/smartcam/modules/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from urllib.parse import quote_plus

from ...credentials import Credentials
from ...device_type import DeviceType
from ...feature import Feature
from ...json import loads as json_loads
from ...module import FeatureAttribute, Module
Expand All @@ -31,6 +30,8 @@ class StreamResolution(StrEnum):
class Camera(SmartCamModule):
"""Implementation of device module."""

REQUIRED_COMPONENT = "video"

def _initialize_features(self) -> None:
"""Initialize features after the initial update."""
if Module.LensMask in self._device.modules:
Expand Down Expand Up @@ -126,7 +127,3 @@ def onvif_url(self) -> str | None:
return None

return f"http://{self._device.host}:{ONVIF_PORT}/onvif/device_service"

async def _check_supported(self) -> bool:
"""Additional check to see if the module is supported by the device."""
return self._device.device_type is DeviceType.Camera
7 changes: 7 additions & 0 deletions kasa/smartcam/smartcamchild.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ def _update_internal_state(self, info: dict[str, Any]) -> None:
# devices
self._info = self._map_child_info_from_parent(info)

@property
def device_type(self) -> DeviceType:
"""Return the device type."""
if self._device_type == DeviceType.Unknown and self._info:
self._device_type = self._get_device_type_from_sysinfo(self._info)
return self._device_type

@staticmethod
def _get_device_info(
info: dict[str, Any], discovery_info: dict[str, Any] | None
Expand Down
20 changes: 9 additions & 11 deletions kasa/smartcam/smartcamdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ class SmartCamDevice(SmartDevice):
@staticmethod
def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType:
"""Find type to be displayed as a supported device category."""
if (
sysinfo
and (device_type := sysinfo.get("device_type"))
and device_type.endswith("HUB")
):
if not (device_type := sysinfo.get("device_type")):
return DeviceType.Unknown

if device_type.endswith("HUB"):
return DeviceType.Hub

if "DOORBELL" in device_type:
return DeviceType.Doorbell

return DeviceType.Camera

@staticmethod
Expand Down Expand Up @@ -165,11 +168,6 @@ async def _initialize_modules(self) -> None:
if (
mod.REQUIRED_COMPONENT
and mod.REQUIRED_COMPONENT not in self._components
# Always add Camera module to cameras
and (
mod._module_name() != Module.Camera
or self._device_type is not DeviceType.Camera
)
):
continue
module = mod(self, mod._module_name())
Expand Down Expand Up @@ -258,7 +256,7 @@ async def set_state(self, on: bool) -> dict:
@property
def device_type(self) -> DeviceType:
"""Return the device type."""
if self._device_type == DeviceType.Unknown:
if self._device_type == DeviceType.Unknown and self._info:
self._device_type = self._get_device_type_from_sysinfo(self._info)
return self._device_type

Expand Down
13 changes: 13 additions & 0 deletions tests/device_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"S200D",
"S210",
"S220",
"D100C", # needs a home category?
}
THERMOSTATS_SMART = {"KE100"}

Expand Down Expand Up @@ -345,6 +346,16 @@ def parametrize(
device_type_filter=[DeviceType.Hub],
protocol_filter={"SMARTCAM"},
)
doobell_smartcam = parametrize(
"doorbell smartcam",
device_type_filter=[DeviceType.Doorbell],
protocol_filter={"SMARTCAM", "SMARTCAM.CHILD"},
)
chime_smart = parametrize(
"chime smart",
device_type_filter=[DeviceType.Chime],
protocol_filter={"SMART"},
)
vacuum = parametrize("vacuums", device_type_filter=[DeviceType.Vacuum])


Expand All @@ -362,7 +373,9 @@ def check_categories():
+ hubs_smart.args[1]
+ sensors_smart.args[1]
+ thermostats_smart.args[1]
+ chime_smart.args[1]
+ camera_smartcam.args[1]
+ doobell_smartcam.args[1]
+ hub_smartcam.args[1]
+ vacuum.args[1]
)
Expand Down
16 changes: 3 additions & 13 deletions tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,9 @@ async def test_device_class_repr(device_class_name_obj):
klass = device_class_name_obj[1]
if issubclass(klass, SmartChildDevice | SmartCamChild):
parent = SmartDevice(host, config=config)
smartcam_required = {
"device_model": "foo",
"device_type": "SMART.TAPODOORBELL",
"alias": "Foo",
"sw_ver": "1.1",
"hw_ver": "1.0",
"mac": "1.2.3.4",
"hwId": "hw_id",
"oem_id": "oem_id",
}
dev = klass(
parent,
{"dummy": "info", "device_id": "dummy", **smartcam_required},
{"dummy": "info", "device_id": "dummy"},
{
"component_list": [{"id": "device", "ver_code": 1}],
"app_component_list": [{"name": "device", "version": 1}],
Expand All @@ -153,8 +143,8 @@ async def test_device_class_repr(device_class_name_obj):
IotCamera: DeviceType.Camera,
SmartChildDevice: DeviceType.Unknown,
SmartDevice: DeviceType.Unknown,
SmartCamDevice: DeviceType.Camera,
SmartCamChild: DeviceType.Camera,
SmartCamDevice: DeviceType.Unknown,
SmartCamChild: DeviceType.Unknown,
}
type_ = CLASS_TO_DEFAULT_TYPE[klass]
child_repr = "<DeviceType.Unknown(child) of <DeviceType.Unknown at 127.0.0.2 - update() needed>>"
Expand Down
12 changes: 12 additions & 0 deletions tests/test_device_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ async def test_device_class_from_unknown_family(caplog):
SslAesTransport,
id="smartcam-hub",
),
pytest.param(
CP(DF.SmartTapoDoorbell, ET.Aes, https=True),
SmartCamProtocol,
SslAesTransport,
id="smartcam-doorbell",
),
pytest.param(
CP(DF.IotIpCamera, ET.Aes, https=True),
IotProtocol,
Expand Down Expand Up @@ -281,6 +287,12 @@ async def test_device_class_from_unknown_family(caplog):
KlapTransportV2,
id="smart-klap",
),
pytest.param(
CP(DF.SmartTapoChime, ET.Klap, https=False),
SmartProtocol,
KlapTransportV2,
id="smart-chime",
),
],
)
async def test_get_protocol(
Expand Down
Loading