Skip to content

Commit

Permalink
Merge branch 'dev' into xml_playsound
Browse files Browse the repository at this point in the history
  • Loading branch information
edenhaus authored Dec 17, 2024
2 parents 7aef39d + e557053 commit d623512
Show file tree
Hide file tree
Showing 137 changed files with 1,979 additions and 658 deletions.
29 changes: 15 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,13 @@ jobs:
- name: ⤵️ Checkout repository
uses: actions/checkout@v4

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

- name: 🏗 Set up Python
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

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

Expand All @@ -50,28 +45,34 @@ jobs:
matrix:
python-version:
- "3.12"
- "3.13"
steps:
- name: ⤵️ Checkout repository
uses: actions/checkout@v4

- name: 🏗 Install uv
uses: astral-sh/setup-uv@v2
- name: 🏗 Install uv and Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
cache-local-path: ${{ env.UV_CACHE_DIR }}

- name: 🏗 Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
python-version: ${{ matrix.python-version }}

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

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

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true

- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
43 changes: 15 additions & 28 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,30 @@ on:
release:
types: [published]

env:
UV_CACHE_DIR: /tmp/.uv-cache

jobs:
deploy:
release:
name: Releasing to PyPi
runs-on: ubuntu-latest
environment: release
environment:
name: release
url: https://pypi.org/manage/project/deebot-client/
permissions:
contents: write
id-token: write
steps:
- name: ⤵️ Checkout repository
uses: actions/checkout@v4

- name: 🏗 Install uv
uses: astral-sh/setup-uv@v2
- name: ⤵️ Check out code from GitHub
uses: actions/[email protected]
- name: 🏗 Set up uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
cache-local-path: ${{ env.UV_CACHE_DIR }}

- name: 🏗 Set up Python
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"

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

- name: 🏗 Set package version
run: |
sed -i "s/^version = \".*\"/version = \"${{ github.event.release.tag_name }}\"/" pyproject.toml
- name: 📦 Build package
run: uv build

- name: 🚀 Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true
print-hash: true

- name: 🚀 Publish to PyPi
run: uv publish
- name: ✍️ Sign published artifacts
uses: sigstore/[email protected]
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ nosetests.xml
.*_cache

test.py
.env
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ ci:
- verifyNoGetLogger

default_language_version:
python: python3.12
python: python3.13

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.4
rev: v0.8.3
hooks:
- id: ruff
args:
- --fix
- --unsafe-fixes
- id: ruff-format
- repo: https://github.com/asottile/pyupgrade
rev: v3.17.0
rev: v3.19.0
hooks:
- id: pyupgrade
args:
Expand All @@ -38,7 +38,7 @@ repos:
- json
exclude: ^uv.lock$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-executables-have-shebangs
- id: check-merge-conflict
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12
3.13.0
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async def main():

devices_ = await api_client.get_devices()

bot = Device(devices_[0], authenticator)
bot = Device(devices_.mqtt[0], authenticator)

mqtt_config = create_mqtt_config(device_id=device_id, country=country)
mqtt = MqttClient(mqtt_config, authenticator)
Expand Down
40 changes: 32 additions & 8 deletions deebot_client/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import asyncio
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

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


@dataclass(frozen=True)
class Devices:
"""Devices."""

mqtt: list[DeviceInfo]
xmpp: list[ApiDeviceInfo]
not_supported: list[ApiDeviceInfo]


class ApiClient:
"""Api client."""

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

return result

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

devices: list[DeviceInfo | ApiDeviceInfo] = []
mqtt: list[DeviceInfo] = []
xmpp: list[ApiDeviceInfo] = []
not_supported: list[ApiDeviceInfo] = []
for device in api_devices.values():
match device.get("company"):
case "eco-ng":
static_device_info = await get_static_device_info(device["class"])
devices.append(DeviceInfo(device, static_device_info))
if static_device_info := await get_static_device_info(
device["class"]
):
mqtt.append(DeviceInfo(device, static_device_info))
else:
_LOGGER.warning(
'Device class "%s" not recognized. Please add support for it: %s',
device["class"],
device,
)
not_supported.append(device)
case "eco-legacy":
devices.append(device)
xmpp.append(device)
case _:
_LOGGER.debug("Skipping device as it is not supported: %s", device)
_LOGGER.warning(
"Skipping device as it is not supported: %s", device
)
not_supported.append(device)

if not devices:
if not mqtt and not xmpp and not not_supported:
_LOGGER.warning("No devices returned by the api. Please check the logs.")

return devices
return Devices(mqtt, xmpp, not_supported)

async def get_product_iot_map(self) -> dict[str, Any]:
"""Get product iot map."""
Expand Down
47 changes: 21 additions & 26 deletions deebot_client/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ApiTimeoutError,
DeebotError,
)
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
Expand Down Expand Up @@ -64,24 +65,18 @@ class Command(ABC):
"""Abstract command object."""

_targets_bot: bool = True
NAME: str
DATA_TYPE: DataType

def __init_subclass__(cls) -> None:
verify_required_class_variables_exists(cls, ("NAME", "DATA_TYPE"))
return super().__init_subclass__()

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

@property # type: ignore[misc]
@classmethod
@abstractmethod
def name(cls) -> str:
"""Command name."""

@property # type: ignore[misc]
@classmethod
@abstractmethod
def data_type(cls) -> DataType:
"""Data type."""

@abstractmethod
def _get_payload(self) -> dict[str, Any] | list[Any] | str:
"""Get the payload for the rest call."""
Expand Down Expand Up @@ -115,7 +110,7 @@ async def execute(
except Exception: # pylint: disable=broad-except
_LOGGER.warning(
"Could not execute command %s",
self.name,
self.NAME,
exc_info=True,
)
return DeviceCommandResult(device_reached=False)
Expand All @@ -132,14 +127,14 @@ async def _execute(
except ApiTimeoutError:
_LOGGER.warning(
"Could not execute command %s: Timeout reached",
self.name,
self.NAME,
)
return CommandResult(HandlingState.ERROR), {}

result = self.__handle_response(event_bus, response)
if result.state == HandlingState.ANALYSE:
_LOGGER.debug(
"ANALYSE: Could not handle command: %s with %s", self.name, response
"ANALYSE: Could not handle command: %s with %s", self.NAME, response
)
return (
CommandResult(
Expand All @@ -150,16 +145,16 @@ async def _execute(
response,
)
if result.state == HandlingState.ERROR:
_LOGGER.warning("Could not parse %s: %s", self.name, response)
_LOGGER.warning("Could not parse %s: %s", self.NAME, response)
return result, response

async def _execute_api_request(
self, authenticator: Authenticator, device_info: ApiDeviceInfo
) -> dict[str, Any]:
payload = {
"cmdName": self.name,
"cmdName": self.NAME,
"payload": self._get_payload(),
"payloadType": self.data_type.value,
"payloadType": self.DATA_TYPE.value,
"td": "q",
"toId": device_info["did"],
"toRes": device_info["resource"],
Expand Down Expand Up @@ -195,7 +190,7 @@ def __handle_response(
result = self._handle_response(event_bus, response)
if result.state == HandlingState.ANALYSE:
_LOGGER.debug(
"ANALYSE: Could not handle command: %s with %s", self.name, response
"ANALYSE: Could not handle command: %s with %s", self.NAME, response
)
return CommandResult(
HandlingState.ANALYSE_LOGGED,
Expand All @@ -206,7 +201,7 @@ def __handle_response(
except Exception: # pylint: disable=broad-except
_LOGGER.warning(
"Could not parse response for %s: %s",
self.name,
self.NAME,
response,
exc_info=True,
)
Expand All @@ -223,12 +218,12 @@ def _handle_response(

def __eq__(self, obj: object) -> bool:
if isinstance(obj, Command):
return self.name == obj.name and self._args == obj._args
return self.NAME == obj.NAME and self._args == obj._args

return False

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


class CommandWithMessageHandling(Command, Message, ABC):
Expand All @@ -253,24 +248,24 @@ def _handle_response(
case 4200:
# bot offline
_LOGGER.info(
'Device is offline. Could not execute command "%s"', self.name
'Device is offline. Could not execute command "%s"', self.NAME
)
event_bus.notify(AvailabilityEvent(available=False))
return CommandResult(HandlingState.FAILED)
case 500:
if self._is_available_check:
_LOGGER.info(
'No response received for command "%s" during availability-check.',
self.name,
self.NAME,
)
else:
_LOGGER.warning(
'No response received for command "%s". This can happen if the device has network issues or does not support the command',
self.name,
self.NAME,
)
return CommandResult(HandlingState.FAILED)

_LOGGER.warning('Command "%s" was not successfully.', self.name)
_LOGGER.warning('Command "%s" was not successfully.', self.NAME)
return CommandResult(HandlingState.ANALYSE)


Expand Down
Loading

0 comments on commit d623512

Please sign in to comment.