diff --git a/homeassistant/components/russound_rio/config_flow.py b/homeassistant/components/russound_rio/config_flow.py
index 80da8c903d34db79554ae428a2949294a6db2e43..5618a424726b18d618018072786749e712a6bd36 100644
--- a/homeassistant/components/russound_rio/config_flow.py
+++ b/homeassistant/components/russound_rio/config_flow.py
@@ -8,12 +8,13 @@ from typing import Any
 from aiorussound import RussoundClient, RussoundTcpConnectionHandler
 import voluptuous as vol
 
+from homeassistant.components import zeroconf
 from homeassistant.config_entries import (
     SOURCE_RECONFIGURE,
     ConfigFlow,
     ConfigFlowResult,
 )
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
 from homeassistant.helpers import config_validation as cv
 
 from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS
@@ -33,6 +34,53 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
 
     VERSION = 1
 
+    def __init__(self) -> None:
+        """Initialize the config flow."""
+        self.data: dict[str, Any] = {}
+
+    async def async_step_zeroconf(
+        self, discovery_info: zeroconf.ZeroconfServiceInfo
+    ) -> ConfigFlowResult:
+        """Handle zeroconf discovery."""
+        self.data[CONF_HOST] = host = discovery_info.host
+        self.data[CONF_PORT] = port = discovery_info.port or 9621
+
+        client = RussoundClient(RussoundTcpConnectionHandler(host, port))
+        try:
+            await client.connect()
+            controller = client.controllers[1]
+            await client.disconnect()
+        except RUSSOUND_RIO_EXCEPTIONS:
+            return self.async_abort(reason="cannot_connect")
+
+        await self.async_set_unique_id(controller.mac_address)
+        self._abort_if_unique_id_configured(updates={CONF_HOST: host})
+
+        self.data[CONF_NAME] = controller.controller_type
+
+        self.context["title_placeholders"] = {
+            "name": self.data[CONF_NAME],
+        }
+        return await self.async_step_discovery_confirm()
+
+    async def async_step_discovery_confirm(
+        self, user_input: dict[str, Any] | None = None
+    ) -> ConfigFlowResult:
+        """Confirm discovery."""
+        if user_input is not None:
+            return self.async_create_entry(
+                title=self.data[CONF_NAME],
+                data={CONF_HOST: self.data[CONF_HOST], CONF_PORT: self.data[CONF_PORT]},
+            )
+
+        self._set_confirm_only()
+        return self.async_show_form(
+            step_id="discovery_confirm",
+            description_placeholders={
+                "name": self.data[CONF_NAME],
+            },
+        )
+
     async def async_step_user(
         self, user_input: dict[str, Any] | None = None
     ) -> ConfigFlowResult:
@@ -51,7 +99,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
                 _LOGGER.exception("Could not connect to Russound RIO")
                 errors["base"] = "cannot_connect"
             else:
-                await self.async_set_unique_id(controller.mac_address)
+                await self.async_set_unique_id(
+                    controller.mac_address, raise_on_progress=False
+                )
                 if self.source == SOURCE_RECONFIGURE:
                     self._abort_if_unique_id_mismatch(reason="wrong_device")
                     return self.async_update_reload_and_abort(
diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json
index 582dfa85a9a75b992a56e9ed2ffa9b929f4033f1..35c53ed71febd56f6e75aae5f6881a75e6ba45c0 100644
--- a/homeassistant/components/russound_rio/manifest.json
+++ b/homeassistant/components/russound_rio/manifest.json
@@ -7,5 +7,6 @@
   "iot_class": "local_push",
   "loggers": ["aiorussound"],
   "quality_scale": "silver",
-  "requirements": ["aiorussound==4.2.0"]
+  "requirements": ["aiorussound==4.2.0"],
+  "zeroconf": ["_rio._tcp.local."]
 }
diff --git a/homeassistant/components/russound_rio/quality_scale.yaml b/homeassistant/components/russound_rio/quality_scale.yaml
index d2bf8065c1bf4254867d0e58f4c8f966cde52e26..02b1eaa6aae705bf5b40b8183891dd948a64e099 100644
--- a/homeassistant/components/russound_rio/quality_scale.yaml
+++ b/homeassistant/components/russound_rio/quality_scale.yaml
@@ -57,7 +57,7 @@ rules:
     status: exempt
     comment: |
       This integration doesn't have enough / noisy entities that warrant being disabled by default.
