Convert discovery helper to use dispatcher (#47008)

This commit is contained in:
Paulus Schoutsen 2021-02-24 13:37:31 -08:00 committed by GitHub
parent 5ab11df551
commit 557ec374f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 79 additions and 149 deletions

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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 + '.' + <<component>>
ATTR_PLATFORM = <<platform>>
ATTR_DISCOVERED = <<discovery info>> }
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 + '.' + <<component>>
ATTR_PLATFORM = <<platform>>
ATTR_DISCOVERED = <<discovery info>> }
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)

View File

@ -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)

View File

@ -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(

View File

@ -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(