diff --git a/CODEOWNERS b/CODEOWNERS index 33ae9d167c2..5198f12519c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -277,8 +277,6 @@ build.json @home-assistant/supervisor /tests/components/discord/ @tkdrob /homeassistant/components/discovergy/ @jpbede /tests/components/discovergy/ @jpbede -/homeassistant/components/discovery/ @home-assistant/core -/tests/components/discovery/ @home-assistant/core /homeassistant/components/dlink/ @tkdrob /tests/components/dlink/ @tkdrob /homeassistant/components/dlna_dmr/ @StevenLooman @chishm diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py deleted file mode 100644 index 79653e1c9bc..00000000000 --- a/homeassistant/components/discovery/__init__.py +++ /dev/null @@ -1,248 +0,0 @@ -"""Starts a service to scan in intervals for new devices.""" -from __future__ import annotations - -from datetime import datetime, timedelta -import json -import logging -from typing import NamedTuple - -from netdisco.discovery import NetworkDiscovery -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.components import zeroconf -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import Event, HassJob, HomeAssistant, callback -from homeassistant.helpers import discovery_flow -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import async_discover, async_load_platform -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import async_get_zeroconf -import homeassistant.util.dt as dt_util - -DOMAIN = "discovery" - -SCAN_INTERVAL = timedelta(seconds=300) -SERVICE_APPLE_TV = "apple_tv" -SERVICE_DAIKIN = "daikin" -SERVICE_DLNA_DMR = "dlna_dmr" -SERVICE_ENIGMA2 = "enigma2" -SERVICE_HASS_IOS_APP = "hass_ios" -SERVICE_HASSIO = "hassio" -SERVICE_HEOS = "heos" -SERVICE_KONNECTED = "konnected" -SERVICE_MOBILE_APP = "hass_mobile_app" -SERVICE_NETGEAR = "netgear_router" -SERVICE_OCTOPRINT = "octoprint" -SERVICE_SABNZBD = "sabnzbd" -SERVICE_SAMSUNG_PRINTER = "samsung_printer" -SERVICE_TELLDUSLIVE = "tellstick" -SERVICE_YEELIGHT = "yeelight" -SERVICE_WEMO = "belkin_wemo" -SERVICE_XIAOMI_GW = "xiaomi_gw" - -# These have custom protocols -CONFIG_ENTRY_HANDLERS = { - SERVICE_TELLDUSLIVE: "tellduslive", - "logitech_mediaserver": "squeezebox", -} - - -class ServiceDetails(NamedTuple): - """Store service details.""" - - component: str - platform: str | None - - -# These have no config flows -SERVICE_HANDLERS = { - SERVICE_ENIGMA2: ServiceDetails("media_player", "enigma2"), - "yamaha": ServiceDetails("media_player", "yamaha"), - "bluesound": ServiceDetails("media_player", "bluesound"), -} - -OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {} - -MIGRATED_SERVICE_HANDLERS = [ - SERVICE_APPLE_TV, - "axis", - "bose_soundtouch", - "deconz", - SERVICE_DAIKIN, - "denonavr", - SERVICE_DLNA_DMR, - "esphome", - "google_cast", - SERVICE_HASS_IOS_APP, - SERVICE_HASSIO, - SERVICE_HEOS, - "harmony", - "homekit", - "ikea_tradfri", - "kodi", - SERVICE_KONNECTED, - SERVICE_MOBILE_APP, - SERVICE_NETGEAR, - SERVICE_OCTOPRINT, - "openhome", - "philips_hue", - SERVICE_SAMSUNG_PRINTER, - "sonos", - "songpal", - SERVICE_WEMO, - SERVICE_XIAOMI_GW, - "volumio", - SERVICE_YEELIGHT, - SERVICE_SABNZBD, - "nanoleaf_aurora", - "lg_smart_device", -] - -DEFAULT_ENABLED = ( - list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + MIGRATED_SERVICE_HANDLERS -) -DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + MIGRATED_SERVICE_HANDLERS - -CONF_IGNORE = "ignore" -CONF_ENABLE = "enable" - -CONFIG_SCHEMA = vol.Schema( - { - vol.Optional(DOMAIN): vol.Schema( - { - vol.Optional(CONF_IGNORE, default=[]): vol.All( - cv.ensure_list, [vol.In(DEFAULT_ENABLED)] - ), - vol.Optional(CONF_ENABLE, default=[]): vol.All( - cv.ensure_list, [vol.In(DEFAULT_DISABLED + DEFAULT_ENABLED)] - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Start a discovery service.""" - - logger = logging.getLogger(__name__) - netdisco = NetworkDiscovery() - already_discovered = set() - - if DOMAIN in config: - # Platforms ignore by config - ignored_platforms = config[DOMAIN][CONF_IGNORE] - - # Optional platforms enabled by config - enabled_platforms = config[DOMAIN][CONF_ENABLE] - else: - ignored_platforms = [] - enabled_platforms = [] - - for platform in enabled_platforms: - if platform in DEFAULT_ENABLED: - logger.warning( - ( - "Please remove %s from your discovery.enable configuration " - "as it is now enabled by default" - ), - platform, - ) - - zeroconf_instance = await zeroconf.async_get_instance(hass) - # Do not scan for types that have already been converted - # as it will generate excess network traffic for questions - # the zeroconf instance already knows the answers - zeroconf_types = list(await async_get_zeroconf(hass)) - - async def new_service_found(service, info): - """Handle a new service if one is found.""" - if service in MIGRATED_SERVICE_HANDLERS: - return - - if service in ignored_platforms: - logger.info("Ignoring service: %s %s", service, info) - return - - discovery_hash = json.dumps([service, info], sort_keys=True) - if discovery_hash in already_discovered: - logger.debug("Already discovered service %s %s.", service, info) - return - - already_discovered.add(discovery_hash) - - if service in CONFIG_ENTRY_HANDLERS: - discovery_flow.async_create_flow( - hass, - CONFIG_ENTRY_HANDLERS[service], - context={"source": config_entries.SOURCE_DISCOVERY}, - data=info, - ) - return - - service_details = SERVICE_HANDLERS.get(service) - - if not service_details and service in enabled_platforms: - service_details = OPTIONAL_SERVICE_HANDLERS[service] - - # We do not know how to handle this service. - if not service_details: - logger.debug("Unknown service discovered: %s %s", service, info) - return - - logger.info("Found new service: %s %s", service, info) - - if service_details.platform is None: - await async_discover(hass, service, info, service_details.component, config) - else: - await async_load_platform( - hass, service_details.component, service_details.platform, info, config - ) - - async def scan_devices(now: datetime) -> None: - """Scan for devices.""" - try: - results = await hass.async_add_executor_job( - _discover, netdisco, zeroconf_instance, zeroconf_types - ) - - for result in results: - hass.async_create_task(new_service_found(*result)) - except OSError: - logger.error("Network is unreachable") - - async_track_point_in_utc_time( - hass, scan_devices_job, dt_util.utcnow() + SCAN_INTERVAL - ) - - @callback - def schedule_first(event: Event) -> None: - """Schedule the first discovery when Home Assistant starts up.""" - async_track_point_in_utc_time(hass, scan_devices_job, dt_util.utcnow()) - - scan_devices_job = HassJob(scan_devices, cancel_on_shutdown=True) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, schedule_first) - - return True - - -def _discover(netdisco, zeroconf_instance, zeroconf_types): - """Discover devices.""" - results = [] - try: - netdisco.scan( - zeroconf_instance=zeroconf_instance, suppress_mdns_types=zeroconf_types - ) - - for disc in netdisco.discover(): - for service in netdisco.get_info(disc): - results.append((disc, service)) - - finally: - netdisco.stop() - - return results diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json deleted file mode 100644 index d6d3443f562..00000000000 --- a/homeassistant/components/discovery/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "discovery", - "name": "Discovery", - "after_dependencies": ["zeroconf"], - "codeowners": ["@home-assistant/core"], - "documentation": "https://www.home-assistant.io/integrations/discovery", - "integration_type": "system", - "loggers": ["netdisco"], - "quality_scale": "internal", - "requirements": ["netdisco==3.0.0"] -} diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index 6d84a5ffd0a..75d4b0b9a00 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -1,7 +1,6 @@ { "domain": "xiaomi_aqara", "name": "Xiaomi Gateway (Aqara)", - "after_dependencies": ["discovery"], "codeowners": ["@danielhiversen", "@syssi"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", diff --git a/requirements_all.txt b/requirements_all.txt index 90f0f7aeb4a..9065b918ba5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1240,9 +1240,6 @@ nessclient==0.10.0 # homeassistant.components.netdata netdata==1.1.0 -# homeassistant.components.discovery -netdisco==3.0.0 - # homeassistant.components.nmap_tracker netmap==0.7.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 255ce1ce7ec..e9b238f1d17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -951,9 +951,6 @@ ndms2-client==0.1.2 # homeassistant.components.ness_alarm nessclient==0.10.0 -# homeassistant.components.discovery -netdisco==3.0.0 - # homeassistant.components.nmap_tracker netmap==0.7.0.2 diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 9a7caec925b..4515f52d8a3 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -62,7 +62,6 @@ NO_IOT_CLASS = [ "device_automation", "device_tracker", "diagnostics", - "discovery", "downloader", "ffmpeg", "file_upload", diff --git a/tests/components/discovery/__init__.py b/tests/components/discovery/__init__.py deleted file mode 100644 index b5744b42d6b..00000000000 --- a/tests/components/discovery/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the discovery component.""" diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py deleted file mode 100644 index 7a9fda82511..00000000000 --- a/tests/components/discovery/test_init.py +++ /dev/null @@ -1,105 +0,0 @@ -"""The tests for the discovery component.""" -from unittest.mock import MagicMock, patch - -import pytest - -from homeassistant import config_entries -from homeassistant.bootstrap import async_setup_component -from homeassistant.components import discovery -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import HomeAssistant -from homeassistant.util.dt import utcnow - -from tests.common import async_fire_time_changed, mock_coro - -# One might consider to "mock" services, but it's easy enough to just use -# what is already available. -SERVICE = "yamaha" -SERVICE_COMPONENT = "media_player" - -SERVICE_INFO = {"key": "value"} # Can be anything - -UNKNOWN_SERVICE = "this_service_will_never_be_supported" - -BASE_CONFIG = {discovery.DOMAIN: {"ignore": [], "enable": []}} - - -@pytest.fixture(autouse=True) -def netdisco_mock(): - """Mock netdisco.""" - with patch.dict("sys.modules", {"netdisco.discovery": MagicMock()}): - yield - - -async def mock_discovery(hass, discoveries, config=BASE_CONFIG): - """Mock discoveries.""" - with patch("homeassistant.components.zeroconf.async_get_instance"), patch( - "homeassistant.components.zeroconf.async_setup", return_value=True - ), patch.object(discovery, "_discover", discoveries), patch( - "homeassistant.components.discovery.async_discover" - ) as mock_discover, patch( - "homeassistant.components.discovery.async_load_platform", - return_value=mock_coro(), - ) as mock_platform: - assert await async_setup_component(hass, "discovery", config) - await hass.async_block_till_done() - await hass.async_start() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow()) - # Work around an issue where our loop.call_soon not get caught - await hass.async_block_till_done() - await hass.async_block_till_done() - - return mock_discover, mock_platform - - -async def test_unknown_service(hass: HomeAssistant) -> None: - """Test that unknown service is ignored.""" - - def discover(netdisco, zeroconf_instance, suppress_mdns_types): - """Fake discovery.""" - return [("this_service_will_never_be_supported", {"info": "some"})] - - mock_discover, mock_platform = await mock_discovery(hass, discover) - - assert not mock_discover.called - assert not mock_platform.called - - -async def test_load_platform(hass: HomeAssistant) -> None: - """Test load a platform.""" - - def discover(netdisco, zeroconf_instance, suppress_mdns_types): - """Fake discovery.""" - return [(SERVICE, SERVICE_INFO)] - - mock_discover, mock_platform = await mock_discovery(hass, discover) - - assert not mock_discover.called - assert mock_platform.called - mock_platform.assert_called_with( - hass, SERVICE_COMPONENT, SERVICE, SERVICE_INFO, BASE_CONFIG - ) - - -async def test_discover_config_flow(hass: HomeAssistant) -> None: - """Test discovery triggering a config flow.""" - discovery_info = {"hello": "world"} - - def discover(netdisco, zeroconf_instance, suppress_mdns_types): - """Fake discovery.""" - return [("mock-service", discovery_info)] - - with patch.dict( - discovery.CONFIG_ENTRY_HANDLERS, {"mock-service": "mock-component"} - ), patch( - "homeassistant.config_entries.ConfigEntriesFlowManager.async_init" - ) as m_init: - await mock_discovery(hass, discover) - - assert len(m_init.mock_calls) == 1 - args, kwargs = m_init.mock_calls[0][1:] - assert args == ("mock-component",) - assert kwargs["context"]["source"] == config_entries.SOURCE_DISCOVERY - assert kwargs["data"] == discovery_info