Skip to content

Commit d623512

Browse files
authored
Merge branch 'dev' into xml_playsound
2 parents 7aef39d + e557053 commit d623512

File tree

137 files changed

+1979
-658
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

137 files changed

+1979
-658
lines changed

.github/workflows/ci.yml

+15-14
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,13 @@ jobs:
1818
- name: ⤵️ Checkout repository
1919
uses: actions/checkout@v4
2020

21-
- name: 🏗 Install uv
22-
uses: astral-sh/setup-uv@v2
21+
- name: 🏗 Install uv and Python
22+
uses: astral-sh/setup-uv@v4
2323
with:
2424
enable-cache: true
2525
cache-dependency-glob: "uv.lock"
2626
cache-local-path: ${{ env.UV_CACHE_DIR }}
2727

28-
- name: 🏗 Set up Python
29-
uses: actions/setup-python@v5
30-
with:
31-
python-version-file: ".python-version"
32-
3328
- name: 🏗 Install the project
3429
run: uv sync --locked --dev
3530

@@ -50,28 +45,34 @@ jobs:
5045
matrix:
5146
python-version:
5247
- "3.12"
48+
- "3.13"
5349
steps:
5450
- name: ⤵️ Checkout repository
5551
uses: actions/checkout@v4
5652

57-
- name: 🏗 Install uv
58-
uses: astral-sh/setup-uv@v2
53+
- name: 🏗 Install uv and Python ${{ matrix.python-version }}
54+
uses: astral-sh/setup-uv@v4
5955
with:
6056
enable-cache: true
6157
cache-dependency-glob: "uv.lock"
6258
cache-local-path: ${{ env.UV_CACHE_DIR }}
63-
64-
- name: 🏗 Set up Python ${{ matrix.python-version }}
65-
run: uv python install ${{ matrix.python-version }}
59+
python-version: ${{ matrix.python-version }}
6660

6761
- name: 🏗 Install the project
6862
run: uv sync --locked --dev
6963

7064
- name: Run pytest
71-
run: uv run --frozen pytest tests --cov=./ --cov-report=xml
65+
run: uv run --frozen pytest tests --cov=./ --cov-report=xml --junitxml=junit.xml -o junit_family=legacy
7266

7367
- name: Upload coverage to Codecov
74-
uses: codecov/codecov-action@v4
68+
uses: codecov/codecov-action@v5
69+
with:
70+
token: ${{ secrets.CODECOV_TOKEN }}
71+
fail_ci_if_error: true
72+
73+
- name: Upload test results to Codecov
74+
if: ${{ !cancelled() }}
75+
uses: codecov/test-results-action@v1
7576
with:
7677
token: ${{ secrets.CODECOV_TOKEN }}
7778
fail_ci_if_error: true

.github/workflows/python-publish.yml

+15-28
Original file line numberDiff line numberDiff line change
@@ -12,43 +12,30 @@ on:
1212
release:
1313
types: [published]
1414

15-
env:
16-
UV_CACHE_DIR: /tmp/.uv-cache
17-
1815
jobs:
19-
deploy:
16+
release:
17+
name: Releasing to PyPi
2018
runs-on: ubuntu-latest
21-
environment: release
19+
environment:
20+
name: release
21+
url: https://pypi.org/manage/project/deebot-client/
2222
permissions:
23+
contents: write
2324
id-token: write
2425
steps:
25-
- name: ⤵️ Checkout repository
26-
uses: actions/checkout@v4
27-
28-
- name: 🏗 Install uv
29-
uses: astral-sh/setup-uv@v2
26+
- name: ⤵️ Check out code from GitHub
27+
uses: actions/[email protected]
28+
- name: 🏗 Set up uv
29+
uses: astral-sh/setup-uv@v4
3030
with:
3131
enable-cache: true
32-
cache-dependency-glob: "uv.lock"
33-
cache-local-path: ${{ env.UV_CACHE_DIR }}
34-
35-
- name: 🏗 Set up Python
36-
uses: actions/setup-python@v5
37-
with:
38-
python-version-file: ".python-version"
39-
40-
- name: 🏗 Install the project
41-
run: uv sync --dev --locked
42-
32+
- name: 🏗 Set package version
33+
run: |
34+
sed -i "s/^version = \".*\"/version = \"${{ github.event.release.tag_name }}\"/" pyproject.toml
4335
- name: 📦 Build package
4436
run: uv build
45-
46-
- name: 🚀 Publish package
47-
uses: pypa/gh-action-pypi-publish@release/v1
48-
with:
49-
verbose: true
50-
print-hash: true
51-
37+
- name: 🚀 Publish to PyPi
38+
run: uv publish
5239
- name: ✍️ Sign published artifacts
5340
uses: sigstore/[email protected]
5441
with:

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ nosetests.xml
2121
.*_cache
2222

2323
test.py
24+
.env

