diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index b97202033bd..2b293179888 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -1,11 +1,4 @@ -""" -Starts a service to scan in intervals for new devices. - -Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered. - -Knows which components handle certain types, will make sure they are -loaded before the EVENT_PLATFORM_DISCOVERED is fired. -""" +"""Starts a service to scan in intervals for new devices.""" from datetime import timedelta import json import logging diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 6f178f26578..66b804927c7 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -6,7 +6,6 @@ from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol -from homeassistant.components.discovery import SERVICE_OCTOPRINT from homeassistant.const import ( CONF_API_KEY, CONF_BINARY_SENSORS, @@ -22,7 +21,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TIME_SECONDS, ) -from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.util import slugify as util_slugify @@ -132,12 +130,6 @@ def setup(hass, config): printers = hass.data[DOMAIN] = {} success = False - def device_discovered(service, info): - """Get called when an Octoprint server has been discovered.""" - _LOGGER.debug("Found an Octoprint server: %s", info) - - discovery.listen(hass, SERVICE_OCTOPRINT, device_discovered) - if DOMAIN not in config: # Skip the setup if there is no configuration present return True diff --git a/homeassistant/const.py b/homeassistant/const.py index f8d2507d051..712f7ede0d3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -210,7 +210,6 @@ EVENT_HOMEASSISTANT_STARTED = "homeassistant_started" EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" EVENT_HOMEASSISTANT_FINAL_WRITE = "homeassistant_final_write" EVENT_LOGBOOK_ENTRY = "logbook_entry" -EVENT_PLATFORM_DISCOVERED = "platform_discovered" EVENT_SERVICE_REGISTERED = "service_registered" EVENT_SERVICE_REMOVED = "service_removed" EVENT_STATE_CHANGED = "state_changed" @@ -313,9 +312,6 @@ CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial" # Electrical attributes ATTR_VOLTAGE = "voltage" -# Contains the information that is discovered -ATTR_DISCOVERED = "discovered" - # Location of the device/sensor ATTR_LOCATION = "location" diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 0770e6798f1..7ee72759d65 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,62 +5,55 @@ There are two different types of discoveries that can be fired/listened for. - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ -from typing import Any, Callable, Collection, Dict, Optional, Union +from typing import Any, Callable, Dict, Optional, TypedDict from homeassistant import core, setup -from homeassistant.const import ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED from homeassistant.core import CALLBACK_TYPE -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import bind_hass -from homeassistant.util.async_ import run_callback_threadsafe +from .dispatcher import async_dispatcher_connect, async_dispatcher_send +from .typing import ConfigType, DiscoveryInfoType + +SIGNAL_PLATFORM_DISCOVERED = "discovery.platform_discovered_{}" EVENT_LOAD_PLATFORM = "load_platform.{}" ATTR_PLATFORM = "platform" +ATTR_DISCOVERED = "discovered" # mypy: disallow-any-generics -@bind_hass -def listen( - hass: core.HomeAssistant, - service: Union[str, Collection[str]], - callback: CALLBACK_TYPE, -) -> None: - """Set up listener for discovery of specific service. +class DiscoveryDict(TypedDict): + """Discovery data.""" - Service can be a string or a list/tuple. - """ - run_callback_threadsafe(hass.loop, async_listen, hass, service, callback).result() + service: str + platform: Optional[str] + discovered: Optional[DiscoveryInfoType] @core.callback @bind_hass def async_listen( hass: core.HomeAssistant, - service: Union[str, Collection[str]], + service: str, callback: CALLBACK_TYPE, ) -> None: """Set up listener for discovery of specific service. Service can be a string or a list/tuple. """ - if isinstance(service, str): - service = (service,) - else: - service = tuple(service) - job = core.HassJob(callback) - async def discovery_event_listener(event: core.Event) -> None: + async def discovery_event_listener(discovered: DiscoveryDict) -> None: """Listen for discovery events.""" - if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service: - task = hass.async_run_hass_job( - job, event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED) - ) - if task: - await task + task = hass.async_run_hass_job( + job, discovered["service"], discovered["discovered"] + ) + if task: + await task - hass.bus.async_listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener) + async_dispatcher_connect( + hass, SIGNAL_PLATFORM_DISCOVERED.format(service), discovery_event_listener + ) @bind_hass @@ -91,22 +84,13 @@ async def async_discover( if component is not None and component not in hass.config.components: await setup.async_setup_component(hass, component, hass_config) - data: Dict[str, Any] = {ATTR_SERVICE: service} + data: DiscoveryDict = { + "service": service, + "platform": None, + "discovered": discovered, + } - if discovered is not None: - data[ATTR_DISCOVERED] = discovered - - hass.bus.async_fire(EVENT_PLATFORM_DISCOVERED, data) - - -@bind_hass -def listen_platform( - hass: core.HomeAssistant, component: str, callback: CALLBACK_TYPE -) -> None: - """Register a platform loader listener.""" - run_callback_threadsafe( - hass.loop, async_listen_platform, hass, component, callback - ).result() + async_dispatcher_send(hass, SIGNAL_PLATFORM_DISCOVERED.format(service), data) @bind_hass @@ -122,21 +106,20 @@ def async_listen_platform( service = EVENT_LOAD_PLATFORM.format(component) job = core.HassJob(callback) - async def discovery_platform_listener(event: core.Event) -> None: + async def discovery_platform_listener(discovered: DiscoveryDict) -> None: """Listen for platform discovery events.""" - if event.data.get(ATTR_SERVICE) != service: - return - - platform = event.data.get(ATTR_PLATFORM) + platform = discovered["platform"] if not platform: return - task = hass.async_run_hass_job(job, platform, event.data.get(ATTR_DISCOVERED)) + task = hass.async_run_hass_job(job, platform, discovered.get("discovered")) if task: await task - hass.bus.async_listen(EVENT_PLATFORM_DISCOVERED, discovery_platform_listener) + async_dispatcher_connect( + hass, SIGNAL_PLATFORM_DISCOVERED.format(service), discovery_platform_listener + ) @bind_hass @@ -147,16 +130,7 @@ def load_platform( discovered: DiscoveryInfoType, hass_config: ConfigType, ) -> None: - """Load a component and platform dynamically. - - Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be - fired to load the platform. The event will contain: - { ATTR_SERVICE = EVENT_LOAD_PLATFORM + '.' + <> - ATTR_PLATFORM = <> - ATTR_DISCOVERED = <> } - - Use `listen_platform` to register a callback for these events. - """ + """Load a component and platform dynamically.""" hass.add_job( async_load_platform( # type: ignore hass, component, platform, discovered, hass_config @@ -174,18 +148,10 @@ async def async_load_platform( ) -> None: """Load a component and platform dynamically. - Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be - fired to load the platform. The event will contain: - { ATTR_SERVICE = EVENT_LOAD_PLATFORM + '.' + <> - ATTR_PLATFORM = <> - ATTR_DISCOVERED = <> } - - Use `listen_platform` to register a callback for these events. + Use `async_listen_platform` to register a callback for these events. Warning: Do not await this inside a setup method to avoid a dead lock. Use `hass.async_create_task(async_load_platform(..))` instead. - - This method is a coroutine. """ assert hass_config, "You need to pass in the real hass config" @@ -194,16 +160,16 @@ async def async_load_platform( if component not in hass.config.components: setup_success = await setup.async_setup_component(hass, component, hass_config) - # No need to fire event if we could not set up component + # No need to send signal if we could not set up component if not setup_success: return - data: Dict[str, Any] = { - ATTR_SERVICE: EVENT_LOAD_PLATFORM.format(component), - ATTR_PLATFORM: platform, + service = EVENT_LOAD_PLATFORM.format(component) + + data: DiscoveryDict = { + "service": service, + "platform": platform, + "discovered": discovered, } - if discovered is not None: - data[ATTR_DISCOVERED] = discovered - - hass.bus.async_fire(EVENT_PLATFORM_DISCOVERED, data) + async_dispatcher_send(hass, SIGNAL_PLATFORM_DISCOVERED.format(service), data) diff --git a/tests/common.py b/tests/common.py index 52d368853b3..0ae6f7ef5c7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -36,11 +36,8 @@ from homeassistant.components.device_automation import ( # noqa: F401 from homeassistant.components.mqtt.models import Message from homeassistant.config import async_process_component_config from homeassistant.const import ( - ATTR_DISCOVERED, - ATTR_SERVICE, DEVICE_DEFAULT_NAME, EVENT_HOMEASSISTANT_CLOSE, - EVENT_PLATFORM_DISCOVERED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, STATE_OFF, @@ -387,21 +384,6 @@ def async_fire_time_changed(hass, datetime_, fire_all=False): fire_time_changed = threadsafe_callback_factory(async_fire_time_changed) -def fire_service_discovered(hass, service, info): - """Fire the MQTT message.""" - hass.bus.fire( - EVENT_PLATFORM_DISCOVERED, {ATTR_SERVICE: service, ATTR_DISCOVERED: info} - ) - - -@ha.callback -def async_fire_service_discovered(hass, service, info): - """Fire the MQTT message.""" - hass.bus.async_fire( - EVENT_PLATFORM_DISCOVERED, {ATTR_SERVICE: service, ATTR_DISCOVERED: info} - ) - - def load_fixture(filename): """Load a fixture.""" path = os.path.join(os.path.dirname(__file__), "fixtures", filename) diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 64f39fb13bd..d0d8580d69d 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -4,6 +4,8 @@ from unittest.mock import patch from homeassistant import setup from homeassistant.core import callback from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.util.async_ import run_callback_threadsafe from tests.common import ( MockModule, @@ -31,23 +33,22 @@ class TestHelpersDiscovery: """Test discovery listen/discover combo.""" helpers = self.hass.helpers calls_single = [] - calls_multi = [] @callback def callback_single(service, info): """Service discovered callback.""" calls_single.append((service, info)) - @callback - def callback_multi(service, info): - """Service discovered callback.""" - calls_multi.append((service, info)) + self.hass.add_job( + helpers.discovery.async_listen, "test service", callback_single + ) - helpers.discovery.listen("test service", callback_single) - helpers.discovery.listen(["test service", "another service"], callback_multi) - - helpers.discovery.discover( - "test service", "discovery info", "test_component", {} + self.hass.add_job( + helpers.discovery.async_discover, + "test service", + "discovery info", + "test_component", + {}, ) self.hass.block_till_done() @@ -56,15 +57,6 @@ class TestHelpersDiscovery: assert len(calls_single) == 1 assert calls_single[0] == ("test service", "discovery info") - helpers.discovery.discover( - "another service", "discovery info", "test_component", {} - ) - self.hass.block_till_done() - - assert len(calls_single) == 1 - assert len(calls_multi) == 2 - assert ["test service", "another service"] == [info[0] for info in calls_multi] - @patch("homeassistant.setup.async_setup_component", return_value=mock_coro(True)) def test_platform(self, mock_setup_component): """Test discover platform method.""" @@ -75,7 +67,13 @@ class TestHelpersDiscovery: """Platform callback method.""" calls.append((platform, info)) - discovery.listen_platform(self.hass, "test_component", platform_callback) + run_callback_threadsafe( + self.hass.loop, + discovery.async_listen_platform, + self.hass, + "test_component", + platform_callback, + ).result() discovery.load_platform( self.hass, @@ -105,13 +103,10 @@ class TestHelpersDiscovery: assert len(calls) == 1 assert calls[0] == ("test_platform", "discovery info") - self.hass.bus.fire( - discovery.EVENT_PLATFORM_DISCOVERED, - { - discovery.ATTR_SERVICE: discovery.EVENT_LOAD_PLATFORM.format( - "test_component" - ) - }, + dispatcher_send( + self.hass, + discovery.SIGNAL_PLATFORM_DISCOVERED, + {"service": discovery.EVENT_LOAD_PLATFORM.format("test_component")}, ) self.hass.block_till_done() @@ -179,10 +174,12 @@ class TestHelpersDiscovery: """ component_calls = [] - def component1_setup(hass, config): + async def component1_setup(hass, config): """Set up mock component.""" print("component1 setup") - discovery.discover(hass, "test_component2", {}, "test_component2", {}) + await discovery.async_discover( + hass, "test_component2", {}, "test_component2", {} + ) return True def component2_setup(hass, config): @@ -191,7 +188,7 @@ class TestHelpersDiscovery: return True mock_integration( - self.hass, MockModule("test_component1", setup=component1_setup) + self.hass, MockModule("test_component1", async_setup=component1_setup) ) mock_integration( diff --git a/tests/test_setup.py b/tests/test_setup.py index 539ed3f1442..abb8f756989 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -441,10 +441,14 @@ class TestSetup: """Test all init work done till start.""" call_order = [] - def component1_setup(hass, config): + async def component1_setup(hass, config): """Set up mock component.""" - discovery.discover(hass, "test_component2", {}, "test_component2", {}) - discovery.discover(hass, "test_component3", {}, "test_component3", {}) + await discovery.async_discover( + hass, "test_component2", {}, "test_component2", {} + ) + await discovery.async_discover( + hass, "test_component3", {}, "test_component3", {} + ) return True def component_track_setup(hass, config): @@ -453,7 +457,7 @@ class TestSetup: return True mock_integration( - self.hass, MockModule("test_component1", setup=component1_setup) + self.hass, MockModule("test_component1", async_setup=component1_setup) ) mock_integration(