Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d8af4f4
added inital api endpoint execute request change
JNickson May 23, 2025
069fa9b
rm symlink + use new body / header
alec-pinson May 23, 2025
585dfac
use 'cleanv3'
alec-pinson May 23, 2025
b4e3226
missed some
alec-pinson May 23, 2025
53c485c
correct path + command
alec-pinson May 23, 2025
41329b8
added other headers
alec-pinson May 23, 2025
19360c0
debug auth json
JNickson May 23, 2025
52cc9cd
another api required to get new auth token for control api
alec-pinson May 23, 2025
ebb08ed
begin adding new perms api
alec-pinson May 23, 2025
05c625f
move credentials if statement
alec-pinson May 23, 2025
9eeefe3
sst token
JNickson May 23, 2025
ce5c9fc
added base url
JNickson May 23, 2025
df9f446
added logging + corrected userid
alec-pinson May 23, 2025
a3d7d82
some fixes
alec-pinson May 23, 2025
eb3e1ea
updating error checking
JNickson May 23, 2025
79ec978
Revert "updating error checking"
alec-pinson May 23, 2025
3b73ef5
setError comment
alec-pinson May 23, 2025
b5a916d
add set_error
alec-pinson May 23, 2025
f628a7b
added set errors for cleanv3 cannot inherit from clean
JNickson May 23, 2025
e033e06
added clean logic in cleanv3
JNickson May 23, 2025
1a67bf9
after start clear 505 + extra resume step
JNickson May 23, 2025
0c00cac
added debug for action executed
JNickson May 23, 2025
698afeb
added resume to args
JNickson May 23, 2025
a089f3c
set errors no data
JNickson May 23, 2025
3d4004c
added state check before error and resume to make sure in paused
JNickson May 23, 2025
64de88a
slight cleanup
alec-pinson May 24, 2025
07158de
try clear error 505 on get error
alec-pinson May 24, 2025
409dc4b
updated set error and clean
JNickson May 24, 2025
f2df532
added resume if still paused after start
JNickson May 24, 2025
8f36c1a
added _execute
JNickson May 24, 2025
cdd445f
removed clean execute resume part
JNickson May 24, 2025
de433ba
send resume and start if docked
alec-pinson May 24, 2025
b618b3d
remove GetCleanInfoV3
alec-pinson May 25, 2025
8853022
cleanup
alec-pinson May 27, 2025
21255cf
Merge branch 'dev' into CleanV3-new-api-endpoint
alec-pinson May 27, 2025
7aa7491
configured precommit
alec-pinson May 27, 2025
5820e17
fix precommit
alec-pinson May 27, 2025
073b0d7
remove extra logging
alec-pinson May 27, 2025
b06ba3c
doc string
alec-pinson May 27, 2025
6f30c3d
Fix pre-commit findings
edenhaus Aug 11, 2025
6337711
Merge branch 'dev' into pr/JNickson/987
edenhaus Aug 11, 2025
eaa7c07
Create DeviceAuthenticator
edenhaus Aug 11, 2025
da7a42c
formatting
edenhaus Aug 11, 2025
1eedbbc
Fix wrong content type
edenhaus Aug 12, 2025
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
410 changes: 302 additions & 108 deletions deebot_client/authentication.py

Large diffs are not rendered by default.

32 changes: 3 additions & 29 deletions deebot_client/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
)
from deebot_client.util import verify_required_class_variables_exists

from .const import PATH_API_IOT_DEVMANAGER, REQUEST_HEADERS, DataType
from .logging_filter import get_logger
from .message import HandlingResult, HandlingState, Message

if TYPE_CHECKING:
from types import MappingProxyType

from .authentication import Authenticator
from .const import DataType
from .event_bus import EventBus
from .models import ApiDeviceInfo

Expand Down Expand Up @@ -78,7 +78,7 @@ def __init__(self, args: dict[str, Any] | list[Any] | None = None) -> None:
self._args = args

@abstractmethod
def _get_payload(self) -> dict[str, Any] | list[Any] | str:
def get_payload(self) -> dict[str, Any] | list[Any] | str:
"""Get the payload for the rest call."""