.pre-commit-config.yaml

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ ci:
77
- verifyNoGetLogger
88

99
default_language_version:
10-
python: python3.12
10+
python: python3.13
1111

1212
repos:
1313
- repo: https://github.com/astral-sh/ruff-pre-commit
14-
rev: v0.6.4
14+
rev: v0.8.3
1515
hooks:
1616
- id: ruff
1717
args:
1818
- --fix
1919
- --unsafe-fixes
2020
- id: ruff-format
2121
- repo: https://github.com/asottile/pyupgrade
22-
rev: v3.17.0
22+
rev: v3.19.0
2323
hooks:
2424
- id: pyupgrade
2525
args:
@@ -38,7 +38,7 @@ repos:
3838
- json
3939
exclude: ^uv.lock$
4040
- repo: https://github.com/pre-commit/pre-commit-hooks
41-
rev: v4.6.0
41+
rev: v5.0.0
4242
hooks:
4343
- id: check-executables-have-shebangs
4444
- id: check-merge-conflict

.python-version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.12
1+
3.13.0

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async def main():
4747

4848
devices_ = await api_client.get_devices()
4949

50-
bot = Device(devices_[0], authenticator)
50+
bot = Device(devices_.mqtt[0], authenticator)
5151

5252
mqtt_config = create_mqtt_config(device_id=device_id, country=country)
5353
mqtt = MqttClient(mqtt_config, authenticator)

deebot_client/api_client.py

+32-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import asyncio
6+
from dataclasses import dataclass
67
from typing import TYPE_CHECKING, Any
78

89
from deebot_client.hardware.deebot import get_static_device_info
@@ -22,6 +23,15 @@
2223
_LOGGER = get_logger(__name__)
2324

2425

26+
@dataclass(frozen=True)
27+
class Devices:
28+
"""Devices."""
29+
30+
mqtt: list[DeviceInfo]
31+
xmpp: list[ApiDeviceInfo]
32+
not_supported: list[ApiDeviceInfo]
33+
34+
2535
class ApiClient:
2636
"""Api client."""
2737

@@ -46,7 +56,7 @@ async def _get_devices(self, path: str, command: str) -> dict[str, ApiDeviceInfo
4656

4757
return result
4858

49-
async def get_devices(self) -> list[DeviceInfo | ApiDeviceInfo]:
59+
async def get_devices(self) -> Devices:
5060
"""Get compatible devices."""
5161
try:
5262
async with asyncio.TaskGroup() as tg:
@@ -62,21 +72,35 @@ async def get_devices(self) -> list[DeviceInfo | ApiDeviceInfo]:
6272
api_devices = task_device_list.result()
6373
api_devices.update(task_global_device_list.result())
6474

65-
devices: list[DeviceInfo | ApiDeviceInfo] = []
75+
mqtt: list[DeviceInfo] = []
76+
xmpp: list[ApiDeviceInfo] = []
77+
not_supported: list[ApiDeviceInfo] = []
6678
for device in api_devices.values():
6779
match device.get("company"):
6880
case "eco-ng":
69-
static_device_info = await get_static_device_info(device["class"])
70-
devices.append(DeviceInfo(device, static_device_info))
81+
if static_device_info := await get_static_device_info(
82+
device["class"]
83+
):
84+
mqtt.append(DeviceInfo(device, static_device_info))
85+
else:
86+
_LOGGER.warning(
87+
'Device class "%s" not recognized. Please add support for it: %s',
88+
device["class"],
89+
device,
90+
)
91+
not_supported.append(device)
7192
case "eco-legacy":
72-
devices.append(device)
93+
xmpp.append(device)
7394
case _:
74-
_LOGGER.debug("Skipping device as it is not supported: %s", device)
95+
_LOGGER.warning(
96+
"Skipping device as it is not supported: %s", device
97+
)
98+
not_supported.append(device)
7599

76-
if not devices:
100+
if not mqtt and not xmpp and not not_supported:
77101
_LOGGER.warning("No devices returned by the api. Please check the logs.")
78102

79-
return devices
103+
return Devices(mqtt, xmpp, not_supported)
80104

81105
async def get_product_iot_map(self) -> dict[str, Any]:
82106
"""Get product iot map."""

deebot_client/command.py

+21-26
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
ApiTimeoutError,
1313
DeebotError,
1414
)
15+
from deebot_client.util import verify_required_class_variables_exists
1516

1617
from .const import PATH_API_IOT_DEVMANAGER, REQUEST_HEADERS, DataType
1718
from .logging_filter import get_logger
@@ -64,24 +65,18 @@ class Command(ABC):
6465
"""Abstract command object."""
6566

6667
_targets_bot: bool = True
68+
NAME: str
69+
DATA_TYPE: DataType
70+
71+
def __init_subclass__(cls) -> None:
72+
verify_required_class_variables_exists(cls, ("NAME", "DATA_TYPE"))
73+
return super().__init_subclass__()
6774

