Add support for tapo login_version 3 in sslaestransport (#1638)

Updates to get_default_credentials and DEFAULT_CREDENTIALS for handling
a new default password for encryption_type 3 in TAPOCAMERA devices that
use encryption_type 3.

This adds support for devices like TC40.
This commit is contained in:
ZeliardM
2026-02-22 13:56:26 -05:00
committed by GitHub
parent 30a8fd45a8
commit eefbf9ec1c
6 changed files with 1103 additions and 6 deletions

View File

@@ -201,7 +201,7 @@ The following devices have been tested and confirmed as working. If your device
- **Wall Switches**: S210, S220, S500, S500D, S505, S505D, TS15 - **Wall Switches**: S210, S220, S500, S500D, S505, S505D, TS15
- **Bulbs**: L430C, L430P, L510B, L510E, L530B, L530E, L535E, L630 - **Bulbs**: L430C, L430P, L510B, L510E, L530B, L530E, L535E, L630
- **Light Strips**: L900-10, L900-5, L920-5, L930-5 - **Light Strips**: L900-10, L900-5, L920-5, L930-5
- **Cameras**: C100, C110, C210, C220, C225, C325WB, C460, C520WS, C720, TC65, TC70 - **Cameras**: C100, C110, C210, C220, C225, C325WB, C460, C520WS, C720, TC40, TC65, TC70
- **Doorbells and chimes**: D100C, D130, D230 - **Doorbells and chimes**: D100C, D130, D230
- **Vacuums**: RV20 Max Plus, RV30 Max - **Vacuums**: RV20 Max Plus, RV30 Max
- **Hubs**: H100, H200 - **Hubs**: H100, H200

View File

@@ -320,6 +320,8 @@ 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
- **TC40**
- Hardware: 2.0 (EU) / Firmware: 1.0.4
- **TC65** - **TC65**
- Hardware: 1.0 / Firmware: 1.3.9 - Hardware: 1.0 / Firmware: 1.3.9
- **TC70** - **TC70**

View File

@@ -16,10 +16,10 @@ class Credentials:
password: str = field(default="", repr=False) password: str = field(default="", repr=False)
def get_default_credentials(tuple: tuple[str, str]) -> Credentials: def get_default_credentials(crdentials: tuple[str, str]) -> Credentials:
"""Return decoded default credentials.""" """Return decoded default credentials."""
un = base64.b64decode(tuple[0].encode()).decode() un = base64.b64decode(crdentials[0].encode()).decode()
pw = base64.b64decode(tuple[1].encode()).decode() pw = base64.b64decode(crdentials[1].encode()).decode()
return Credentials(un, pw) return Credentials(un, pw)
@@ -28,4 +28,5 @@ DEFAULT_CREDENTIALS = {
"KASACAMERA": ("YWRtaW4=", "MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM="), "KASACAMERA": ("YWRtaW4=", "MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM="),
"TAPO": ("dGVzdEB0cC1saW5rLm5ldA==", "dGVzdA=="), "TAPO": ("dGVzdEB0cC1saW5rLm5ldA==", "dGVzdA=="),
"TAPOCAMERA": ("YWRtaW4=", "YWRtaW4="), "TAPOCAMERA": ("YWRtaW4=", "YWRtaW4="),
"TAPOCAMERA_LV3": ("YWRtaW4=", "VFBMMDc1NTI2NDYwNjAz"),
} }

View File

@@ -95,8 +95,12 @@ class SslAesTransport(BaseTransport):
not self._credentials or self._credentials.username is None not self._credentials or self._credentials.username is None
) and not self._credentials_hash: ) and not self._credentials_hash:
self._credentials = Credentials() self._credentials = Credentials()
if self._login_version == 3:
_default_credentials = DEFAULT_CREDENTIALS["TAPOCAMERA_LV3"]
else:
_default_credentials = DEFAULT_CREDENTIALS["TAPOCAMERA"]
self._default_credentials: Credentials = get_default_credentials( self._default_credentials: Credentials = get_default_credentials(
DEFAULT_CREDENTIALS["TAPOCAMERA"] _default_credentials
) )
self._http_client: HttpClient = HttpClient(config) self._http_client: HttpClient = HttpClient(config)

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import base64
import logging import logging
import secrets import secrets
from contextlib import nullcontext as does_not_raise from contextlib import nullcontext as does_not_raise
@@ -12,7 +13,12 @@ import pytest
from yarl import URL from yarl import URL
from kasa.credentials import DEFAULT_CREDENTIALS, Credentials, get_default_credentials from kasa.credentials import DEFAULT_CREDENTIALS, Credentials, get_default_credentials
from kasa.deviceconfig import DeviceConfig from kasa.deviceconfig import (
DeviceConfig,
DeviceConnectionParameters,
DeviceEncryptionType,
DeviceFamily,
)
from kasa.exceptions import ( from kasa.exceptions import (
AuthenticationError, AuthenticationError,
DeviceError, DeviceError,
@@ -393,6 +399,53 @@ async def test_port_override():
assert str(transport._app_url) == f"https://127.0.0.1:{port_override}" assert str(transport._app_url) == f"https://127.0.0.1:{port_override}"
@pytest.mark.parametrize(
("login_version", "expected_password_b64"),
[
pytest.param(
3,
"VFBMMDc1NTI2NDYwNjAz", # noqa: S105
id="version-3-uses-lv3-credentials",
),
pytest.param(
2,
"YWRtaW4=", # noqa: S105
id="version-2-uses-tapocamera-credentials",
),
pytest.param(
None,
"YWRtaW4=", # noqa: S105
id="no-version-uses-tapocamera-credentials",
),
],
)
async def test_login_version_default_credentials(
mocker, login_version, expected_password_b64
):
"""Test that login_version=3 uses TAPOCAMERA_LV3 credentials while other versions use TAPOCAMERA."""
host = "127.0.0.1"
tapo_family = DeviceFamily.SmartIpCamera
aes_type = DeviceEncryptionType.Aes
mock_ssl_aes_device = MockSslAesDevice(host)
mocker.patch.object(
aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
)
config = DeviceConfig(
host,
credentials=Credentials("foo", "bar"),
connection_type=DeviceConnectionParameters(
tapo_family, aes_type, login_version=login_version
),
)
transport = SslAesTransport(config=config)
assert transport._default_credentials.username == "admin"
password_b64 = base64.b64encode(
transport._default_credentials.password.encode()
).decode()
assert password_b64 == expected_password_b64
class MockSslAesDevice: class MockSslAesDevice:
BAD_USER_RESP = { BAD_USER_RESP = {
"error_code": SmartErrorCode.SESSION_EXPIRED.value, "error_code": SmartErrorCode.SESSION_EXPIRED.value,