-  discovery: todo
+  discovery: done
   stale-devices: todo
   diagnostics: done
   exception-translations: done
@@ -67,7 +67,7 @@ rules:
       There are no entities that require icons.
   reconfiguration-flow: done
   dynamic-devices: todo
-  discovery-update-info: todo
+  discovery-update-info: done
   repair-issues:
     status: exempt
     comment: |
diff --git a/homeassistant/components/russound_rio/strings.json b/homeassistant/components/russound_rio/strings.json
index ae4e2f7ffd2091cd874ded2df4b0d13fc34e61d5..534c321e631d6e7e75af669d5f162c25ca40289a 100644
--- a/homeassistant/components/russound_rio/strings.json
+++ b/homeassistant/components/russound_rio/strings.json
@@ -15,6 +15,9 @@
           "port": "The port of the Russound controller."
         }
       },
+      "discovery_confirm": {
+        "description": "Do you want to setup {name}?"
+      },
       "reconfigure": {
         "description": "Reconfigure your Russound controller.",
         "data": {
diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py
index 66c576d8840ce31784226a9e78cbf3309f435c1c..0766e1ce01106ff59d1622109ce416e65ad22845 100644
--- a/homeassistant/generated/zeroconf.py
+++ b/homeassistant/generated/zeroconf.py
@@ -775,6 +775,11 @@ ZEROCONF = {
             },
         },
     ],
