mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-05-16 03:21:22 +00:00
Add support for doorbells and chimes (#1435)
Add support for `smart` chimes and `smartcam` doorbells that are not hub child devices. Co-authored-by: Steven B <51370195+sdb9696@users.noreply.github.com>
This commit is contained in:
parent
acc0e9a80a
commit
54bb53899e
@ -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
|
||||
|
21
SUPPORTED.md
21
SUPPORTED.md
@ -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**
|
||||
@ -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
|
||||
|
@ -36,10 +36,12 @@ DEVICE_TYPE_TO_PRODUCT_GROUP = {
|
||||
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",
|
||||
}
|
||||
|
||||
|
||||
|
@ -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,
|
||||
@ -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))
|
||||
|
@ -22,6 +22,8 @@ class DeviceType(Enum):
|
||||
Fan = "fan"
|
||||
Thermostat = "thermostat"
|
||||
Vacuum = "vacuum"
|
||||
Chime = "chime"
|
||||
Doorbell = "doorbell"
|
||||
Unknown = "unknown"
|
||||
|
||||
@staticmethod
|
||||
|
@ -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):
|
||||
|
@ -885,6 +885,8 @@ class SmartDevice(Device):
|
||||
return DeviceType.Thermostat
|
||||
if "ROBOVAC" in device_type:
|
||||
return DeviceType.Vacuum
|
||||
if "TAPOCHIME" in device_type:
|
||||
return DeviceType.Chime
|
||||
_LOGGER.warning("Unknown device type, falling back to plug")
|
||||
return DeviceType.Plug
|
||||
|
||||
|
@ -9,7 +9,6 @@ from typing import Annotated
|
||||
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
|
||||
@ -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:
|
||||
@ -126,7 +127,3 @@ class Camera(SmartCamModule):
|
||||
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
|
||||
|
@ -85,6 +85,13 @@ class SmartCamChild(SmartChildDevice, SmartCamDevice):
|
||||
# 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
|
||||
|
@ -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
|
||||
@ -165,11 +168,6 @@ class SmartCamDevice(SmartDevice):
|
||||
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())
|
||||
@ -258,7 +256,7 @@ class SmartCamDevice(SmartDevice):
|
||||
@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
|
||||
|
||||
|
@ -131,6 +131,7 @@ SENSORS_SMART = {
|
||||
"S200D",
|
||||
"S210",
|
||||
"S220",
|
||||
"D100C", # needs a home category?
|
||||
}
|
||||
THERMOSTATS_SMART = {"KE100"}
|
||||
|
||||
@ -345,6 +346,16 @@ hub_smartcam = 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])
|
||||
|
||||
|
||||
@ -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]
|
||||
)
|
||||
|
@ -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}],
|
||||
@ -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>>"
|
||||
|
@ -245,6 +245,12 @@ ET = DeviceEncryptionType
|
||||
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,
|
||||
@ -281,6 +287,12 @@ ET = DeviceEncryptionType
|
||||
KlapTransportV2,
|
||||
id="smart-klap",
|
||||
),
|
||||
pytest.param(
|
||||
CP(DF.SmartTapoChime, ET.Klap, https=False),
|
||||
SmartProtocol,
|
||||
KlapTransportV2,
|
||||
id="smart-chime",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_get_protocol(
|
||||
|
Loading…
x
Reference in New Issue
Block a user