diff --git a/custom_components/robovac/vacuums/T2257.py b/custom_components/robovac/vacuums/T2257.py new file mode 100644 index 00000000..af43bd04 --- /dev/null +++ b/custom_components/robovac/vacuums/T2257.py @@ -0,0 +1,89 @@ +"""RoboVac G20 (T2257). +Added 2026-09-10: the plain (non-Hybrid) G20 (Tuya model code T2257) was missing +from damacus/robovac — only the G20 Hybrid (T2258) was supported, so the device +reported "model is not supported". Command codes confirmed live against a T2257 +("Dobby") via a direct Tuya status query (tinytuya): start/pause 2, direction 3, +mode 5, status 15, fan 102, locate 103, battery 104, error 106, cleaning area 109, +cleaning time 110. Observed payload while docked/idle: +{'2': False, '3': 'forward', '5': 'Nosweep', '15': 'completed', '102': 'Max', +'103': False, '104': 100, '106': 0, '109': 2395, '110': 32} +Fan speed and mode value sets follow the wider G-series convention (see T2256/ +T2258) but have only been confirmed for 'Max' and 'Nosweep' respectively; other +values are unverified and should be tested against the live device before relying +on them. +""" +from homeassistant.components.vacuum import VacuumEntityFeature +from .base import RoboVacEntityFeature, RobovacCommand, RobovacModelDetails + + +class T2257(RobovacModelDetails): + homeassistant_features = ( + VacuumEntityFeature.CLEAN_SPOT + | VacuumEntityFeature.FAN_SPEED + | VacuumEntityFeature.LOCATE + | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.RETURN_HOME + | VacuumEntityFeature.SEND_COMMAND + | VacuumEntityFeature.START + | VacuumEntityFeature.STATE + | VacuumEntityFeature.STOP + ) + robovac_features = ( + RoboVacEntityFeature.CLEANING_TIME + | RoboVacEntityFeature.CLEANING_AREA + | RoboVacEntityFeature.AUTO_RETURN + ) + commands = { + RobovacCommand.START_PAUSE: { + "code": 2, + "values": {"start": True, "pause": False}, + }, + RobovacCommand.DIRECTION: { + "code": 3, + "values": { + "forward": "forward", + "back": "back", + "left": "left", + "right": "right", + }, + }, + RobovacCommand.MODE: { + "code": 5, + "values": { + "auto": "Auto", + "small_room": "SmallRoom", + "spot": "Spot", + "edge": "Edge", + "nosweep": "Nosweep", + }, + }, + RobovacCommand.STATUS: { + "code": 15, + }, + RobovacCommand.RETURN_HOME: { + "code": 101, + }, + RobovacCommand.FAN_SPEED: { + "code": 102, + "values": { + "standard": "Standard", + "turbo": "Turbo", + "max": "Max", + }, + }, + RobovacCommand.LOCATE: { + "code": 103, + }, + RobovacCommand.BATTERY: { + "code": 104, + }, + RobovacCommand.ERROR: { + "code": 106, + }, + RobovacCommand.CLEANING_AREA: { + "code": 109, + }, + RobovacCommand.CLEANING_TIME: { + "code": 110, + }, + } diff --git a/custom_components/robovac/vacuums/__init__.py b/custom_components/robovac/vacuums/__init__.py index 690c895c..83a6093e 100644 --- a/custom_components/robovac/vacuums/__init__.py +++ b/custom_components/robovac/vacuums/__init__.py @@ -27,6 +27,7 @@ from .T2254 import T2254 from .T2255 import T2255 from .T2256 import T2256 +from .T2257 import T2257 from .T2258 import T2258 from .T2259 import T2259 from .T2261 import T2261 @@ -69,6 +70,7 @@ "T2212": T2212, "T2255": T2255, "T2256": T2256, + "T2257": T2257, "T2258": T2258, "T2259": T2259, "T2270": T2270, diff --git a/tests/test_vacuum/test_t2257_command_mappings.py b/tests/test_vacuum/test_t2257_command_mappings.py new file mode 100644 index 00000000..79491b9b --- /dev/null +++ b/tests/test_vacuum/test_t2257_command_mappings.py @@ -0,0 +1,109 @@ +"""Tests for T2257 command mappings and DPS codes.""" + +import pytest +from unittest.mock import patch + +from custom_components.robovac.robovac import RoboVac +from custom_components.robovac.vacuums.base import RobovacCommand + + +@pytest.fixture +def mock_t2257_robovac() -> RoboVac: + """Create a mock T2257 RoboVac instance for testing.""" + with patch("custom_components.robovac.robovac.TuyaDevice.__init__", return_value=None): + return RoboVac( + model_code="T2257", + device_id="test_id", + host="192.168.1.100", + local_key="test_key", + ) + + +def test_t2257_reads_status_from_dps_15(mock_t2257_robovac) -> None: + """Test T2257 takes status directly from DPS 15, unlike T2258. + + T2258 reads status from DPS 2 because DPS 15 is stuck on that model. On a + live T2257 DPS 15 correctly reports real status strings (observed: + 'completed' while docked), so no DPS 2 workaround is needed here. + """ + status = mock_t2257_robovac.model_details.commands[RobovacCommand.STATUS] + + assert status["code"] == 15 + + +def test_t2257_uses_confirmed_cleaning_modes(mock_t2257_robovac) -> None: + """Test T2257 exposes the wider G-series cleaning modes. + + Only 'nosweep' has been confirmed live against the device; the others + follow the documented G-series convention but are unverified. + """ + values = mock_t2257_robovac.model_details.commands[RobovacCommand.MODE]["values"] + + assert values == { + "auto": "Auto", + "small_room": "SmallRoom", + "spot": "Spot", + "edge": "Edge", + "nosweep": "Nosweep", + } + assert mock_t2257_robovac.getRoboVacCommandValue(RobovacCommand.MODE, "nosweep") == "Nosweep" + + +def test_t2257_uses_confirmed_suction_levels(mock_t2257_robovac) -> None: + """Test T2257 exposes suction levels without quiet or boost_iq. + + Only 'max' has been confirmed live against the device; quiet and boost_iq + were omitted since they were not observed and this model tier may not + support them (unlike the Hybrid variant T2258). + """ + values = mock_t2257_robovac.model_details.commands[RobovacCommand.FAN_SPEED]["values"] + + assert values == { + "standard": "Standard", + "turbo": "Turbo", + "max": "Max", + } + assert mock_t2257_robovac.getRoboVacCommandValue(RobovacCommand.FAN_SPEED, "max") == "Max" + + +def test_t2257_exposes_direction_command(mock_t2257_robovac) -> None: + """Test T2257 exposes manual direction controls, unlike T2258.""" + assert RobovacCommand.DIRECTION in mock_t2257_robovac.model_details.commands + + +def test_t2257_model_has_basic_commands(mock_t2257_robovac) -> None: + """Test that T2257 model has required basic commands defined.""" + commands = mock_t2257_robovac.model_details.commands + + assert RobovacCommand.START_PAUSE in commands + assert RobovacCommand.MODE in commands + assert RobovacCommand.STATUS in commands + assert RobovacCommand.RETURN_HOME in commands + assert RobovacCommand.FAN_SPEED in commands + assert RobovacCommand.LOCATE in commands + assert RobovacCommand.BATTERY in commands + assert RobovacCommand.ERROR in commands + assert RobovacCommand.CLEANING_AREA in commands + assert RobovacCommand.CLEANING_TIME in commands + + +def test_t2257_battery_code(mock_t2257_robovac) -> None: + """Test T2257 reads battery from the confirmed DPS code 104.""" + battery = mock_t2257_robovac.model_details.commands[RobovacCommand.BATTERY] + + assert battery["code"] == 104 + + +def test_t2257_error_code(mock_t2257_robovac) -> None: + """Test T2257 reads error state from the confirmed DPS code 106.""" + error = mock_t2257_robovac.model_details.commands[RobovacCommand.ERROR] + + assert error["code"] == 106 + + +def test_t2257_cleaning_stats_codes(mock_t2257_robovac) -> None: + """Test T2257 reads cleaning area and time from confirmed DPS codes.""" + commands = mock_t2257_robovac.model_details.commands + + assert commands[RobovacCommand.CLEANING_AREA]["code"] == 109 + assert commands[RobovacCommand.CLEANING_TIME]["code"] == 110