+    "_rio._tcp.local.": [
+        {
+            "domain": "russound_rio",
+        },
+    ],
     "_sideplay._tcp.local.": [
         {
             "domain": "ecobee",
diff --git a/tests/components/russound_rio/test_config_flow.py b/tests/components/russound_rio/test_config_flow.py
index 060d5114f590fa5f36674390a35351be128dc1f5..51cbb9772dcb9e2bb5830ef8371ea0e300a4ed47 100644
--- a/tests/components/russound_rio/test_config_flow.py
+++ b/tests/components/russound_rio/test_config_flow.py
@@ -1,9 +1,11 @@
 """Test the Russound RIO config flow."""
 
+from ipaddress import ip_address
 from unittest.mock import AsyncMock
 
 from homeassistant.components.russound_rio.const import DOMAIN
-from homeassistant.config_entries import SOURCE_USER, ConfigFlowResult
+from homeassistant.components.zeroconf import ZeroconfServiceInfo
+from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF, ConfigFlowResult
 from homeassistant.const import CONF_HOST, CONF_PORT
 from homeassistant.core import HomeAssistant
 from homeassistant.data_entry_flow import FlowResultType
@@ -12,6 +14,23 @@ from .const import MOCK_CONFIG, MOCK_RECONFIGURATION_CONFIG, MODEL
 
 from tests.common import MockConfigEntry
 
+ZEROCONF_DISCOVERY = ZeroconfServiceInfo(
+    ip_address=ip_address("192.168.20.17"),
+    ip_addresses=[ip_address("192.168.20.17")],
+    hostname="controller1.local.",
+    name="controller1._stream-magic._tcp.local.",
+    port=9621,
+    type="_rio._tcp.local.",
+    properties={
+        "txtvers": "0",
+        "productType": "2",
+        "productId": "59",
+        "version": "07.04.00",
+        "buildDate": "Jul 8 2019",
+        "localName": "0",
+    },
+)
+
 
 async def test_form(
     hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound_client: AsyncMock
@@ -89,6 +108,159 @@ async def test_duplicate(
     assert result["reason"] == "already_configured"
 
 
+async def test_zeroconf_flow(
+    hass: HomeAssistant,
+    mock_russound_client: AsyncMock,
+    mock_setup_entry: AsyncMock,
+) -> None:
+    """Test zeroconf flow."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_ZEROCONF},
+        data=ZEROCONF_DISCOVERY,
+    )
+    assert result["type"] is FlowResultType.FORM
+    assert result["step_id"] == "discovery_confirm"
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {},
+    )
+    assert result["type"] is FlowResultType.CREATE_ENTRY
+    assert result["title"] == "MCA-C5"
+    assert result["data"] == {
+        CONF_HOST: "192.168.20.17",
+        CONF_PORT: 9621,
+    }
+    assert result["result"].unique_id == "00:11:22:33:44:55"
+
+
+async def test_zeroconf_flow_errors(
+    hass: HomeAssistant,
+    mock_russound_client: AsyncMock,
+    mock_setup_entry: AsyncMock,
+) -> None:
+    """Test zeroconf flow."""
+    mock_russound_client.connect.side_effect = TimeoutError
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_ZEROCONF},
+        data=ZEROCONF_DISCOVERY,
+    )
+    assert result["type"] is FlowResultType.ABORT
+    assert result["reason"] == "cannot_connect"
+
+    mock_russound_client.connect.side_effect = None
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_ZEROCONF},
+        data=ZEROCONF_DISCOVERY,
+    )
+    assert result["type"] is FlowResultType.FORM
+    assert result["step_id"] == "discovery_confirm"
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {},
+    )
+    assert result["type"] is FlowResultType.CREATE_ENTRY
+    assert result["title"] == "MCA-C5"
+    assert result["data"] == {
+        CONF_HOST: "192.168.20.17",
+        CONF_PORT: 9621,
+    }
+    assert result["result"].unique_id == "00:11:22:33:44:55"
+
+
+async def test_zeroconf_duplicate(
+    hass: HomeAssistant,
+    mock_russound_client: AsyncMock,
+    mock_setup_entry: AsyncMock,
+    mock_config_entry: MockConfigEntry,
+) -> None:
+    """Test duplicate flow."""
+    mock_config_entry.add_to_hass(hass)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_ZEROCONF},
+        data=ZEROCONF_DISCOVERY,
+    )
+    assert result["type"] is FlowResultType.ABORT
+    assert result["reason"] == "already_configured"
+
+
+async def test_zeroconf_duplicate_different_ip(
+    hass: HomeAssistant,
+    mock_russound_client: AsyncMock,
+    mock_setup_entry: AsyncMock,
+    mock_config_entry: MockConfigEntry,
+) -> None:
+    """Test duplicate flow with different IP."""
+    mock_config_entry.add_to_hass(hass)
+
+    ZEROCONF_DISCOVERY_DIFFERENT_IP = ZeroconfServiceInfo(
+        ip_address=ip_address("192.168.20.18"),
+        ip_addresses=[ip_address("192.168.20.18")],
+        hostname="controller1.local.",
+        name="controller1._stream-magic._tcp.local.",
+        port=9621,
+        type="_rio._tcp.local.",
+        properties={
+            "txtvers": "0",
+            "productType": "2",
+            "productId": "59",
+            "version": "07.04.00",
+            "buildDate": "Jul 8 2019",
+            "localName": "0",
+        },
+    )
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_ZEROCONF},
+        data=ZEROCONF_DISCOVERY_DIFFERENT_IP,
+    )
+    assert result["type"] is FlowResultType.ABORT
+    assert result["reason"] == "already_configured"
+
+    entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
+    assert entry
+    assert entry.data == {
+        CONF_HOST: "192.168.20.18",
+        CONF_PORT: 9621,
+    }
+
+
+async def test_user_flow_works_discovery(
+    hass: HomeAssistant,
+    mock_russound_client: AsyncMock,
+    mock_setup_entry: AsyncMock,
+) -> None:
+    """Test user flow can continue after discovery happened."""
+    await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_ZEROCONF},
+        data=ZEROCONF_DISCOVERY,
+    )
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": SOURCE_USER}
+    )
+    assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2
+    assert result["type"] is FlowResultType.FORM
+    assert result["step_id"] == "user"
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        MOCK_CONFIG,
+    )
+
+    assert result["type"] is FlowResultType.CREATE_ENTRY
+
+    assert not hass.config_entries.flow.async_progress(DOMAIN)
+
+
 async def _start_reconfigure_flow(
     hass: HomeAssistant, mock_config_entry: MockConfigEntry
 ) -> ConfigFlowResult: