From ec366b2462a8476a2d1a1e8d22499d3207fca77f Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 25 Nov 2024 11:53:06 +0100 Subject: [PATCH] Remove fallback and return unsupported devices too (#600) --- .github/workflows/ci.yml | 1 + .pre-commit-config.yaml | 2 +- README.md | 2 +- deebot_client/api_client.py | 40 ++++- deebot_client/hardware/deebot/__init__.py | 10 +- deebot_client/hardware/deebot/fallback.py | 184 ---------------------- pyproject.toml | 3 +- tests/conftest.py | 6 +- tests/hardware/test_init.py | 45 +----- 9 files changed, 49 insertions(+), 244 deletions(-) delete mode 100644 deebot_client/hardware/deebot/fallback.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0e30ba0..8f1d95d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,7 @@ jobs: matrix: python-version: - "3.12" + - "3.13" steps: - name: ⤵️ Checkout repository uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c948d610..392040d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ ci: - verifyNoGetLogger default_language_version: - python: python3.12 + python: python3.13 repos: - repo: https://github.com/astral-sh/ruff-pre-commit diff --git a/README.md b/README.md index 79d7592e..ae5a902d 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/deebot_client/api_client.py b/deebot_client/api_client.py index 987ff99c..f767a87b 100644 --- a/deebot_client/api_client.py +++ b/deebot_client/api_client.py @@ -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 @@ -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.""" @@ -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: @@ -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.""" diff --git a/deebot_client/hardware/deebot/__init__.py b/deebot_client/hardware/deebot/__init__.py index 30eca3dd..b6d83c00 100644 --- a/deebot_client/hardware/deebot/__init__.py +++ b/deebot_client/hardware/deebot/__init__.py @@ -17,8 +17,6 @@ _LOGGER = get_logger(__name__) -FALLBACK = "fallback" - DEVICES: dict[str, StaticDeviceInfo] = {} @@ -28,7 +26,7 @@ def _load() -> None: importlib.import_module(full_package_name) -async def get_static_device_info(class_: str) -> StaticDeviceInfo: +async def get_static_device_info(class_: str) -> StaticDeviceInfo | None: """Get static device info for given class.""" if not DEVICES: await asyncio.get_event_loop().run_in_executor(None, _load) @@ -37,8 +35,4 @@ async def get_static_device_info(class_: str) -> StaticDeviceInfo: _LOGGER.debug("Capabilities found for %s", class_) return device - _LOGGER.info( - "No capabilities found for %s, therefore not all features are available. trying to use fallback...", - class_, - ) - return DEVICES[FALLBACK] + return None diff --git a/deebot_client/hardware/deebot/fallback.py b/deebot_client/hardware/deebot/fallback.py deleted file mode 100644 index c5deec88..00000000 --- a/deebot_client/hardware/deebot/fallback.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Fallback Capabilities.""" - -from __future__ import annotations - -from deebot_client.capabilities import ( - Capabilities, - CapabilityClean, - CapabilityCleanAction, - CapabilityCustomCommand, - CapabilityEvent, - CapabilityExecute, - CapabilityLifeSpan, - CapabilityMap, - CapabilitySet, - CapabilitySetEnable, - CapabilitySettings, - CapabilitySetTypes, - CapabilityStats, - DeviceType, -) -from deebot_client.commands.json.advanced_mode import GetAdvancedMode, SetAdvancedMode -from deebot_client.commands.json.battery import GetBattery -from deebot_client.commands.json.carpet import ( - GetCarpetAutoFanBoost, - SetCarpetAutoFanBoost, -) -from deebot_client.commands.json.charge import Charge -from deebot_client.commands.json.charge_state import GetChargeState -from deebot_client.commands.json.clean import Clean, CleanArea, GetCleanInfo -from deebot_client.commands.json.clean_count import GetCleanCount, SetCleanCount -from deebot_client.commands.json.clean_logs import GetCleanLogs -from deebot_client.commands.json.clean_preference import ( - GetCleanPreference, - SetCleanPreference, -) -from deebot_client.commands.json.continuous_cleaning import ( - GetContinuousCleaning, - SetContinuousCleaning, -) -from deebot_client.commands.json.custom import CustomCommand -from deebot_client.commands.json.error import GetError -from deebot_client.commands.json.fan_speed import GetFanSpeed, SetFanSpeed -from deebot_client.commands.json.life_span import GetLifeSpan, ResetLifeSpan -from deebot_client.commands.json.map import GetCachedMapInfo, GetMajorMap, GetMapTrace -from deebot_client.commands.json.multimap_state import ( - GetMultimapState, - SetMultimapState, -) -from deebot_client.commands.json.network import GetNetInfo -from deebot_client.commands.json.play_sound import PlaySound -from deebot_client.commands.json.pos import GetPos -from deebot_client.commands.json.relocation import SetRelocationState -from deebot_client.commands.json.stats import GetStats, GetTotalStats -from deebot_client.commands.json.true_detect import GetTrueDetect, SetTrueDetect -from deebot_client.commands.json.volume import GetVolume, SetVolume -from deebot_client.commands.json.water_info import GetWaterInfo, SetWaterInfo -from deebot_client.const import DataType -from deebot_client.events import ( - AdvancedModeEvent, - AvailabilityEvent, - BatteryEvent, - CachedMapInfoEvent, - CarpetAutoFanBoostEvent, - CleanCountEvent, - CleanLogEvent, - CleanPreferenceEvent, - ContinuousCleaningEvent, - CustomCommandEvent, - ErrorEvent, - FanSpeedEvent, - FanSpeedLevel, - LifeSpan, - LifeSpanEvent, - MajorMapEvent, - MapChangedEvent, - MapTraceEvent, - MultimapStateEvent, - NetworkInfoEvent, - PositionsEvent, - ReportStatsEvent, - RoomsEvent, - StateEvent, - StatsEvent, - TotalStatsEvent, - TrueDetectEvent, - VolumeEvent, - WaterAmount, - WaterInfoEvent, -) -from deebot_client.models import StaticDeviceInfo -from deebot_client.util import short_name - -from . import DEVICES - -DEVICES[short_name(__name__)] = StaticDeviceInfo( - DataType.JSON, - Capabilities( - device_type=DeviceType.VACUUM, - availability=CapabilityEvent( - AvailabilityEvent, [GetBattery(is_available_check=True)] - ), - battery=CapabilityEvent(BatteryEvent, [GetBattery()]), - charge=CapabilityExecute(Charge), - clean=CapabilityClean( - action=CapabilityCleanAction(command=Clean, area=CleanArea), - continuous=CapabilitySetEnable( - ContinuousCleaningEvent, - [GetContinuousCleaning()], - SetContinuousCleaning, - ), - count=CapabilitySet(CleanCountEvent, [GetCleanCount()], SetCleanCount), - log=CapabilityEvent(CleanLogEvent, [GetCleanLogs()]), - preference=CapabilitySetEnable( - CleanPreferenceEvent, [GetCleanPreference()], SetCleanPreference - ), - ), - custom=CapabilityCustomCommand( - event=CustomCommandEvent, get=[], set=CustomCommand - ), - error=CapabilityEvent(ErrorEvent, [GetError()]), - fan_speed=CapabilitySetTypes( - event=FanSpeedEvent, - get=[GetFanSpeed()], - set=SetFanSpeed, - types=( - FanSpeedLevel.QUIET, - FanSpeedLevel.NORMAL, - FanSpeedLevel.MAX, - FanSpeedLevel.MAX_PLUS, - ), - ), - life_span=CapabilityLifeSpan( - types=(LifeSpan.BRUSH, LifeSpan.FILTER, LifeSpan.SIDE_BRUSH), - event=LifeSpanEvent, - get=[GetLifeSpan([LifeSpan.BRUSH, LifeSpan.FILTER, LifeSpan.SIDE_BRUSH])], - reset=ResetLifeSpan, - ), - map=CapabilityMap( - cached_info=CapabilityEvent(CachedMapInfoEvent, [GetCachedMapInfo()]), - changed=CapabilityEvent(MapChangedEvent, []), - major=CapabilityEvent(MajorMapEvent, [GetMajorMap()]), - multi_state=CapabilitySetEnable( - MultimapStateEvent, [GetMultimapState()], SetMultimapState - ), - position=CapabilityEvent(PositionsEvent, [GetPos()]), - relocation=CapabilityExecute(SetRelocationState), - rooms=CapabilityEvent(RoomsEvent, [GetCachedMapInfo()]), - trace=CapabilityEvent(MapTraceEvent, [GetMapTrace()]), - ), - network=CapabilityEvent(NetworkInfoEvent, [GetNetInfo()]), - play_sound=CapabilityExecute(PlaySound), - settings=CapabilitySettings( - advanced_mode=CapabilitySetEnable( - AdvancedModeEvent, [GetAdvancedMode()], SetAdvancedMode - ), - carpet_auto_fan_boost=CapabilitySetEnable( - CarpetAutoFanBoostEvent, - [GetCarpetAutoFanBoost()], - SetCarpetAutoFanBoost, - ), - true_detect=CapabilitySetEnable( - TrueDetectEvent, [GetTrueDetect()], SetTrueDetect - ), - volume=CapabilitySet(VolumeEvent, [GetVolume()], SetVolume), - ), - state=CapabilityEvent(StateEvent, [GetChargeState(), GetCleanInfo()]), - stats=CapabilityStats( - clean=CapabilityEvent(StatsEvent, [GetStats()]), - report=CapabilityEvent(ReportStatsEvent, []), - total=CapabilityEvent(TotalStatsEvent, [GetTotalStats()]), - ), - water=CapabilitySetTypes( - event=WaterInfoEvent, - get=[GetWaterInfo()], - set=SetWaterInfo, - types=( - WaterAmount.LOW, - WaterAmount.MEDIUM, - WaterAmount.HIGH, - WaterAmount.ULTRAHIGH, - ), - ), - ), -) diff --git a/pyproject.toml b/pyproject.toml index af07c341..923c91f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Home Automation", "Topic :: Software Development :: Libraries :: Python Modules", ] @@ -133,7 +134,7 @@ max-args = 7 [tool.pylint.MAIN] -py-version = "3.12" +py-version = "3.13" ignore = [ "tests", ] diff --git a/tests/conftest.py b/tests/conftest.py index bbcc1af2..dd41b1b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ create_rest_config as create_config_rest, ) from deebot_client.event_bus import EventBus -from deebot_client.hardware.deebot import FALLBACK, get_static_device_info +from deebot_client.hardware.deebot import get_static_device_info from deebot_client.models import ( ApiDeviceInfo, Credentials, @@ -127,7 +127,9 @@ async def test_mqtt_client( @pytest.fixture async def static_device_info() -> StaticDeviceInfo: - return await get_static_device_info(FALLBACK) + info = await get_static_device_info("yna5xi") + assert info is not None + return info @pytest.fixture diff --git a/tests/hardware/test_init.py b/tests/hardware/test_init.py index ac8a370d..d6dc4100 100644 --- a/tests/hardware/test_init.py +++ b/tests/hardware/test_init.py @@ -79,7 +79,7 @@ from deebot_client.events.network import NetworkInfoEvent from deebot_client.events.water_info import WaterInfoEvent from deebot_client.hardware import get_static_device_info -from deebot_client.hardware.deebot import DEVICES, FALLBACK, _load +from deebot_client.hardware.deebot import DEVICES, _load if TYPE_CHECKING: from collections.abc import Callable @@ -92,7 +92,7 @@ @pytest.mark.parametrize( ("class_", "expected"), [ - ("not_specified", lambda: DEVICES[FALLBACK]), + ("not_specified", lambda: None), ("yna5xi", lambda: DEVICES["yna5xi"]), ], ) @@ -107,40 +107,6 @@ async def test_get_static_device_info( @pytest.mark.parametrize( ("class_", "expected"), [ - ( - FALLBACK, - { - AdvancedModeEvent: [GetAdvancedMode()], - AvailabilityEvent: [GetBattery(is_available_check=True)], - BatteryEvent: [GetBattery()], - CachedMapInfoEvent: [GetCachedMapInfo()], - CarpetAutoFanBoostEvent: [GetCarpetAutoFanBoost()], - CleanCountEvent: [GetCleanCount()], - CleanLogEvent: [GetCleanLogs()], - CleanPreferenceEvent: [GetCleanPreference()], - ContinuousCleaningEvent: [GetContinuousCleaning()], - CustomCommandEvent: [], - ErrorEvent: [GetError()], - FanSpeedEvent: [GetFanSpeed()], - LifeSpanEvent: [ - GetLifeSpan([LifeSpan.BRUSH, LifeSpan.FILTER, LifeSpan.SIDE_BRUSH]) - ], - MapChangedEvent: [], - MajorMapEvent: [GetMajorMap()], - MapTraceEvent: [GetMapTrace()], - MultimapStateEvent: [GetMultimapState()], - NetworkInfoEvent: [GetNetInfo()], - PositionsEvent: [GetPos()], - ReportStatsEvent: [], - RoomsEvent: [GetCachedMapInfo()], - StateEvent: [GetChargeState(), GetCleanInfo()], - StatsEvent: [GetStats()], - TotalStatsEvent: [GetTotalStats()], - TrueDetectEvent: [GetTrueDetect()], - VolumeEvent: [GetVolume()], - WaterInfoEvent: [GetWaterInfo()], - }, - ), ( "5xu9h3", { @@ -265,12 +231,14 @@ async def test_get_static_device_info( }, ), ], - ids=[FALLBACK, "5xu9h3", "itk04l", "yna5xi", "p95mgv"], + ids=["5xu9h3", "itk04l", "yna5xi", "p95mgv"], ) async def test_capabilities_event_extraction( class_: str, expected: dict[type[Event], list[Command]] ) -> None: - capabilities = (await get_static_device_info(class_)).capabilities + info = await get_static_device_info(class_) + assert info is not None + capabilities = info.capabilities assert capabilities._events.keys() == expected.keys() for event, expected_commands in expected.items(): assert ( @@ -294,7 +262,6 @@ def test_all_models_loaded() -> None: "9s1s80", "clojes", "e6ofmn", - "fallback", "guzput", "itk04l", "kr0277",