mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-05-16 19:41:09 +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
|
- **Wall Switches**: S210, S220, S500D, S505, S505D
|
||||||
- **Bulbs**: L510B, L510E, L530E, L630
|
- **Bulbs**: L510B, L510E, L530E, L630
|
||||||
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
|
- **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
|
- **Hubs**: H100, H200
|
||||||
- **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315
|
- **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315
|
||||||
- **Vacuums**: RV20 Max Plus, RV30 Max
|
|
||||||
|
|
||||||
<!--SUPPORTED_END-->
|
<!--SUPPORTED_END-->
|
||||||
[^1]: Model requires authentication
|
[^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
|
- Hardware: 1.0 (US) / Firmware: 1.2.8
|
||||||
- **C720**
|
- **C720**
|
||||||
- Hardware: 1.0 (US) / Firmware: 1.2.3
|
- Hardware: 1.0 (US) / Firmware: 1.2.3
|
||||||
- **D230**
|
|
||||||
- Hardware: 1.20 (EU) / Firmware: 1.1.19
|
|
||||||
- **TC65**
|
- **TC65**
|
||||||
- Hardware: 1.0 / Firmware: 1.3.9
|
- Hardware: 1.0 / Firmware: 1.3.9
|
||||||
- **TC70**
|
- **TC70**
|
||||||
- Hardware: 3.0 / Firmware: 1.3.11
|
- 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
|
### Hubs
|
||||||
|
|
||||||
- **H100**
|
- **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 (EU) / Firmware: 1.7.0
|
||||||
- Hardware: 1.0 (US) / Firmware: 1.8.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-->
|
<!--SUPPORTED_END-->
|
||||||
[^1]: Model requires authentication
|
[^1]: Model requires authentication
|
||||||
|
@ -36,10 +36,12 @@ DEVICE_TYPE_TO_PRODUCT_GROUP = {
|
|||||||
DeviceType.Bulb: "Bulbs",
|
DeviceType.Bulb: "Bulbs",
|
||||||
DeviceType.LightStrip: "Light Strips",
|
DeviceType.LightStrip: "Light Strips",
|
||||||
DeviceType.Camera: "Cameras",
|
DeviceType.Camera: "Cameras",
|
||||||
|
DeviceType.Doorbell: "Doorbells and chimes",
|
||||||
|
DeviceType.Chime: "Doorbells and chimes",
|
||||||
|
DeviceType.Vacuum: "Vacuums",
|
||||||
DeviceType.Hub: "Hubs",
|
DeviceType.Hub: "Hubs",
|
||||||
DeviceType.Sensor: "Hub-Connected Devices",
|
DeviceType.Sensor: "Hub-Connected Devices",
|
||||||
DeviceType.Thermostat: "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.KASAHUB": SmartDevice,
|
||||||
"SMART.KASASWITCH": SmartDevice,
|
"SMART.KASASWITCH": SmartDevice,
|
||||||
"SMART.IPCAMERA.HTTPS": SmartCamDevice,
|
"SMART.IPCAMERA.HTTPS": SmartCamDevice,
|
||||||
|
"SMART.TAPODOORBELL.HTTPS": SmartCamDevice,
|
||||||
"SMART.TAPOROBOVAC.HTTPS": SmartDevice,
|
"SMART.TAPOROBOVAC.HTTPS": SmartDevice,
|
||||||
"IOT.SMARTPLUGSWITCH": IotPlug,
|
"IOT.SMARTPLUGSWITCH": IotPlug,
|
||||||
"IOT.SMARTBULB": IotBulb,
|
"IOT.SMARTBULB": IotBulb,
|
||||||
@ -194,7 +195,10 @@ def get_protocol(config: DeviceConfig, *, strict: bool = False) -> BaseProtocol
|
|||||||
protocol_name = ctype.device_family.value.split(".")[0]
|
protocol_name = ctype.device_family.value.split(".")[0]
|
||||||
_LOGGER.debug("Finding protocol for %s", ctype.device_family)
|
_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:
|
if strict and ctype.encryption_type is not DeviceEncryptionType.Aes:
|
||||||
return None
|
return None
|
||||||
return SmartCamProtocol(transport=SslAesTransport(config=config))
|
return SmartCamProtocol(transport=SslAesTransport(config=config))
|
||||||
|
@ -22,6 +22,8 @@ class DeviceType(Enum):
|
|||||||
Fan = "fan"
|
Fan = "fan"
|
||||||
Thermostat = "thermostat"
|
Thermostat = "thermostat"
|
||||||
Vacuum = "vacuum"
|
Vacuum = "vacuum"
|
||||||
|
Chime = "chime"
|
||||||
|
Doorbell = "doorbell"
|
||||||
Unknown = "unknown"
|
Unknown = "unknown"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -79,6 +79,8 @@ class DeviceFamily(Enum):
|
|||||||
SmartKasaHub = "SMART.KASAHUB"
|
SmartKasaHub = "SMART.KASAHUB"
|
||||||
SmartIpCamera = "SMART.IPCAMERA"
|
SmartIpCamera = "SMART.IPCAMERA"
|
||||||
SmartTapoRobovac = "SMART.TAPOROBOVAC"
|
SmartTapoRobovac = "SMART.TAPOROBOVAC"
|
||||||
|
SmartTapoChime = "SMART.TAPOCHIME"
|
||||||
|
SmartTapoDoorbell = "SMART.TAPODOORBELL"
|
||||||
|
|
||||||
|
|
||||||
class _DeviceConfigBaseMixin(DataClassJSONMixin):
|
class _DeviceConfigBaseMixin(DataClassJSONMixin):
|
||||||
|
@ -885,6 +885,8 @@ class SmartDevice(Device):
|
|||||||
return DeviceType.Thermostat
|
return DeviceType.Thermostat
|
||||||
if "ROBOVAC" in device_type:
|
if "ROBOVAC" in device_type:
|
||||||
return DeviceType.Vacuum
|
return DeviceType.Vacuum
|
||||||
|
if "TAPOCHIME" in device_type:
|
||||||
|
return DeviceType.Chime
|
||||||
_LOGGER.warning("Unknown device type, falling back to plug")
|
_LOGGER.warning("Unknown device type, falling back to plug")
|
||||||
return DeviceType.Plug
|
return DeviceType.Plug
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ from typing import Annotated
|
|||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from ...credentials import Credentials
|
from ...credentials import Credentials
|
||||||
from ...device_type import DeviceType
|
|
||||||
from ...feature import Feature
|
from ...feature import Feature
|
||||||
from ...json import loads as json_loads
|
from ...json import loads as json_loads
|
||||||
from ...module import FeatureAttribute, Module
|
from ...module import FeatureAttribute, Module
|
||||||
@ -31,6 +30,8 @@ class StreamResolution(StrEnum):
|
|||||||
class Camera(SmartCamModule):
|
class Camera(SmartCamModule):
|
||||||
"""Implementation of device module."""
|
"""Implementation of device module."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = "video"
|
||||||
|
|
||||||
def _initialize_features(self) -> None:
|
def _initialize_features(self) -> None:
|
||||||
"""Initialize features after the initial update."""
|
"""Initialize features after the initial update."""
|
||||||
if Module.LensMask in self._device.modules:
|
if Module.LensMask in self._device.modules:
|
||||||
@ -126,7 +127,3 @@ class Camera(SmartCamModule):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return f"http://{self._device.host}:{ONVIF_PORT}/onvif/device_service"
|
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
|
# devices
|
||||||
self._info = self._map_child_info_from_parent(info)
|
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
|
@staticmethod
|
||||||
def _get_device_info(
|
def _get_device_info(
|
||||||
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
||||||
|
@ -26,12 +26,15 @@ class SmartCamDevice(SmartDevice):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType:
|
def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType:
|
||||||
"""Find type to be displayed as a supported device category."""
|
"""Find type to be displayed as a supported device category."""
|
||||||
if (
|
if not (device_type := sysinfo.get("device_type")):
|
||||||
sysinfo
|
return DeviceType.Unknown
|
||||||
and (device_type := sysinfo.get("device_type"))
|
|
||||||
and device_type.endswith("HUB")
|
if device_type.endswith("HUB"):
|
||||||
):
|
|
||||||
return DeviceType.Hub
|
return DeviceType.Hub
|
||||||
|
|
||||||
|
if "DOORBELL" in device_type:
|
||||||
|
return DeviceType.Doorbell
|
||||||
|
|
||||||
return DeviceType.Camera
|
return DeviceType.Camera
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -165,11 +168,6 @@ class SmartCamDevice(SmartDevice):
|
|||||||
if (
|
if (
|
||||||
mod.REQUIRED_COMPONENT
|
mod.REQUIRED_COMPONENT
|
||||||
and mod.REQUIRED_COMPONENT not in self._components
|
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
|
continue
|
||||||
module = mod(self, mod._module_name())
|
module = mod(self, mod._module_name())
|
||||||
@ -258,7 +256,7 @@ class SmartCamDevice(SmartDevice):
|
|||||||
@property
|
@property
|
||||||
def device_type(self) -> DeviceType:
|
def device_type(self) -> DeviceType:
|
||||||
"""Return the device type."""
|
"""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)
|
self._device_type = self._get_device_type_from_sysinfo(self._info)
|
||||||
return self._device_type
|
return self._device_type
|
||||||
|
|
||||||
|
@ -131,6 +131,7 @@ SENSORS_SMART = {
|
|||||||
"S200D",
|
"S200D",
|
||||||
"S210",
|
"S210",
|
||||||
"S220",
|
"S220",
|
||||||
|
"D100C", # needs a home category?
|
||||||
}
|
}
|
||||||
THERMOSTATS_SMART = {"KE100"}
|
THERMOSTATS_SMART = {"KE100"}
|
||||||
|
|
||||||
@ -345,6 +346,16 @@ hub_smartcam = parametrize(
|
|||||||
device_type_filter=[DeviceType.Hub],
|
device_type_filter=[DeviceType.Hub],
|
||||||
protocol_filter={"SMARTCAM"},
|
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])
|
vacuum = parametrize("vacuums", device_type_filter=[DeviceType.Vacuum])
|
||||||
|
|
||||||
|
|
||||||
@ -362,7 +373,9 @@ def check_categories():
|
|||||||
+ hubs_smart.args[1]
|
+ hubs_smart.args[1]
|
||||||
+ sensors_smart.args[1]
|
+ sensors_smart.args[1]
|
||||||
+ thermostats_smart.args[1]
|
+ thermostats_smart.args[1]
|
||||||
|
+ chime_smart.args[1]
|
||||||
+ camera_smartcam.args[1]
|
+ camera_smartcam.args[1]
|
||||||
|
+ doobell_smartcam.args[1]
|
||||||
+ hub_smartcam.args[1]
|
+ hub_smartcam.args[1]
|
||||||
+ vacuum.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]
|
klass = device_class_name_obj[1]
|
||||||
if issubclass(klass, SmartChildDevice | SmartCamChild):
|
if issubclass(klass, SmartChildDevice | SmartCamChild):
|
||||||
parent = SmartDevice(host, config=config)
|
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(
|
dev = klass(
|
||||||
parent,
|
parent,
|
||||||
{"dummy": "info", "device_id": "dummy", **smartcam_required},
|
{"dummy": "info", "device_id": "dummy"},
|
||||||
{
|
{
|
||||||
"component_list": [{"id": "device", "ver_code": 1}],
|
"component_list": [{"id": "device", "ver_code": 1}],
|
||||||
"app_component_list": [{"name": "device", "version": 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,
|
IotCamera: DeviceType.Camera,
|
||||||
SmartChildDevice: DeviceType.Unknown,
|
SmartChildDevice: DeviceType.Unknown,
|
||||||
SmartDevice: DeviceType.Unknown,
|
SmartDevice: DeviceType.Unknown,
|
||||||
SmartCamDevice: DeviceType.Camera,
|
SmartCamDevice: DeviceType.Unknown,
|
||||||
SmartCamChild: DeviceType.Camera,
|
SmartCamChild: DeviceType.Unknown,
|
||||||
}
|
}
|
||||||
type_ = CLASS_TO_DEFAULT_TYPE[klass]
|
type_ = CLASS_TO_DEFAULT_TYPE[klass]
|
||||||
child_repr = "<DeviceType.Unknown(child) of <DeviceType.Unknown at 127.0.0.2 - update() needed>>"
|
child_repr = "<DeviceType.Unknown(child) of <DeviceType.Unknown at 127.0.0.2 - update() needed>>"
|
||||||
|
@ -245,6 +245,12 @@ ET = DeviceEncryptionType
|
|||||||
SslAesTransport,
|
SslAesTransport,
|
||||||
id="smartcam-hub",
|
id="smartcam-hub",
|
||||||
),
|
),
|
||||||
|
pytest.param(
|
||||||
|
CP(DF.SmartTapoDoorbell, ET.Aes, https=True),
|
||||||
|
SmartCamProtocol,
|
||||||
|
SslAesTransport,
|
||||||
|
id="smartcam-doorbell",
|
||||||
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
CP(DF.IotIpCamera, ET.Aes, https=True),
|
CP(DF.IotIpCamera, ET.Aes, https=True),
|
||||||
IotProtocol,
|
IotProtocol,
|
||||||
@ -281,6 +287,12 @@ ET = DeviceEncryptionType
|
|||||||
KlapTransportV2,
|
KlapTransportV2,
|
||||||
id="smart-klap",
|
id="smart-klap",
|
||||||
),
|
),
|
||||||
|
pytest.param(
|
||||||
|
CP(DF.SmartTapoChime, ET.Klap, https=False),
|
||||||
|
SmartProtocol,
|
||||||
|
KlapTransportV2,
|
||||||
|
id="smart-chime",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_get_protocol(
|
async def test_get_protocol(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user