6875
def __init__(self, args: dict[str, Any] | list[Any] | None = None) -> None:
6976
if args is None:
7077
args = {}
7178
self._args = args
7279

73-
@property # type: ignore[misc]
74-
@classmethod
75-
@abstractmethod
76-
def name(cls) -> str:
77-
"""Command name."""
78-
79-
@property # type: ignore[misc]
80-
@classmethod
81-
@abstractmethod
82-
def data_type(cls) -> DataType:
83-
"""Data type."""
84-
8580
@abstractmethod
8681
def _get_payload(self) -> dict[str, Any] | list[Any] | str:
8782
"""Get the payload for the rest call."""
@@ -115,7 +110,7 @@ async def execute(
115110
except Exception: # pylint: disable=broad-except
116111
_LOGGER.warning(
117112
"Could not execute command %s",
118-
self.name,
113+
self.NAME,
119114
exc_info=True,
120115
)
121116
return DeviceCommandResult(device_reached=False)
@@ -132,14 +127,14 @@ async def _execute(
132127
except ApiTimeoutError:
133128
_LOGGER.warning(
134129
"Could not execute command %s: Timeout reached",
135-
self.name,
130+
self.NAME,
136131
)
137132
return CommandResult(HandlingState.ERROR), {}
138133

139134
result = self.__handle_response(event_bus, response)
140135
if result.state == HandlingState.ANALYSE:
141136
_LOGGER.debug(
142-
"ANALYSE: Could not handle command: %s with %s", self.name, response
137+
"ANALYSE: Could not handle command: %s with %s", self.NAME, response
143138
)
144139
return (
145140
CommandResult(
@@ -150,16 +145,16 @@ async def _execute(
150145
response,
151146
)
152147
if result.state == HandlingState.ERROR:
153-
_LOGGER.warning("Could not parse %s: %s", self.name, response)
148+
_LOGGER.warning("Could not parse %s: %s", self.NAME, response)
154149
return result, response
155150

156151
async def _execute_api_request(
157152
self, authenticator: Authenticator, device_info: ApiDeviceInfo
158153
) -> dict[str, Any]:
159154
payload = {
160-
"cmdName": self.name,
155+
"cmdName": self.NAME,
161156
"payload": self._get_payload(),
162-
"payloadType": self.data_type.value,
157+
"payloadType": self.DATA_TYPE.value,
163158
"td": "q",
164159
"toId": device_info["did"],
165160
"toRes": device_info["resource"],
@@ -195,7 +190,7 @@ def __handle_response(
195190
result = self._handle_response(event_bus, response)
196191
if result.state == HandlingState.ANALYSE:
197192
_LOGGER.debug(
198-
"ANALYSE: Could not handle command: %s with %s", self.name, response
193+
"ANALYSE: Could not handle command: %s with %s", self.NAME, response
199194
)
200195
return CommandResult(
201196
HandlingState.ANALYSE_LOGGED,
@@ -206,7 +201,7 @@ def __handle_response(
206201
except Exception: # pylint: disable=broad-except
207202
_LOGGER.warning(
208203
"Could not parse response for %s: %s",
209-
self.name,
204+
self.NAME,
210205
response,
211206
exc_info=True,
212207
)
@@ -223,12 +218,12 @@ def _handle_response(
223218

224219
def __eq__(self, obj: object) -> bool:
225220
if isinstance(obj, Command):
226-
return self.name == obj.name and self._args == obj._args
221+
return self.NAME == obj.NAME and self._args == obj._args
227222

228223
return False
229224

230225
def __hash__(self) -> int:
231-
return hash(self.name) + hash(self._args)
226+
return hash(self.NAME) + hash(self._args)
232227

233228

234229
class CommandWithMessageHandling(Command, Message, ABC):
@@ -253,24 +248,24 @@ def _handle_response(
253248
case 4200:
254249
# bot offline
255250
_LOGGER.info(
256-
'Device is offline. Could not execute command "%s"', self.name
251+
'Device is offline. Could not execute command "%s"', self.NAME
257252
)
258253
event_bus.notify(AvailabilityEvent(available=False))
259254
return CommandResult(HandlingState.FAILED)
260255
case 500:
261256
if self._is_available_check:
262257
_LOGGER.info(
263258
'No response received for command "%s" during availability-check.',
264-
self.name,
259+
self.NAME,
265260
)
266261
else:
267262
_LOGGER.warning(
268263
'No response received for command "%s". This can happen if the device has network issues or does not support the command',
269-
self.name,
264+
self.NAME,
270265
)
271266
return CommandResult(HandlingState.FAILED)
272267

273-
_LOGGER.warning('Command "%s" was not successfully.', self.name)
268+
_LOGGER.warning('Command "%s" was not successfully.', self.NAME)
274269
return CommandResult(HandlingState.ANALYSE)
275270

276271

0 commit comments

Comments
 (0)