8000 Add support for doorbells and chimes (#1435) · python-kasa/python-kasa@54bb538 · GitHub
[go: up one dir, main page]

Skip to content

Commit 54bb538

Browse files
stevereddensdb9696
andauthored
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>
1 parent acc0e9a commit 54bb538

13 files changed

+75
-42
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,11 @@ The following devices have been tested and confirmed as working. If your device
201201
- **Wall Switches**: S210, S220, S500D, S505, S505D
202202
- **Bulbs**: L510B, L510E, L530E, L630
203203
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
204-
- **Cameras**: C100, C210, C220, C225, C325WB, C520WS, C720, D230, TC65, TC70
204+
- **Cameras**: C100, C210, C220, C225, C325WB, C520WS, C720, TC65, TC70
205+
- **Doorbells and chimes**: D230
206+
- **Vacuums**: RV20 Max Plus, RV30 Max
205207
- **Hubs**: H100, H200
206208
- **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315
207-
- **Vacuums**: RV20 Max Plus, RV30 Max
208209

209210
<!--SUPPORTED_END-->
210211
[^1]: Model requires authentication

SUPPORTED.md

Lines changed: 12 additions & 9 deletions
< 629A td data-grid-cell-id="diff-25a3722bf6006b7a060d6900ea707cb596a854ea3cdb6ef3b1980531d5d85bc5-294-302-2" data-line-anchor="diff-25a3722bf6006b7a060d6900ea707cb596a854ea3cdb6ef3b1980531d5d85bc5R302" data-selected="false" role="gridcell" style="background-color:var(--diffBlob-additionLine-bgColor, var(--diffBlob-addition-bgColor-line));padding-right:24px" tabindex="-1" valign="top" class="focusable-grid-cell diff-text-cell right-side-diff-cell left-side">+
- **RV30 Max**
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,23 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
285285
- Hardware: 1.0 (US) / Firmware: 1.2.8
286286
- **C720**
287287
- Hardware: 1.0 (US) / Firmware: 1.2.3
288-
- **D230**
289-
- Hardware: 1.20 (EU) / Firmware: 1.1.19
290288
- **TC65**
291289
- Hardware: 1.0 / Firmware: 1.3.9
292290
- **TC70**
293291
- Hardware: 3.0 / Firmware: 1.3.11
294292

293+
### Doorbells and chimes
294+
295+
- **D230**
296+
- Hardware: 1.20 (EU) / Firmware: 1.1.19
297+
298+
### Vacuums
299+
300+
- **RV20 Max Plus**
301+
- Hardware: 1.0 (EU) / Firmware: 1.0.7
302
303+
- Hardware: 1.0 (US) / Firmware: 1.2.0
304+
295305
### Hubs
296306

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

329-
### Vacuums
330-
331-
- **RV20 Max Plus**
332-
- Hardware: 1.0 (EU) / Firmware: 1.0.7
333-
- **RV30 Max**
334-
- Hardware: 1.0 (US) / Firmware: 1.2.0
335-
336339

337340
<!--SUPPORTED_END-->
338341
[^1]: Model requires authentication

devtools/generate_supported.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ class SupportedVersion(NamedTuple):
3636
DeviceType.Bulb: "Bulbs",
3737
DeviceType.LightStrip: "Light Strips",
3838
DeviceType.Camera: "Cameras",
39+
DeviceType.Doorbell: "Doorbells and chimes",
40+
DeviceType.Chime: "Doorbells and chimes",
41+
DeviceType.Vacuum: "Vacuums",
3942
DeviceType.Hub: "Hubs",
4043
DeviceType.Sensor: "Hub-Connected Devices",
4144
DeviceType.Thermostat: "Hub-Connected Devices",
42-
DeviceType.Vacuum: "Vacuums",
4345
}
4446

4547

