Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions custom_components/robovac/vacuums/T2257.py
Original file line number Diff line number Diff line change
@@ -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,
},
}
2 changes: 2 additions & 0 deletions custom_components/robovac/vacuums/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,6 +70,7 @@
"T2212": T2212,
"T2255": T2255,
"T2256": T2256,
"T2257": T2257,
"T2258": T2258,
"T2259": T2259,
"T2270": T2270,
Expand Down
109 changes: 109 additions & 0 deletions tests/test_vacuum/test_t2257_command_mappings.py
Original file line number Diff line number Diff line change
@@ -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