From a923da9bd9ada2557c6c0e59c1d8c70bfc90b036 Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Mon, 15 Mar 2021 20:56:07 +0800 Subject: [PATCH] [Add] Add 360 Heurist support --- libdyson/__init__.py | 11 ++- libdyson/const.py | 69 ++++++++++++++++ libdyson/dyson_360_eye.py | 136 ++------------------------------ libdyson/dyson_360_heurist.py | 64 +++++++++++++++ libdyson/dyson_vacuum_device.py | 80 +++++++++++++++++++ tests/test_360_eye.py | 41 ++-------- tests/test_360_heurist.py | 112 ++++++++++++++++++++++++++ tests/test_init.py | 3 + tests/test_vacuum.py | 62 +++++++++++++++ 9 files changed, 410 insertions(+), 168 deletions(-) create mode 100644 libdyson/dyson_360_heurist.py create mode 100644 libdyson/dyson_vacuum_device.py create mode 100644 tests/test_360_heurist.py create mode 100644 tests/test_vacuum.py diff --git a/libdyson/__init__.py b/libdyson/__init__.py index b9dde07..38d73fc 100644 --- a/libdyson/__init__.py +++ b/libdyson/__init__.py @@ -4,6 +4,7 @@ from .const import ( DEVICE_TYPE_360_EYE, + DEVICE_TYPE_360_HEURIST, DEVICE_TYPE_PURE_COOL, DEVICE_TYPE_PURE_COOL_DESK, DEVICE_TYPE_PURE_COOL_LINK, @@ -12,14 +13,18 @@ DEVICE_TYPE_PURE_HOT_COOL_LINK, DEVICE_TYPE_PURE_HUMIDIFY_COOL, ) +from .const import CleaningMode # noqa: F401 +from .const import CleaningType # noqa: F401 from .const import DEVICE_TYPE_NAMES # noqa: F401 from .const import HumidifyOscillationMode # noqa: F401 from .const import MessageType # noqa: F401 +from .const import VacuumEyePowerMode # noqa: F401 +from .const import VacuumHeuristPowerMode # noqa: F401 +from .const import VacuumState # noqa: F401 from .const import WaterHardness # noqa: F401 from .discovery import DysonDiscovery # noqa: F401 from .dyson_360_eye import Dyson360Eye -from .dyson_360_eye import VacuumPowerMode # noqa: F401 -from .dyson_360_eye import VacuumState # noqa: F401 +from .dyson_360_heurist import Dyson360Heurist from .dyson_device import DysonDevice from .dyson_pure_cool import DysonPureCool from .dyson_pure_cool_link import DysonPureCoolLink @@ -33,6 +38,8 @@ def get_device(serial: str, credential: str, device_type: str) -> Optional[Dyson """Get a new DysonDevice instance.""" if device_type == DEVICE_TYPE_360_EYE: return Dyson360Eye(serial, credential) + if device_type == DEVICE_TYPE_360_HEURIST: + return Dyson360Heurist(serial, credential) if device_type in [ DEVICE_TYPE_PURE_COOL_LINK_DESK, DEVICE_TYPE_PURE_COOL_LINK, diff --git a/libdyson/const.py b/libdyson/const.py index 83bc1b4..e1bbf63 100644 --- a/libdyson/const.py +++ b/libdyson/const.py @@ -2,6 +2,7 @@ from enum import Enum, auto DEVICE_TYPE_360_EYE = "N223" +DEVICE_TYPE_360_HEURIST = "276" DEVICE_TYPE_PURE_COOL_LINK = "475" DEVICE_TYPE_PURE_COOL_LINK_DESK = "469" DEVICE_TYPE_PURE_COOL = "438" @@ -12,6 +13,7 @@ DEVICE_TYPE_NAMES = { DEVICE_TYPE_360_EYE: "360 Eye robot vacuum", + DEVICE_TYPE_360_HEURIST: "360 Heurist robot vacuum", DEVICE_TYPE_PURE_COOL: "Pure Cool", DEVICE_TYPE_PURE_COOL_DESK: "Pure Cool Desk", DEVICE_TYPE_PURE_COOL_LINK: "Pure Cool Link", @@ -56,3 +58,70 @@ class WaterHardness(Enum): SOFT = "Soft" MEDIUM = "Medium" HARD = "Hard" + + +class VacuumState(Enum): + """Dyson vacuum state.""" + + FAULT_CALL_HELPLINE = "FAULT_CALL_HELPLINE" + FAULT_CONTACT_HELPLINE = "FAULT_CONTACT_HELPLINE" + FAULT_CRITICAL = "FAULT_CRITICAL" + FAULT_GETTING_INFO = "FAULT_GETTING_INFO" + FAULT_LOST = "FAULT_LOST" + FAULT_ON_DOCK = "FAULT_ON_DOCK" + FAULT_ON_DOCK_CHARGED = "FAULT_ON_DOCK_CHARGED" + FAULT_ON_DOCK_CHARGING = "FAULT_ON_DOCK_CHARGING" + FAULT_REPLACE_ON_DOCK = "FAULT_REPLACE_ON_DOCK" + FAULT_RETURN_TO_DOCK = "FAULT_RETURN_TO_DOCK" + FAULT_RUNNING_DIAGNOSTIC = "FAULT_RUNNING_DIAGNOSTIC" + FAULT_USER_RECOVERABLE = "FAULT_USER_RECOVERABLE" + FULL_CLEAN_ABANDONED = "FULL_CLEAN_ABANDONED" + FULL_CLEAN_ABORTED = "FULL_CLEAN_ABORTED" + FULL_CLEAN_CHARGING = "FULL_CLEAN_CHARGING" + FULL_CLEAN_DISCOVERING = "FULL_CLEAN_DISCOVERING" + FULL_CLEAN_FINISHED = "FULL_CLEAN_FINISHED" + FULL_CLEAN_INITIATED = "FULL_CLEAN_INITIATED" + FULL_CLEAN_NEEDS_CHARGE = "FULL_CLEAN_NEEDS_CHARGE" + FULL_CLEAN_PAUSED = "FULL_CLEAN_PAUSED" + FULL_CLEAN_RUNNING = "FULL_CLEAN_RUNNING" + FULL_CLEAN_TRAVERSING = "FULL_CLEAN_TRAVERSING" + INACTIVE_CHARGED = "INACTIVE_CHARGED" + INACTIVE_CHARGING = "INACTIVE_CHARGING" + INACTIVE_DISCHARGING = "INACTIVE_DISCHARGING" + MAPPING_ABORTED = "MAPPING_ABORTED" + MAPPING_CHARGING = "MAPPING_CHARGING" + MAPPING_FINISHED = "MAPPING_FINISHED" + MAPPING_INITIATED = "MAPPING_INITIATED" + MAPPING_NEEDS_CHARGE = "MAPPING_NEEDS_CHARGE" + MAPPING_PAUSED = "MAPPING_PAUSED" + MAPPING_RUNNING = "MAPPING_RUNNING" + + +class VacuumEyePowerMode(Enum): + """Dyson 360 Eye power mode.""" + + QUIET = "halfPower" + MAX = "fullPower" + + +class VacuumHeuristPowerMode(Enum): + """Dyson 360 Heurist power mode.""" + + QUITE = "1" + HIGH = "2" + MAX = "3" + + +class CleaningType(Enum): + """Vacuum cleaning type.""" + + IMMEDIATE = "immediate" + MANUAL = "manual" + SCHEDULED = "scheduled" + + +class CleaningMode(Enum): + """Vacuum cleaning mode.""" + + GLOBAL = "global" + ZONE_CONFIGURED = "zoneConfigured" diff --git a/libdyson/dyson_360_eye.py b/libdyson/dyson_360_eye.py index 6a379fe..0147c92 100644 --- a/libdyson/dyson_360_eye.py +++ b/libdyson/dyson_360_eye.py @@ -1,64 +1,10 @@ """Dyson 360 Eye vacuum robot.""" -from enum import Enum -from typing import Optional, Tuple -from .const import DEVICE_TYPE_360_EYE -from .dyson_device import DysonDevice +from .const import DEVICE_TYPE_360_EYE, VacuumEyePowerMode +from .dyson_vacuum_device import DysonVacuumDevice -class VacuumState(Enum): - """Dyson vacuum state.""" - - FAULT_CALL_HELPLINE = "FAULT_CALL_HELPLINE" - FAULT_CONTACT_HELPLINE = "FAULT_CONTACT_HELPLINE" - FAULT_CRITICAL = "FAULT_CRITICAL" - FAULT_GETTING_INFO = "FAULT_GETTING_INFO" - FAULT_LOST = "FAULT_LOST" - FAULT_ON_DOCK = "FAULT_ON_DOCK" - FAULT_ON_DOCK_CHARGED = "FAULT_ON_DOCK_CHARGED" - FAULT_ON_DOCK_CHARGING = "FAULT_ON_DOCK_CHARGING" - FAULT_REPLACE_ON_DOCK = "FAULT_REPLACE_ON_DOCK" - FAULT_RETURN_TO_DOCK = "FAULT_RETURN_TO_DOCK" - FAULT_RUNNING_DIAGNOSTIC = "FAULT_RUNNING_DIAGNOSTIC" - FAULT_USER_RECOVERABLE = "FAULT_USER_RECOVERABLE" - FULL_CLEAN_ABANDONED = "FULL_CLEAN_ABANDONED" - FULL_CLEAN_ABORTED = "FULL_CLEAN_ABORTED" - FULL_CLEAN_CHARGING = "FULL_CLEAN_CHARGING" - FULL_CLEAN_DISCOVERING = "FULL_CLEAN_DISCOVERING" - FULL_CLEAN_FINISHED = "FULL_CLEAN_FINISHED" - FULL_CLEAN_INITIATED = "FULL_CLEAN_INITIATED" - FULL_CLEAN_NEEDS_CHARGE = "FULL_CLEAN_NEEDS_CHARGE" - FULL_CLEAN_PAUSED = "FULL_CLEAN_PAUSED" - FULL_CLEAN_RUNNING = "FULL_CLEAN_RUNNING" - FULL_CLEAN_TRAVERSING = "FULL_CLEAN_TRAVERSING" - INACTIVE_CHARGED = "INACTIVE_CHARGED" - INACTIVE_CHARGING = "INACTIVE_CHARGING" - INACTIVE_DISCHARGING = "INACTIVE_DISCHARGING" - MAPPING_ABORTED = "MAPPING_ABORTED" - MAPPING_CHARGING = "MAPPING_CHARGING" - MAPPING_FINISHED = "MAPPING_FINISHED" - MAPPING_INITIATED = "MAPPING_INITIATED" - MAPPING_NEEDS_CHARGE = "MAPPING_NEEDS_CHARGE" - MAPPING_PAUSED = "MAPPING_PAUSED" - MAPPING_RUNNING = "MAPPING_RUNNING" - - -class VacuumPowerMode(Enum): - """Dyson vacuum power mode.""" - - QUIET = "halfPower" - MAX = "fullPower" - - -class CleaningType(Enum): - """Vacuum cleaning type.""" - - IMMEDIATE = "immediate" - MANUAL = "manual" - Scheduled = "scheduled" - - -class Dyson360Eye(DysonDevice): +class Dyson360Eye(DysonVacuumDevice): """Dyson 360 Eye device.""" @property @@ -67,85 +13,15 @@ def device_type(self) -> str: return DEVICE_TYPE_360_EYE @property - def _status_topic(self) -> str: - """MQTT status topic.""" - return f"{self.device_type}/{self._serial}/status" - - @property - def state(self) -> VacuumPowerMode: - """State of the device.""" - return VacuumState( - self._status["state"] - if "state" in self._status - else self._status["newstate"] - ) - - @property - def power_mode(self) -> VacuumPowerMode: + def power_mode(self) -> VacuumEyePowerMode: """Power mode of the device.""" - return VacuumPowerMode(self._status["currentVacuumPowerMode"]) - - @property - def cleaning_type(self) -> Optional[CleaningType]: - """Return the type of the current cleaning task.""" - cleaning_type = self._status["fullCleanType"] - if cleaning_type == "": - return None - return CleaningType(cleaning_type) - - @property - def cleaning_id(self) -> Optional[str]: - """Return the id of the current cleaning task.""" - cleaning_id = self._status["cleanId"] - if cleaning_id == "": - return None - return cleaning_id - - @property - def battery_level(self) -> int: - """Battery level of the device in percentage.""" - return self._status["batteryChargeLevel"] - - @property - def position(self) -> Optional[Tuple[int, int]]: - """Position (x, y) of the device.""" - if ( - "globalPosition" in self._status - and len(self._status["globalPosition"]) == 2 - ): - return tuple(self._status["globalPosition"]) - return None - - @property - def is_charging(self) -> bool: - """Whether the device is charging.""" - return self.state in [ - VacuumState.INACTIVE_CHARGING, - VacuumState.INACTIVE_CHARGED, - VacuumState.FULL_CLEAN_CHARGING, - VacuumState.MAPPING_CHARGING, - ] - - def _update_status(self, payload: dict) -> None: - self._status = payload + return VacuumEyePowerMode(self._status["currentVacuumPowerMode"]) def start(self) -> None: """Start cleaning.""" self._send_command("START", {"fullCleanType": "immediate"}) - def pause(self) -> None: - """Pause cleaning.""" - self._send_command("PAUSE") - - def resume(self) -> None: - """Resume cleaning.""" - self._send_command("RESUME") - - def abort(self) -> None: - """Abort cleaning.""" - self._send_command("ABORT") - - def set_power_mode(self, power_mode: VacuumPowerMode) -> None: + def set_power_mode(self, power_mode: VacuumEyePowerMode) -> None: """Set power mode.""" self._send_command( "STATE-SET", diff --git a/libdyson/dyson_360_heurist.py b/libdyson/dyson_360_heurist.py new file mode 100644 index 0000000..70c8911 --- /dev/null +++ b/libdyson/dyson_360_heurist.py @@ -0,0 +1,64 @@ +"""Dyson 360 Heurist vacuum robot.""" + +from typing import Optional + +from .const import DEVICE_TYPE_360_HEURIST, CleaningMode, VacuumHeuristPowerMode +from .dyson_vacuum_device import DysonVacuumDevice + + +class Dyson360Heurist(DysonVacuumDevice): + """Dyson 360 Heurist device.""" + + @property + def device_type(self) -> str: + """Return the device type.""" + return DEVICE_TYPE_360_HEURIST + + @property + def current_power_mode(self) -> VacuumHeuristPowerMode: + """Return current power mode.""" + return VacuumHeuristPowerMode(self._status["currentVacuumPowerMode"]) + + @property + def default_power_mode(self) -> VacuumHeuristPowerMode: + """Return default power mode.""" + return VacuumHeuristPowerMode(self._status["defaultVacuumPowerMode"]) + + @property + def current_cleaning_mode(self) -> CleaningMode: + """Return current cleaning mode.""" + return CleaningMode(self._status["currentCleaningMode"]) + + @property + def default_cleaning_mode(self) -> CleaningMode: + """Return default cleaning mode.""" + return CleaningMode(self._status["defaultCleaningMode"]) + + @property + def is_bin_full(self) -> bool: + """Return if the bin is full.""" + airways = self._status.get("faults", {}).get("AIRWAYS") + if airways is None: + return False + return ( + airways.get("active") is True and airways.get("description") == "1.0.-1" + ) # Not sure what this means + + def _send_command(self, command: str, data: Optional[dict] = None): + if data is None: + data = {} + data["mode-reason"] = "LAPP" + super()._send_command(command, data) + + def start_all_zones(self) -> None: + """Start cleaning of all zones.""" + self._send_command( + "START", {"cleaningMode": "global", "fullCleanType": "immediate"} + ) + + def set_default_power_mode(self, power_mode: VacuumHeuristPowerMode) -> None: + """Set default power mode.""" + self._send_command( + "STATE-SET", + {"defaults": {"defaultVacuumPowerMode": power_mode.value}}, + ) diff --git a/libdyson/dyson_vacuum_device.py b/libdyson/dyson_vacuum_device.py new file mode 100644 index 0000000..3d7f44c --- /dev/null +++ b/libdyson/dyson_vacuum_device.py @@ -0,0 +1,80 @@ +"""Dyson vacuum device.""" + +from typing import Optional, Tuple + +from .const import CleaningType, VacuumState +from .dyson_device import DysonDevice + + +class DysonVacuumDevice(DysonDevice): + """Dyson vacuum device.""" + + @property + def _status_topic(self) -> str: + """MQTT status topic.""" + return f"{self.device_type}/{self._serial}/status" + + @property + def state(self) -> VacuumState: + """State of the device.""" + return VacuumState( + self._status["state"] + if "state" in self._status + else self._status["newstate"] + ) + + @property + def cleaning_type(self) -> Optional[CleaningType]: + """Return the type of the current cleaning task.""" + cleaning_type = self._status["fullCleanType"] + if cleaning_type == "": + return None + return CleaningType(cleaning_type) + + @property + def cleaning_id(self) -> Optional[str]: + """Return the id of the current cleaning task.""" + cleaning_id = self._status["cleanId"] + if cleaning_id == "": + return None + return cleaning_id + + @property + def battery_level(self) -> int: + """Battery level of the device in percentage.""" + return self._status["batteryChargeLevel"] + + @property + def position(self) -> Optional[Tuple[int, int]]: + """Position (x, y) of the device.""" + if ( + "globalPosition" in self._status + and len(self._status["globalPosition"]) == 2 + ): + return tuple(self._status["globalPosition"]) + return None + + @property + def is_charging(self) -> bool: + """Whether the device is charging.""" + return self.state in [ + VacuumState.INACTIVE_CHARGING, + VacuumState.INACTIVE_CHARGED, + VacuumState.FULL_CLEAN_CHARGING, + VacuumState.MAPPING_CHARGING, + ] + + def _update_status(self, payload: dict) -> None: + self._status = payload + + def pause(self) -> None: + """Pause cleaning.""" + self._send_command("PAUSE") + + def resume(self) -> None: + """Resume cleaning.""" + self._send_command("RESUME") + + def abort(self) -> None: + """Abort cleaning.""" + self._send_command("ABORT") diff --git a/tests/test_360_eye.py b/tests/test_360_eye.py index 678c227..1e89538 100644 --- a/tests/test_360_eye.py +++ b/tests/test_360_eye.py @@ -3,25 +3,14 @@ import pytest -from libdyson import DEVICE_TYPE_360_EYE -from libdyson.dyson_360_eye import ( - CleaningType, - Dyson360Eye, - VacuumPowerMode, - VacuumState, -) +from libdyson import DEVICE_TYPE_360_EYE, Dyson360Eye, VacuumEyePowerMode from . import CREDENTIAL, HOST, SERIAL from .mocked_mqtt import MockedMQTT STATUS = { - "state": "INACTIVE_CHARGED", - "fullCleanType": "", - "cleanId": "", "currentVacuumPowerMode": "fullPower", "defaultVacuumPowerMode": "fullPower", - "globalPosition": [0, 0], - "batteryChargeLevel": 100, } @@ -44,34 +33,14 @@ def test_properties(mqtt_client: MockedMQTT): """Test properties of 360 Eye.""" device = Dyson360Eye(SERIAL, CREDENTIAL) device.connect(HOST) + assert device.power_mode == VacuumEyePowerMode.MAX - assert device.state == VacuumState.INACTIVE_CHARGED - assert device.cleaning_type is None - assert device.cleaning_id is None - assert device.power_mode == VacuumPowerMode.MAX - assert device.position == (0, 0) - assert device.is_charging is True - assert device.battery_level == 100 - - clean_id = "b599d00f-6f3b-401a-9c05-69877251e843" new_status = { - "oldstate": "INACTIVE_CHARGED", - "newstate": "FULL_CLEAN_RUNNING", - "fullCleanType": "immediate", - "cleanId": clean_id, "currentVacuumPowerMode": "halfPower", "defaultVacuumPowerMode": "halfPower", - "globalPosition": [], - "batteryChargeLevel": 30, } mqtt_client.state_change(new_status) - assert device.state == VacuumState.FULL_CLEAN_RUNNING - assert device.cleaning_type == CleaningType.IMMEDIATE - assert device.cleaning_id == clean_id - assert device.power_mode == VacuumPowerMode.QUIET - assert device.position is None - assert device.is_charging is False - assert device.battery_level == 30 + assert device.power_mode == VacuumEyePowerMode.QUIET @pytest.mark.parametrize( @@ -83,13 +52,13 @@ def test_properties(mqtt_client: MockedMQTT): ("abort", [], "ABORT", {}), ( "set_power_mode", - [VacuumPowerMode.MAX], + [VacuumEyePowerMode.MAX], "STATE-SET", {"data": {"defaultVacuumPowerMode": "fullPower"}}, ), ( "set_power_mode", - [VacuumPowerMode.QUIET], + [VacuumEyePowerMode.QUIET], "STATE-SET", {"data": {"defaultVacuumPowerMode": "halfPower"}}, ), diff --git a/tests/test_360_heurist.py b/tests/test_360_heurist.py new file mode 100644 index 0000000..940c5ea --- /dev/null +++ b/tests/test_360_heurist.py @@ -0,0 +1,112 @@ +"""Tests for Dyson 360 Heurist vacuum.""" +from unittest.mock import patch + +import pytest + +from libdyson import ( + DEVICE_TYPE_360_HEURIST, + CleaningMode, + Dyson360Heurist, + VacuumHeuristPowerMode, +) + +from . import CREDENTIAL, HOST, SERIAL +from .mocked_mqtt import MockedMQTT + +STATUS = { + "currentVacuumPowerMode": "1", + "defaultVacuumPowerMode": "2", + "currentCleaningMode": "zoneConfigured", + "defaultCleaningMode": "global", +} + + +@pytest.fixture(autouse=True) +def mqtt_client() -> MockedMQTT: + """Return mocked mqtt client.""" + mocked_mqtt = MockedMQTT( + HOST, + SERIAL, + CREDENTIAL, + f"{DEVICE_TYPE_360_HEURIST}/{SERIAL}/command", + f"{DEVICE_TYPE_360_HEURIST}/{SERIAL}/status", + STATUS, + ) + with patch("libdyson.dyson_device.mqtt.Client", mocked_mqtt.refersh): + yield mocked_mqtt + + +def test_properties(mqtt_client: MockedMQTT): + """Test properties of 360 Heurist.""" + device = Dyson360Heurist(SERIAL, CREDENTIAL) + device.connect(HOST) + assert device.current_power_mode == VacuumHeuristPowerMode.QUITE + assert device.default_power_mode == VacuumHeuristPowerMode.HIGH + assert device.current_cleaning_mode == CleaningMode.ZONE_CONFIGURED + assert device.default_cleaning_mode == CleaningMode.GLOBAL + assert device.is_bin_full is False + + new_status = { + "currentVacuumPowerMode": "2", + "defaultVacuumPowerMode": "3", + "currentCleaningMode": "global", + "defaultCleaningMode": "zoneConfigured", + "faults": { + "AIRWAYS": {"active": True, "description": "1.0.-1"}, + }, + } + mqtt_client.state_change(new_status) + assert device.current_power_mode == VacuumHeuristPowerMode.HIGH + assert device.default_power_mode == VacuumHeuristPowerMode.MAX + assert device.current_cleaning_mode == CleaningMode.GLOBAL + assert device.default_cleaning_mode == CleaningMode.ZONE_CONFIGURED + assert device.is_bin_full is True + + +@pytest.mark.parametrize( + "command,command_args,msg,msg_data", + [ + ( + "start_all_zones", + [], + "START", + {"cleaningMode": "global", "fullCleanType": "immediate"}, + ), + ("pause", [], "PAUSE", {}), + ("resume", [], "RESUME", {}), + ("abort", [], "ABORT", {}), + ( + "set_default_power_mode", + [VacuumHeuristPowerMode.QUITE], + "STATE-SET", + {"defaults": {"defaultVacuumPowerMode": "1"}}, + ), + ( + "set_default_power_mode", + [VacuumHeuristPowerMode.HIGH], + "STATE-SET", + {"defaults": {"defaultVacuumPowerMode": "2"}}, + ), + ( + "set_default_power_mode", + [VacuumHeuristPowerMode.MAX], + "STATE-SET", + {"defaults": {"defaultVacuumPowerMode": "3"}}, + ), + ], +) +def test_command( + mqtt_client: MockedMQTT, command: str, command_args: list, msg: str, msg_data: dict +): + """Test commands of 360 Heurist.""" + device = Dyson360Heurist(SERIAL, CREDENTIAL) + device.connect(HOST) + + func = getattr(device, command) + func(*command_args) + len(mqtt_client.commands) == 1 + payload = mqtt_client.commands[0] + assert payload.pop("msg") == msg + assert payload.pop("mode-reason") == "LAPP" + payload.pop("time") + assert payload == msg_data diff --git a/tests/test_init.py b/tests/test_init.py index 273b9b7..3eda70c 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -5,6 +5,7 @@ from libdyson import ( DEVICE_TYPE_360_EYE, + DEVICE_TYPE_360_HEURIST, DEVICE_TYPE_PURE_COOL, DEVICE_TYPE_PURE_COOL_DESK, DEVICE_TYPE_PURE_COOL_LINK, @@ -13,6 +14,7 @@ DEVICE_TYPE_PURE_HOT_COOL_LINK, DEVICE_TYPE_PURE_HUMIDIFY_COOL, Dyson360Eye, + Dyson360Heurist, DysonDevice, DysonPureCool, DysonPureCoolLink, @@ -29,6 +31,7 @@ "device_type,class_type", [ (DEVICE_TYPE_360_EYE, Dyson360Eye), + (DEVICE_TYPE_360_HEURIST, Dyson360Heurist), (DEVICE_TYPE_PURE_COOL_LINK_DESK, DysonPureCoolLink), (DEVICE_TYPE_PURE_COOL_LINK, DysonPureCoolLink), (DEVICE_TYPE_PURE_COOL, DysonPureCool), diff --git a/tests/test_vacuum.py b/tests/test_vacuum.py new file mode 100644 index 0000000..d228d82 --- /dev/null +++ b/tests/test_vacuum.py @@ -0,0 +1,62 @@ +"""Tests for Dyson vacuum base class.""" +from unittest.mock import patch + +import pytest + +from libdyson import DEVICE_TYPE_360_EYE, CleaningType, Dyson360Eye, VacuumState + +from . import CREDENTIAL, HOST, SERIAL +from .mocked_mqtt import MockedMQTT + +STATUS = { + "state": "INACTIVE_CHARGED", + "fullCleanType": "", + "cleanId": "", + "globalPosition": [0, 0], + "batteryChargeLevel": 100, +} + + +@pytest.fixture(autouse=True) +def mqtt_client() -> MockedMQTT: + """Return mocked mqtt client.""" + mocked_mqtt = MockedMQTT( + HOST, + SERIAL, + CREDENTIAL, + f"{DEVICE_TYPE_360_EYE}/{SERIAL}/command", + f"{DEVICE_TYPE_360_EYE}/{SERIAL}/status", + STATUS, + ) + with patch("libdyson.dyson_device.mqtt.Client", mocked_mqtt.refersh): + yield mocked_mqtt + + +def test_properties(mqtt_client: MockedMQTT): + """Test properties of vaccums.""" + device = Dyson360Eye(SERIAL, CREDENTIAL) + device.connect(HOST) + + assert device.state == VacuumState.INACTIVE_CHARGED + assert device.cleaning_type is None + assert device.cleaning_id is None + assert device.position == (0, 0) + assert device.is_charging is True + assert device.battery_level == 100 + + clean_id = "b599d00f-6f3b-401a-9c05-69877251e843" + new_status = { + "oldstate": "INACTIVE_CHARGED", + "newstate": "FULL_CLEAN_RUNNING", + "fullCleanType": "immediate", + "cleanId": clean_id, + "globalPosition": [], + "batteryChargeLevel": 30, + } + mqtt_client.state_change(new_status) + assert device.state == VacuumState.FULL_CLEAN_RUNNING + assert device.cleaning_type == CleaningType.IMMEDIATE + assert device.cleaning_id == clean_id + assert device.position is None + assert device.is_charging is False + assert device.battery_level == 30