Add Vacuum support to smartthings (#148724)

Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
jennoian 2025-07-28 14:37:37 +01:00 committed by GitHub
parent a71eecaaa4
commit 2a5448835f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 329 additions and 1 deletions

View File

@ -103,6 +103,7 @@ PLATFORMS = [
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
Platform.VACUUM,
Platform.VALVE,
Platform.WATER_HEATER,
]

View File

@ -0,0 +1,95 @@
"""SmartThings vacuum platform."""
from __future__ import annotations
import logging
from typing import Any
from pysmartthings import Attribute, Command, SmartThings
from pysmartthings.capability import Capability
from homeassistant.components.vacuum import (
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FullDevice, SmartThingsConfigEntry
from .const import MAIN
from .entity import SmartThingsEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: SmartThingsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up vacuum entities from SmartThings devices."""
entry_data = entry.runtime_data
async_add_entities(
SamsungJetBotVacuum(entry_data.client, device)
for device in entry_data.devices.values()
if Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE in device.status[MAIN]
)
class SamsungJetBotVacuum(SmartThingsEntity, StateVacuumEntity):
"""Representation of a Vacuum."""
_attr_name = None
_attr_supported_features = (
VacuumEntityFeature.START
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.STATE
)
def __init__(self, client: SmartThings, device: FullDevice) -> None:
"""Initialize the Samsung robot cleaner vacuum entity."""
super().__init__(
client,
device,
{Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE},
)
@property
def activity(self) -> VacuumActivity | None:
"""Return the current vacuum activity based on operating state."""
status = self.get_attribute_value(
Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE,
Attribute.OPERATING_STATE,
)
return {
"cleaning": VacuumActivity.CLEANING,
"homing": VacuumActivity.RETURNING,
"idle": VacuumActivity.IDLE,
"paused": VacuumActivity.PAUSED,
"docked": VacuumActivity.DOCKED,
"error": VacuumActivity.ERROR,
"charging": VacuumActivity.DOCKED,
}.get(status)
async def async_start(self) -> None:
"""Start the vacuum's operation."""
await self.execute_device_command(
Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE,
Command.START,
)
async def async_pause(self) -> None:
"""Pause the vacuum's current operation."""
await self.execute_device_command(
Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE, Command.PAUSE
)
async def async_return_to_base(self, **kwargs: Any) -> None:
"""Return the vacuum to its base."""
await self.execute_device_command(
Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE,
Command.RETURN_TO_HOME,
)

View File

@ -878,7 +878,7 @@
"timestamp": "2025-06-20T14:12:58.012Z"
},
"operatingState": {
"value": "dryingMop",
"value": "charging",
"timestamp": "2025-07-10T09:52:40.510Z"
},
"cleaningStep": {

View File

@ -0,0 +1,99 @@
# serializer version: 1
# name: test_all_entities[da_rvc_map_01011][vacuum.robot_vacuum-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'vacuum',
'entity_category': None,
'entity_id': 'vacuum.robot_vacuum',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <VacuumEntityFeature: 12308>,
'translation_key': None,
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][vacuum.robot_vacuum-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot vacuum',
'supported_features': <VacuumEntityFeature: 12308>,
}),
'context': <ANY>,
'entity_id': 'vacuum.robot_vacuum',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'docked',
})
# ---
# name: test_all_entities[da_rvc_normal_000001][vacuum.robot_vacuum-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'vacuum',
'entity_category': None,
'entity_id': 'vacuum.robot_vacuum',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <VacuumEntityFeature: 12308>,
'translation_key': None,
'unique_id': '3442dfc6-17c0-a65f-dae0-4c6e01786f44_main',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_normal_000001][vacuum.robot_vacuum-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot vacuum',
'supported_features': <VacuumEntityFeature: 12308>,
}),
'context': <ANY>,
'entity_id': 'vacuum.robot_vacuum',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'idle',
})
# ---

View File

@ -0,0 +1,133 @@
"""Test for the SmartThings vacuum platform."""
from unittest.mock import AsyncMock
from pysmartthings import Attribute, Capability, Command
from pysmartthings.models import HealthStatus
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.smartthings import MAIN
from homeassistant.components.vacuum import (
DOMAIN as VACUUM_DOMAIN,
SERVICE_PAUSE,
SERVICE_RETURN_TO_BASE,
SERVICE_START,
VacuumActivity,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import (
setup_integration,
snapshot_smartthings_entities,
trigger_health_update,
trigger_update,
)
from tests.common import MockConfigEntry
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
await setup_integration(hass, mock_config_entry)
snapshot_smartthings_entities(hass, entity_registry, snapshot, Platform.VACUUM)
@pytest.mark.parametrize("device_fixture", ["da_rvc_map_01011"])
@pytest.mark.parametrize(
("action", "command"),
[
(SERVICE_START, Command.START),
(SERVICE_PAUSE, Command.PAUSE),
(SERVICE_RETURN_TO_BASE, Command.RETURN_TO_HOME),
],
)
async def test_vacuum_actions(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
action: str,
command: Command,
) -> None:
"""Test vacuum actions."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
VACUUM_DOMAIN,
action,
{ATTR_ENTITY_ID: "vacuum.robot_vacuum"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"05accb39-2017-c98b-a5ab-04a81f4d3d9a",
Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE,
command,
MAIN,
)
@pytest.mark.parametrize("device_fixture", ["da_rvc_map_01011"])
async def test_state_update(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test state update."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("vacuum.robot_vacuum").state == VacuumActivity.DOCKED
await trigger_update(
hass,
devices,
"05accb39-2017-c98b-a5ab-04a81f4d3d9a",
Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE,
Attribute.OPERATING_STATE,
"error",
)
assert hass.states.get("vacuum.robot_vacuum").state == VacuumActivity.ERROR
@pytest.mark.parametrize("device_fixture", ["da_rvc_map_01011"])
async def test_availability(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test availability."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("vacuum.robot_vacuum").state == VacuumActivity.DOCKED
await trigger_health_update(
hass, devices, "05accb39-2017-c98b-a5ab-04a81f4d3d9a", HealthStatus.OFFLINE
)
assert hass.states.get("vacuum.robot_vacuum").state == STATE_UNAVAILABLE
await trigger_health_update(
hass, devices, "05accb39-2017-c98b-a5ab-04a81f4d3d9a", HealthStatus.ONLINE
)
assert hass.states.get("vacuum.robot_vacuum").state == VacuumActivity.DOCKED
@pytest.mark.parametrize("device_fixture", ["da_rvc_map_01011"])
async def test_availability_at_start(
hass: HomeAssistant,
unavailable_device: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test unavailable at boot."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("vacuum.robot_vacuum").state == STATE_UNAVAILABLE