diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py
index e4c8a96569578d06b7dfdcdd1809020dd762a0e3..6f07b61de4a75eab9c1b6d9754a4754236021b35 100644
--- a/homeassistant/components/ecovacs/__init__.py
+++ b/homeassistant/components/ecovacs/__init__.py
@@ -25,6 +25,7 @@ CONFIG_SCHEMA = vol.Schema(
 )
 
 PLATFORMS = [
+    Platform.BINARY_SENSOR,
     Platform.VACUUM,
 ]
 
diff --git a/homeassistant/components/ecovacs/binary_sensor.py b/homeassistant/components/ecovacs/binary_sensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea22c9de43277882ff5b875bdc425194f8363067
--- /dev/null
+++ b/homeassistant/components/ecovacs/binary_sensor.py
@@ -0,0 +1,75 @@
+"""Binary sensor module."""
+from collections.abc import Callable
+from dataclasses import dataclass
+from typing import Generic
+
+from deebot_client.capabilities import CapabilityEvent
+from deebot_client.events.water_info import WaterInfoEvent
+
+from homeassistant.components.binary_sensor import (
+    BinarySensorEntity,
+    BinarySensorEntityDescription,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EntityCategory
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from .const import DOMAIN
+from .controller import EcovacsController
+from .entity import EcovacsDescriptionEntity, EcovacsEntityDescription, EventT
+
+
+@dataclass(kw_only=True, frozen=True)
+class EcovacsBinarySensorEntityDescription(
+    BinarySensorEntityDescription,
+    EcovacsEntityDescription,
+    Generic[EventT],
+):
+    """Class describing Deebot binary sensor entity."""
+
+    value_fn: Callable[[EventT], bool | None]
+
+
+ENTITY_DESCRIPTIONS: tuple[EcovacsBinarySensorEntityDescription, ...] = (
+    EcovacsBinarySensorEntityDescription[WaterInfoEvent](
+        capability_fn=lambda caps: caps.water,
+        value_fn=lambda e: e.mop_attached,
+        key="mop_attached",
+        translation_key="mop_attached",
+        entity_category=EntityCategory.DIAGNOSTIC,
+    ),
+)
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    config_entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Add entities for passed config_entry in HA."""
+    controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
+    controller.register_platform_add_entities(
+        EcovacsBinarySensor, ENTITY_DESCRIPTIONS, async_add_entities
+    )
+
+
+class EcovacsBinarySensor(
+    EcovacsDescriptionEntity[
+        CapabilityEvent[EventT], EcovacsBinarySensorEntityDescription
+    ],
+    BinarySensorEntity,
+):
+    """Ecovacs binary sensor."""
+
+    entity_description: EcovacsBinarySensorEntityDescription
+
+    async def async_added_to_hass(self) -> None:
+        """Set up the event listeners now that hass is ready."""
+        await super().async_added_to_hass()
+
+        async def on_event(event: EventT) -> None:
+            self._attr_is_on = self.entity_description.value_fn(event)
+            self.async_write_ha_state()
+
+        self._subscribe(self._capability.event, on_event)
diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py
index 645c5b9bc191145cc32e575d471349df24fbff83..78b05a8a7d18937b2c12ff587ce196ef17ab22d1 100644
--- a/homeassistant/components/ecovacs/controller.py
+++ b/homeassistant/components/ecovacs/controller.py
@@ -23,7 +23,9 @@ from homeassistant.const import (
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
 from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
+from .entity import EcovacsDescriptionEntity, EcovacsEntityDescription
 from .util import get_client_device_id
 
 _LOGGER = logging.getLogger(__name__)
@@ -86,6 +88,23 @@ class EcovacsController:
 
         _LOGGER.debug("Controller initialize complete")
 
+    def register_platform_add_entities(
+        self,
+        entity_class: type[EcovacsDescriptionEntity],
+        descriptions: tuple[EcovacsEntityDescription, ...],
+        async_add_entities: AddEntitiesCallback,
+    ) -> None:
+        """Create entities from descriptions and add them."""
+        new_entites: list[EcovacsDescriptionEntity] = []
+
+        for device in self.devices:
+            for description in descriptions:
+                if capability := description.capability_fn(device.capabilities):
+                    new_entites.append(entity_class(device, capability, description))
+
+        if new_entites:
+            async_add_entities(new_entites)
+
     async def teardown(self) -> None:
         """Disconnect controller."""
         for device in self.devices:
diff --git a/homeassistant/components/ecovacs/entity.py b/homeassistant/components/ecovacs/entity.py
index caaefef0956f5116a4a0af0f3a039ea94a66d63f..3a2bb03aabb2084a92ab47febeb843de8c2866a8 100644
--- a/homeassistant/components/ecovacs/entity.py
+++ b/homeassistant/components/ecovacs/entity.py
@@ -104,3 +104,18 @@ class EcovacsEntity(Entity, Generic[CapabilityT, _EntityDescriptionT]):
         """
         for event_type in self._subscribed_events:
             self._device.events.request_refresh(event_type)
+
+
+class EcovacsDescriptionEntity(EcovacsEntity[CapabilityT, _EntityDescriptionT]):
+    """Ecovacs entity."""
+
+    def __init__(
+        self,
+        device: Device,
+        capability: CapabilityT,
+        entity_description: _EntityDescriptionT,
+        **kwargs: Any,
+    ) -> None:
+        """Initialize entity."""
+        self.entity_description = entity_description
+        super().__init__(device, capability, **kwargs)
diff --git a/homeassistant/components/ecovacs/icons.json b/homeassistant/components/ecovacs/icons.json
new file mode 100644
index 0000000000000000000000000000000000000000..74c27776f64d208be648fcebb158a66789be189b
--- /dev/null
+++ b/homeassistant/components/ecovacs/icons.json
@@ -0,0 +1,12 @@
+{
+  "entity": {
+    "binary_sensor": {
+      "mop_attached": {
+        "default": "mdi:water-off",
+        "state": {
+          "on": "mdi:water"
+        }
+      }
+    }
+  }
+}
diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json
index 2ae12c244a1a958b920cc96d42bed8da20826f4f..6e4c97be360e60dccd31a271fd6e7f90c333cd56 100644
--- a/homeassistant/components/ecovacs/strings.json
+++ b/homeassistant/components/ecovacs/strings.json
@@ -19,6 +19,11 @@
     }
   },
   "entity": {
+    "binary_sensor": {
+      "mop_attached": {
+        "name": "Mop attached"
+      }
+    },
     "vacuum": {
       "vacuum": {
         "state_attributes": {
diff --git a/tests/components/ecovacs/conftest.py b/tests/components/ecovacs/conftest.py
index 9ba28857cbea005f8c0e259f09eece35049df334..38ae8ea54ae8b5fc7a206d5dc6d5ea85183b4388 100644
--- a/tests/components/ecovacs/conftest.py
+++ b/tests/components/ecovacs/conftest.py
@@ -1,17 +1,21 @@
 """Common fixtures for the Ecovacs tests."""
 from collections.abc import Generator
+from typing import Any
 from unittest.mock import AsyncMock, Mock, patch
 
-from deebot_client.api_client import ApiClient
-from deebot_client.authentication import Authenticator
+from deebot_client.const import PATH_API_APPSVR_APP
+from deebot_client.device import Device
+from deebot_client.exceptions import ApiError
 from deebot_client.models import Credentials
 import pytest
 
 from homeassistant.components.ecovacs.const import DOMAIN
+from homeassistant.components.ecovacs.controller import EcovacsController
+from homeassistant.core import HomeAssistant
 
 from .const import VALID_ENTRY_DATA
 
-from tests.common import MockConfigEntry
+from tests.common import MockConfigEntry, load_json_object_fixture
 
 
 @pytest.fixture
@@ -34,18 +38,43 @@ def mock_config_entry() -> MockConfigEntry:
 
 
 @pytest.fixture
-def mock_authenticator() -> Generator[Mock, None, None]:
+def device_classes() -> list[str]:
+    """Device classes, which should be returned by the get_devices api call."""
+    return ["yna5x1"]
+
+
+@pytest.fixture
+def mock_authenticator(device_classes: list[str]) -> Generator[Mock, None, None]:
     """Mock the authenticator."""
-    mock_authenticator = Mock(spec_set=Authenticator)
-    mock_authenticator.authenticate.return_value = Credentials("token", "user_id", 0)
     with patch(
         "homeassistant.components.ecovacs.controller.Authenticator",
-        return_value=mock_authenticator,
-    ), patch(
+        autospec=True,
+    ) as mock, patch(
         "homeassistant.components.ecovacs.config_flow.Authenticator",
-        return_value=mock_authenticator,
+        new=mock,
     ):
-        yield mock_authenticator
+        authenticator = mock.return_value
+        authenticator.authenticate.return_value = Credentials("token", "user_id", 0)
+
+        devices = []
+        for device_class in device_classes:
+            devices.append(
+                load_json_object_fixture(f"devices/{device_class}/device.json", DOMAIN)
+            )
+
+        def post_authenticated(
+            path: str,
+            json: dict[str, Any],
+            *,
+            query_params: dict[str, Any] | None = None,
+            headers: dict[str, Any] | None = None,
+        ) -> dict[str, Any]:
+            if path == PATH_API_APPSVR_APP:
+                return {"code": 0, "devices": devices, "errno": "0"}
+            raise ApiError("Path not mocked: {path}")
+
+        authenticator.post_authenticated.side_effect = post_authenticated
+        yield authenticator
 
 
 @pytest.fixture
@@ -55,10 +84,46 @@ def mock_authenticator_authenticate(mock_authenticator: Mock) -> AsyncMock:
 
 
 @pytest.fixture
-def mock_api_client(mock_authenticator: Mock) -> Mock:
-    """Mock the API client."""
+def mock_mqtt_client(mock_authenticator: Mock) -> Mock:
+    """Mock the MQTT client."""
     with patch(
-        "homeassistant.components.ecovacs.controller.ApiClient",
-        return_value=Mock(spec_set=ApiClient),
-    ) as mock_api_client:
-        yield mock_api_client.return_value
+        "homeassistant.components.ecovacs.controller.MqttClient",
+        autospec=True,
+    ) as mock_mqtt_client:
+        client = mock_mqtt_client.return_value
+        client._authenticator = mock_authenticator
+        client.subscribe.return_value = lambda: None
+        yield client
+
+
+@pytest.fixture
+def mock_device_execute() -> AsyncMock:
+    """Mock the device execute function."""
+    with patch.object(
+        Device, "_execute_command", return_value=True
+    ) as mock_device_execute:
+        yield mock_device_execute
+
+
+@pytest.fixture
+async def init_integration(
+    hass: HomeAssistant,
+    mock_config_entry: MockConfigEntry,
+    mock_authenticator: Mock,
+    mock_mqtt_client: Mock,
+    mock_device_execute: AsyncMock,
+) -> MockConfigEntry:
+    """Set up the Ecovacs integration for testing."""
+    mock_config_entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(mock_config_entry.entry_id)
+    await hass.async_block_till_done()
+    return mock_config_entry
+
+
+@pytest.fixture
+def controller(
+    hass: HomeAssistant, init_integration: MockConfigEntry
+) -> EcovacsController:
+    """Get the controller for the config entry."""
+    return hass.data[DOMAIN][init_integration.entry_id]
diff --git a/tests/components/ecovacs/fixtures/devices/yna5x1/device.json b/tests/components/ecovacs/fixtures/devices/yna5x1/device.json
new file mode 100644
index 0000000000000000000000000000000000000000..0b2957af93bc428cc84d79272b7c5b673effa5f2
--- /dev/null
+++ b/tests/components/ecovacs/fixtures/devices/yna5x1/device.json
@@ -0,0 +1,22 @@
+{
+  "did": "E1234567890000000001",
+  "name": "E1234567890000000001",
+  "class": "yna5xi",
+  "resource": "upQ6",
+  "company": "eco-ng",
+  "service": {
+    "jmq": "jmq-ngiot-eu.dc.ww.ecouser.net",
+    "mqs": "api-ngiot.dc-as.ww.ecouser.net"
+  },
+  "deviceName": "DEEBOT OZMO 950 Series",
+  "icon": "https://portal-ww.ecouser.net/api/pim/file/get/606278df4a84d700082b39f1",
+  "UILogicId": "DX_9G",
+  "materialNo": "110-1820-0101",
+  "pid": "5c19a91ca1e6ee000178224a",
+  "product_category": "DEEBOT",
+  "model": "DX9G",
+  "nick": "Ozmo 950",
+  "homeSort": 9999,
+  "status": 1,
+  "otaUpgrade": {}
+}
diff --git a/tests/components/ecovacs/snapshots/test_binary_sensor.ambr b/tests/components/ecovacs/snapshots/test_binary_sensor.ambr
new file mode 100644
index 0000000000000000000000000000000000000000..0ddf8c00a1f9b34dca805b1b6afea4981a389e89
--- /dev/null
+++ b/tests/components/ecovacs/snapshots/test_binary_sensor.ambr
@@ -0,0 +1,63 @@
+# serializer version: 1
+# name: test_mop_attached[binary_sensor.ozmo_950_mop_attached-entity_entry]
+  EntityRegistryEntrySnapshot({
+    'aliases': set({
+    }),
+    'area_id': None,
+    'capabilities': None,
+    'config_entry_id': <ANY>,
+    'device_class': None,
+    'device_id': <ANY>,
+    'disabled_by': None,
+    'domain': 'binary_sensor',
+    'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
+    'entity_id': 'binary_sensor.ozmo_950_mop_attached',
+    'has_entity_name': True,
+    'hidden_by': None,
+    'icon': None,
+    'id': <ANY>,
+    'name': None,
+    'options': dict({
+    }),
+    'original_device_class': None,
+    'original_icon': None,
+    'original_name': 'Mop attached',
+    'platform': 'ecovacs',
+    'previous_unique_id': None,
+    'supported_features': 0,
+    'translation_key': 'mop_attached',
+    'unique_id': 'E1234567890000000001_mop_attached',
+    'unit_of_measurement': None,
+  })
+# ---
+# name: test_mop_attached[binary_sensor.ozmo_950_mop_attached-state]
+  EntityRegistryEntrySnapshot({
+    'aliases': set({
+    }),
+    'area_id': None,
+    'capabilities': None,
+    'config_entry_id': <ANY>,
+    'device_class': None,
+    'device_id': <ANY>,
+    'disabled_by': None,
+    'domain': 'binary_sensor',
+    'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
+    'entity_id': 'binary_sensor.ozmo_950_mop_attached',
+    'has_entity_name': True,
+    'hidden_by': None,
+    'icon': None,
+    'id': <ANY>,
+    'name': None,
+    'options': dict({
+    }),
+    'original_device_class': None,
+    'original_icon': None,
+    'original_name': 'Mop attached',
+    'platform': 'ecovacs',
+    'previous_unique_id': None,
+    'supported_features': 0,
+    'translation_key': 'mop_attached',
+    'unique_id': 'E1234567890000000001_mop_attached',
+    'unit_of_measurement': None,
+  })
+# ---
diff --git a/tests/components/ecovacs/snapshots/test_init.ambr b/tests/components/ecovacs/snapshots/test_init.ambr
new file mode 100644
index 0000000000000000000000000000000000000000..c5d3409020482e67aa7bed41e6d5005348432a72
--- /dev/null
+++ b/tests/components/ecovacs/snapshots/test_init.ambr
@@ -0,0 +1,29 @@
+# serializer version: 1
+# name: test_devices_in_dr[E1234567890000000001]
+  DeviceRegistryEntrySnapshot({
+    'area_id': None,
+    'config_entries': <ANY>,
+    'configuration_url': None,
+    'connections': set({
+    }),
+    'disabled_by': None,
+    'entry_type': None,
+    'hw_version': None,
+    'id': <ANY>,
+    'identifiers': set({
+      tuple(
+        'ecovacs',
+        'E1234567890000000001',
+      ),
+    }),
+    'is_new': False,
+    'manufacturer': 'Ecovacs',
+    'model': 'DEEBOT OZMO 950 Series',
+    'name': 'Ozmo 950',
+    'name_by_user': None,
+    'serial_number': 'E1234567890000000001',
+    'suggested_area': None,
+    'sw_version': None,
+    'via_device_id': None,
+  })
+# ---
diff --git a/tests/components/ecovacs/test_binary_sensor.py b/tests/components/ecovacs/test_binary_sensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..a912df60c62f2977c7cc9ee620ee257d961efc58
--- /dev/null
+++ b/tests/components/ecovacs/test_binary_sensor.py
@@ -0,0 +1,46 @@
+"""Tests for Ecovacs binary sensors."""
+
+from deebot_client.event_bus import EventBus
+from deebot_client.events import WaterAmount, WaterInfoEvent
+import pytest
+from syrupy import SnapshotAssertion
+
+from homeassistant.components.ecovacs.controller import EcovacsController
+from homeassistant.const import STATE_OFF, STATE_UNKNOWN
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .util import notify_and_wait
+
+pytestmark = [pytest.mark.usefixtures("init_integration")]
+
+
+async def test_mop_attached(
+    hass: HomeAssistant,
+    entity_registry: er.EntityRegistry,
+    controller: EcovacsController,
+    snapshot: SnapshotAssertion,
+) -> None:
+    """Test mop_attached binary sensor."""
+    entity_id = "binary_sensor.ozmo_950_mop_attached"
+    assert (state := hass.states.get(entity_id))
+    assert state.state == STATE_UNKNOWN
+
+    assert (entity_entry := entity_registry.async_get(state.entity_id))
+    assert entity_entry == snapshot(name=f"{entity_id}-entity_entry")
+    assert entity_entry.device_id
+
+    event_bus: EventBus = controller.devices[0].events
+    await notify_and_wait(
+        hass, event_bus, WaterInfoEvent(WaterAmount.HIGH, mop_attached=True)
+    )
+
+    assert (state := hass.states.get(state.entity_id))
+    assert entity_entry == snapshot(name=f"{entity_id}-state")
+
+    await notify_and_wait(
+        hass, event_bus, WaterInfoEvent(WaterAmount.HIGH, mop_attached=False)
+    )
+
+    assert (state := hass.states.get(state.entity_id))
+    assert state.state == STATE_OFF
diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py
index e6be4e22233bcff448288db0b6236c17de91e8be..103ab254650435773cedbab26835a44911cdc5fe 100644
--- a/tests/components/ecovacs/test_init.py
+++ b/tests/components/ecovacs/test_init.py
@@ -1,13 +1,16 @@
 """Test init of ecovacs."""
 from typing import Any
-from unittest.mock import AsyncMock, Mock
+from unittest.mock import AsyncMock, Mock, patch
 
 from deebot_client.exceptions import DeebotError, InvalidAuthenticationError
 import pytest
+from syrupy import SnapshotAssertion
 
 from homeassistant.components.ecovacs.const import DOMAIN
+from homeassistant.components.ecovacs.controller import EcovacsController
 from homeassistant.config_entries import ConfigEntryState
 from homeassistant.core import HomeAssistant
+from homeassistant.helpers import device_registry as dr
 from homeassistant.setup import async_setup_component
 
 from .const import IMPORT_DATA
@@ -15,22 +18,31 @@ from .const import IMPORT_DATA
 from tests.common import MockConfigEntry
 
 
-@pytest.mark.usefixtures("mock_api_client")
+@pytest.mark.usefixtures("init_integration")
 async def test_load_unload_config_entry(
     hass: HomeAssistant,
-    mock_config_entry: MockConfigEntry,
+    init_integration: MockConfigEntry,
 ) -> None:
     """Test loading and unloading the integration."""
-    mock_config_entry.add_to_hass(hass)
-    await hass.config_entries.async_setup(mock_config_entry.entry_id)
-    await hass.async_block_till_done()
-
+    mock_config_entry = init_integration
     assert mock_config_entry.state is ConfigEntryState.LOADED
+    assert DOMAIN in hass.data
 
     await hass.config_entries.async_unload(mock_config_entry.entry_id)
     await hass.async_block_till_done()
 
     assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
+    assert DOMAIN not in hass.data
+
+
+@pytest.fixture
+def mock_api_client(mock_authenticator: Mock) -> Mock:
+    """Mock the API client."""
+    with patch(
+        "homeassistant.components.ecovacs.controller.ApiClient",
+        autospec=True,
+    ) as mock_api_client:
+        yield mock_api_client.return_value
 
 
 async def test_config_entry_not_ready(
@@ -83,3 +95,18 @@ async def test_async_setup_import(
     assert len(hass.config_entries.async_entries(DOMAIN)) == config_entries_expected
     assert mock_setup_entry.call_count == config_entries_expected
     assert mock_authenticator_authenticate.call_count == config_entries_expected
+
+
+async def test_devices_in_dr(
+    device_registry: dr.DeviceRegistry,
+    controller: EcovacsController,
+    snapshot: SnapshotAssertion,
+) -> None:
+    """Test all devices are in the device registry."""
+    for device in controller.devices:
+        assert (
+            device_entry := device_registry.async_get_device(
+                identifiers={(DOMAIN, device.device_info.did)}
+            )
+        )
+        assert device_entry == snapshot(name=device.device_info.did)
diff --git a/tests/components/ecovacs/util.py b/tests/components/ecovacs/util.py
new file mode 100644
index 0000000000000000000000000000000000000000..ba697226ae286fc1e7d23acef3cf2da6b8a5351b
--- /dev/null
+++ b/tests/components/ecovacs/util.py
@@ -0,0 +1,18 @@
+"""Ecovacs test util."""
+
+
+import asyncio
+
+from deebot_client.event_bus import EventBus
+from deebot_client.events import Event
+
+from homeassistant.core import HomeAssistant
+
+
+async def notify_and_wait(
+    hass: HomeAssistant, event_bus: EventBus, event: Event
+) -> None:
+    """Block till done."""
+    event_bus.notify(event)
+    await asyncio.gather(*event_bus._tasks)
+    await hass.async_block_till_done()