@final
Expand Down Expand Up @@ -151,33 +151,7 @@ async def _execute(
async def _execute_api_request(
self, authenticator: Authenticator, device_info: ApiDeviceInfo
) -> dict[str, Any]:
payload = {
"cmdName": self.NAME,
"payload": self._get_payload(),
"payloadType": self.DATA_TYPE.value,
"td": "q",
"toId": device_info["did"],
"toRes": device_info["resource"],
"toType": device_info["class"],
}

credentials = await authenticator.authenticate()
query_params = {
"mid": payload["toType"],
"did": payload["toId"],
"td": payload["td"],
"u": credentials.user_id,
"cv": "1.67.3",
"t": "a",
"av": "1.3.1",
}

return await authenticator.post_authenticated(
PATH_API_IOT_DEVMANAGER,
payload,
query_params=query_params,
headers=REQUEST_HEADERS,
)
return await authenticator.execute_command_request(self, device_info)

def __handle_response(
self, event_bus: EventBus, response: dict[str, Any]
Expand Down
3 changes: 2 additions & 1 deletion deebot_client/commands/json/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ class JsonCommand(Command, ABC):

DATA_TYPE = DataType.JSON

def _get_payload(self) -> dict[str, Any] | list[Any]:
def get_payload(self) -> dict[str, Any] | list[Any]:
"""Get the payload for the rest call."""
payload = {
"header": {
"pri": "1",
Expand Down
3 changes: 2 additions & 1 deletion deebot_client/commands/xml/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class XmlCommand(Command, ABC):
DATA_TYPE = DataType.XML
HAS_SUB_ELEMENT = False

def _get_payload(self) -> str:
def get_payload(self) -> str:
"""Get the payload for the rest call."""
element = ctl_element = Element("ctl")

if len(self._args) > 0:
Expand Down
7 changes: 6 additions & 1 deletion deebot_client/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
from enum import Enum, StrEnum
from typing import Self

from aiohttp import hdrs

REALM = "ecouser.net"
PATH_API_APPSVR_APP = "appsvr/app.do"
PATH_API_PIM_PRODUCT_IOT_MAP = "pim/product/getProductIotMap"
PATH_API_IOT_DEVMANAGER = "iot/devmanager.do"
PATH_API_IOT_CONTROL = "iot/endpoint/control"
PATH_API_ISSUE_NEW_PERMISSION = "new-perm/token/sst/issue"
PATH_API_LG_LOG = "lg/log.do"
PATH_API_USERS_USER = "users/user.do"
REQUEST_HEADERS = {
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 5.1.1; A5010 Build/LMY48Z)",
hdrs.USER_AGENT: "Dalvik/2.1.0 (Linux; U; Android 5.1.1; A5010 Build/LMY48Z)",
}
COUNTRY_CHINA = "CN"

Expand Down Expand Up @@ -104,6 +108,7 @@ class UndefinedType(Enum):
319: "Cleaning solution is running low",
404: "Recipient unavailable",
500: "Request Timeout",
505: "An error occurred, please clear it and try again",
601: "ERROR_ClosedAIVISideAbnormal",
602: "ClosedAIVIRollAbnormal",
1007: "Mop plugged",
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,12 @@ ignore = [
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false


[tool.ruff.lint.isort]
combine-as-imports = true
force-sort-within-sections = true
known-first-party = ["deebot_client"]
required-imports = ["from __future__ import annotations"]


[tool.ruff.lint.per-file-ignores]
"tests/fixtures/**" = [
"T201", # print found
Expand Down Expand Up @@ -153,3 +151,6 @@ required-imports = ["from __future__ import annotations"]

[tool.ruff.lint.mccabe]
max-complexity = 13

[tool.ruff.lint.pylint]
max-args = 6
4 changes: 2 additions & 2 deletions scripts/check_for_similar_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import aiohttp

from deebot_client.api_client import ApiClient
from deebot_client.authentication import Authenticator, create_rest_config
from deebot_client.authentication import UserAuthenticator, create_rest_config
from deebot_client.hardware.deebot import DEVICES, _load
from deebot_client.util import md5

Expand Down Expand Up @@ -58,7 +58,7 @@ async def main() -> None:
alpha_2_country=os.environ["ECOVACS_COUNTRY"],
)

authenticator = Authenticator(
authenticator = UserAuthenticator(
rest, os.environ["ECOVACS_USERNAME"], md5(os.environ["ECOVACS_PASSWORD"])
)
api_client = ApiClient(authenticator)
Expand Down
8 changes: 5 additions & 3 deletions tests/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ async def assert_command(
*,
command_result: CommandResult | None = None,
expected_raw_response: dict[str, Any] | None = None,
mock_authenticator_func_name: str = "execute_command_request",
) -> None:
command_result = command_result or CommandResult.success()
event_bus = Mock(spec_set=EventBus)
Expand All @@ -59,11 +60,12 @@ async def assert_command(
return_value=Credentials("token", "user_id", 9999)
)
if isinstance(json_api_response, tuple):
authenticator.post_authenticated = AsyncMock(side_effect=json_api_response)
mock = AsyncMock(side_effect=json_api_response)
else:
authenticator.post_authenticated = AsyncMock(return_value=json_api_response)
mock = AsyncMock(return_value=json_api_response)
if expected_raw_response is None:
expected_raw_response = json_api_response
setattr(authenticator, mock_authenticator_func_name, mock)
device_info = ApiDeviceInfo(
{
"company": "company",
Expand All @@ -81,7 +83,7 @@ async def assert_command(

# verify
verify_result(command_result, expected_raw_response)
authenticator.post_authenticated.assert_called()
getattr(authenticator, mock_authenticator_func_name).assert_called()
if expected_events:
if isinstance(expected_events, Sequence):
event_bus.notify.assert_has_calls([call(x) for x in expected_events])
Expand Down
9 changes: 8 additions & 1 deletion tests/commands/json/test_clean_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,12 @@ async def test_GetCleanLogs(caplog: pytest.LogCaptureFixture) -> None:
]
)

await assert_command(GetCleanLogs(), json, expected)
await assert_command(
GetCleanLogs(),
json,
expected,
mock_authenticator_func_name="post_authenticated",
)

assert (
"deebot_client.commands.json.clean_logs",
Expand All @@ -130,6 +135,7 @@ async def test_GetCleanLogs_analyse_logged(
json,
None,
command_result=CommandResult(HandlingState.ANALYSE_LOGGED),
mock_authenticator_func_name="post_authenticated",
)

assert (
Expand All @@ -145,6 +151,7 @@ async def test_GetCleanLogs_handle_error(caplog: pytest.LogCaptureFixture) -> No
{},
None,
command_result=CommandResult(HandlingState.ERROR),
mock_authenticator_func_name="post_authenticated",
)

assert (
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/json/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async def test_common_functionality(
assert_func: Callable[[Mock], None],
caplog: pytest.LogCaptureFixture,
) -> None:
authenticator.post_authenticated.return_value = response_json
authenticator.execute_command_request.return_value = response_json
event_bus = Mock(spec_set=EventBus)

available = await command.execute(authenticator, api_device_info, event_bus)
Expand Down
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ def rest_config(
def authenticator() -> Authenticator:
authenticator = Mock(spec_set=Authenticator)
authenticator.authenticate.return_value = Credentials("token", "user_id", 9999)
authenticator.post_authenticated.return_value = {
authenticator.execute_command_request.return_value = (
authenticator.post_authenticated.return_value
) = {
"header": {
"pri": 1,
"tzm": 480,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from deebot_client.authentication import Authenticator, create_rest_config
from deebot_client.authentication import UserAuthenticator, create_rest_config
from deebot_client.models import Credentials

if TYPE_CHECKING:
Expand All @@ -29,7 +29,7 @@ async def on_changed(_: Credentials) -> None:
login_mock.return_value = Credentials(
"token", "user_id", int(time.time() + 123456789)
)
authenticator = Authenticator(rest_config, "test", "test")
authenticator = UserAuthenticator(rest_config, "test", "test")

unsub = authenticator.subscribe(on_changed)

Expand Down
5 changes: 3 additions & 2 deletions tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ def _handle_mqtt_p2p(
) -> None:
pass

def _get_payload(self) -> dict[str, Any] | list[Any]:
def get_payload(self) -> dict[str, Any] | list[Any]:
"""Get the payload for the rest call."""
return {}

def _handle_response(
Expand Down Expand Up @@ -103,7 +104,7 @@ async def test_execute_api_timeout_error(
) -> None:
"""Test that on api timeout the stack trace is not logged."""
command = _TestCommand(1)
authenticator.post_authenticated.side_effect = ApiTimeoutError(
authenticator.execute_command_request.side_effect = ApiTimeoutError(
"test", ClientTimeout(60)
)
result = await command.execute(authenticator, api_device_info, event_bus_mock)
Expand Down