kasa/device_factory.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ def get_device_class_from_family(
159159
"SMART.KASAHUB": SmartDevice,
160160
"SMART.KASASWITCH": SmartDevice,
161161
"SMART.IPCAMERA.HTTPS": SmartCamDevice,
162+
"SMART.TAPODOORBELL.HTTPS": SmartCamDevice,
162163
"SMART.TAPOROBOVAC.HTTPS": SmartDevice,
163164
"IOT.SMARTPLUGSWITCH": IotPlug,
164165
"IOT.SMARTBULB": IotBulb,
@@ -194,7 +195,10 @@ def get_protocol(config: DeviceConfig, *, strict: bool = False) -> BaseProtocol
194195
protocol_name = ctype.device_family.value.split(".")[0]
195196
_LOGGER.debug("Finding protocol for %s", ctype.device_family)
196197

197-
if ctype.device_family is DeviceFamily.SmartIpCamera:
198+
if ctype.device_family in {
199+
DeviceFamily.SmartIpCamera,
200+
DeviceFamily.SmartTapoDoorbell,
201+
}:
198202
if strict and ctype.encryption_type is not DeviceEncryptionType.Aes:
199203
return None
200204
return SmartCamProtocol(transport=SslAesTransport(config=config))

kasa/device_type.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class DeviceType(Enum):
2222
Fan = "fan"
2323
Thermostat = "thermostat"
2424
Vacuum = "vacuum"
25+
Chime = "chime"
26+
Doorbell = "doorbell"
2527
Unknown = "unknown"
2628

2729
@staticmethod

kasa/deviceconfig.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ class DeviceFamily(Enum):
7979
SmartKasaHub = "SMART.KASAHUB"
8080
SmartIpCamera = "SMART.IPCAMERA"
8181
SmartTapoRobovac = "SMART.TAPOROBOVAC"
82+
SmartTapoChime = "SMART.TAPOCHIME"
83+
SmartTapoDoorbell = "SMART.TAPODOORBELL"
8284

8385

8486
class _DeviceConfigBaseMixin(DataClassJSONMixin):

kasa/smart/smartdevice.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,8 @@ def _get_device_type_from_components(
885885
return DeviceType.Thermostat
886886
if "ROBOVAC" in device_type:
887887
return DeviceType.Vacuum
888+
if "TAPOCHIME" in device_type:
889+
return DeviceType.Chime
888890
_LOGGER.warning("Unknown device type, falling back to plug")
889891
return DeviceType.Plug
890892

kasa/smartcam/modules/camera.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from urllib.parse import quote_plus
1010

1111
from ...credentials import Credentials
12-
from ...device_type import DeviceType
1312
from ...feature import Feature
1413
from ...json import loads as json_loads
1514
from ...module import FeatureAttribute, Module
@@ -31,6 +30,8 @@ class StreamResolution(StrEnum):
3130
class Camera(SmartCamModule):
3231
"""Implementation of device module."""
3332

33+
REQUIRED_COMPONENT = "video"
34+
3435
def _initialize_features(self) -> None:
3536
"""Initialize features after the initial update."""
3637
if Module.LensMask in self._device.modules:
@@ -126,7 +127,3 @@ def onvif_url(self) -> str | None:
126127
return None
127128

128129
return f"http://{self._device.host}:{ONVIF_PORT}/onvif/device_service"
129-
130-
async def _check_supported(self) -> bool:
131-
"""Additional check to see if the module is supported by the device."""
132-
return self._device.device_type is DeviceType.Camera

kasa/smartcam/smartcamchild.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ def _update_internal_state(self, info: dict[str, Any]) -> None:
8585
# devices
8686
self._info = self._map_child_info_from_parent(info)
8787

88+
@property
89+
def device_type(self) -> DeviceType:
90+
"""Return the device type."""
91+
if self._device_type == DeviceType.Unknown and self._info:
92+
self._device_type = self._get_device_type_from_sysinfo(self._info)
93+
return self._device_type
94+
8895
@staticmethod
8996
def _get_device_info(
9097
info: dict[str, Any], discovery_info: dict[str, Any] | None

kasa/smartcam/smartcamdevice.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ class SmartCamDevice(SmartDevice):
2626
@staticmethod
2727
def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType:
2828
"""Find type to be displayed as a supported device category."""
29-
if (
30-
sysinfo
31-
and (device_type := sysinfo.get("device_type"))
32-
and device_type.endswith("HUB")
33-
):
29+
if not (device_type := sysinfo.get("device_type")):
30+
return DeviceType.Unknown
31+
32+
if device_type.endswith("HUB"):
3433
return DeviceType.Hub
34+
35+
if "DOORBELL" in device_type:
36+
return DeviceType.Doorbell
37+
3538
return DeviceType.Camera
3639

3740
@staticmethod
@@ -165,11 +168,6 @@ async def _initialize_modules(self) -> None:
165168
if (
166169
mod.REQUIRED_COMPONENT
167170
and mod.REQUIRED_COMPONENT not in self._components
168-
# Always add Camera module to cameras
169-
and (
170-
mod._module_name() != Module.Camera
171-
or self._device_type is not DeviceType.Camera
172-
)
173171
):
174172
continue
175173
module = mod(self, mod._module_name())
@@ -258,7 +256,7 @@ async def set_state(self, on: bool) -> dict:
258256
@property
259257
def device_type(self) -> DeviceType:
260258
"""Return the device type."""
261-
if self._device_type == DeviceType.Unknown:
259+
if self._device_type == DeviceType.Unknown and self._info:
262260
self._device_type = self._get_device_type_from_sysinfo(self._info)
263261
return self._device_type
264262

tests/device_fixtures.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
"S200D",
132132
"S210",
133133
"S220",
134+
"D100C", # needs a home category?
134135
}
135136
THERMOSTATS_SMART = {"KE100"}
136137

@@ -345,6 +346,16 @@ def parametrize(
345346
device_type_filter=[DeviceType.Hub],
346347
protocol_filter={"SMARTCAM"},
347348
)
349+
doobell_smartcam = parametrize(
350+
"doorbell smartcam",
351+
device_type_filter=[DeviceType.Doorbell],
352+
protocol_filter={"SMARTCAM", "SMARTCAM.CHILD"},
353+
)
354+
chime_smart = parametrize(
355+
"chime smart",
356+
device_type_filter=[DeviceType.Chime],
357+
protocol_filter={"SMART"},
358+
)
348359
vacuum = parametrize("vacuums", device_type_filter=[DeviceType.Vacuum])
349360

350361

@@ -362,7 +373,9 @@ def check_categories():
362373
+ hubs_smart.args[1]
363374
+ sensors_smart.args[1]
364375
+ thermostats_smart.args[1]
376+
+ chime_smart.args[1]
365377
+ camera_smartcam.args[1]
378+
+ doobell_smartcam.args[1]
366379
+ hub_smartcam.args[1]
367380
+ vacuum.args[1]
368381
)

tests/test_device.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,19 +121,9 @@ async def test_device_class_repr(device_class_name_obj):
121121
klass = device_class_name_obj[1]
122122
if issubclass(klass, SmartChildDevice | SmartCamChild):
123123
parent = SmartDevice(host, config=config)
124-
smartcam_required = {
125-
"device_model": "foo",
126-
"device_type": "SMART.TAPODOORBELL",
127-
"alias": "Foo",
128-
"sw_ver": "1.1",
129-
"hw_ver": "1.0",
130-
"mac": "1.2.3.4",
131-
"hwId": "hw_id",
132-
"oem_id": "oem_id",
133-
}
134124
dev = klass(
135125
parent,
136-
{"dummy": "info", "device_id": "dummy", **smartcam_required},
126+
{"dummy": "info", "device_id": "dummy"},
137127
{
138128
"component_list": [{"id": "device", "ver_code": 1}],
139129
"app_component_list": [{"name": "device", "version": 1}],
@@ -153,8 +143,8 @@ async def test_device_class_repr(device_class_name_obj):
153143
IotCamera: DeviceType.Camera,
154144
SmartChildDevice: DeviceType.Unknown,
155145
SmartDevice: DeviceType.Unknown,
156-
SmartCamDevice: DeviceType.Camera,
157-
SmartCamChild: DeviceType.Camera,
146+
SmartCamDevice: DeviceType.Unknown,
147+
SmartCamChild: DeviceType.Unknown,
158148
}
159149
type_ = CLASS_TO_DEFAULT_TYPE[klass]
160150
child_repr = "<DeviceType.Unknown(child) of <DeviceType.Unknown at 127.0.0.2 - update() needed>>"

tests/test_device_factory.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ async def test_device_class_from_unknown_family(caplog):
245245
SslAesTransport,
246246
id="smartcam-hub",
247247
),
248+
pytest.param(
249+
CP(DF.SmartTapoDoorbell, ET.Aes, https=True),
250+
SmartCamProtocol,
251+
SslAesTransport,
252+
id="smartcam-doorbell",
253+
),
248254
pytest.param(
249255
CP(DF.IotIpCamera, ET.Aes, https=True),
250256
IotProtocol,
@@ -281,6 +287,12 @@ async def test_device_class_from_unknown_family(caplog):
281287
KlapTransportV2,
282288
id="smart-klap",
283289
),
290+
pytest.param(
291+
CP(DF.SmartTapoChime, ET.Klap, https=False),
292+
SmartProtocol,
293+
KlapTransportV2,
294+
id="smart-chime",
295+
),
284296
],
285297
)
286298
async def test_get_protocol(

0 commit comments

Comments
 (0)
0