2021-02-24 21:37:31 +00:00
|
|
|
"""Starts a service to scan in intervals for new devices."""
|
Config-flow for DLNA-DMR integration (#55267)
* Modernize dlna_dmr component: configflow, test, types
* Support config-flow with ssdp discovery
* Add unit tests
* Enforce strict typing
* Gracefully handle network devices (dis)appearing
* Fix Aiohttp mock response headers type to match actual response class
* Fixes from code review
* Fixes from code review
* Import device config in flow if unavailable at hass start
* Support SSDP advertisements
* Ignore bad BOOTID, fix ssdp:byebye handling
* Only listen for events on interface connected to device
* Release all listeners when entities are removed
* Warn about deprecated dlna_dmr configuration
* Use sublogger for dlna_dmr.config_flow for easier filtering
* Tests for dlna_dmr.data module
* Rewrite DMR tests for HA style
* Fix DMR strings: "Digital Media *Renderer*"
* Update DMR entity state and device info when changed
* Replace deprecated async_upnp_client State with TransportState
* supported_features are dynamic, based on current device state
* Cleanup fully when subscription fails
* Log warnings when device connection fails unexpectedly
* Set PARALLEL_UPDATES to unlimited
* Fix spelling
* Fixes from code review
* Simplify has & can checks to just can, which includes has
* Treat transitioning state as playing (not idle) to reduce UI jerking
* Test if device is usable
* Handle ssdp:update messages properly
* Fix _remove_ssdp_callbacks being shared by all DlnaDmrEntity instances
* Fix tests for transitioning state
* Mock DmrDevice.is_profile_device (added to support embedded devices)
* Use ST & NT SSDP headers to find DMR devices, not deviceType
The deviceType is extracted from the device's description XML, and will not
be what we want when dealing with embedded devices.
* Use UDN from SSDP headers, not device description, as unique_id
The SSDP headers have the UDN of the embedded device that we're interested
in, whereas the device description (`ATTR_UPNP_UDN`) field will always be
for the root device.
* Fix DMR string English localization
* Test config flow with UDN from SSDP headers
* Bump async-upnp-client==0.22.1, fix flake8 error
* fix test for remapping
* DMR HA Device connections based on root and embedded UDN
* DmrDevice's UpnpDevice is now named profile_device
* Use device type from SSDP headers, not device description
* Mark dlna_dmr constants as Final
* Use embedded device UDN and type for unique ID when connected via URL
* More informative connection error messages
* Also match SSDP messages on NT headers
The NT header is to ssdp:alive messages what ST is to M-SEARCH responses.
* Bump async-upnp-client==0.22.2
* fix merge
* Bump async-upnp-client==0.22.3
Co-authored-by: Steven Looman <steven.looman@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-09-27 20:47:01 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2017-03-01 15:38:49 +00:00
|
|
|
from datetime import timedelta
|
2019-12-09 16:42:00 +00:00
|
|
|
import json
|
2015-01-09 08:07:58 +00:00
|
|
|
import logging
|
|
|
|
|
2019-11-25 11:15:55 +00:00
|
|
|
from netdisco.discovery import NetworkDiscovery
|
2016-09-30 02:02:22 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2018-08-09 11:24:14 +00:00
|
|
|
from homeassistant import config_entries
|
2020-06-30 00:34:38 +00:00
|
|
|
from homeassistant.components import zeroconf
|
2020-07-22 00:18:43 +00:00
|
|
|
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
2019-12-09 16:42:00 +00:00
|
|
|
from homeassistant.core import callback
|
2017-03-01 15:38:49 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2019-12-09 16:42:00 +00:00
|
|
|
from homeassistant.helpers.discovery import async_discover, async_load_platform
|
2017-03-01 15:38:49 +00:00
|
|
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
2021-06-28 09:46:56 +00:00
|
|
|
from homeassistant.loader import async_get_zeroconf
|
2017-03-01 15:38:49 +00:00
|
|
|
import homeassistant.util.dt as dt_util
|
2015-01-09 08:07:58 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DOMAIN = "discovery"
|
2015-01-09 08:07:58 +00:00
|
|
|
|
2017-03-01 15:38:49 +00:00
|
|
|
SCAN_INTERVAL = timedelta(seconds=300)
|
2019-07-31 19:25:30 +00:00
|
|
|
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_WINK = "wink"
|
|
|
|
SERVICE_XIAOMI_GW = "xiaomi_gw"
|
2015-07-20 07:07:01 +00:00
|
|
|
|
2021-02-25 11:19:21 +00:00
|
|
|
# These have custom protocols
|
2018-03-30 03:15:40 +00:00
|
|
|
CONFIG_ENTRY_HANDLERS = {
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_TELLDUSLIVE: "tellduslive",
|
2020-06-22 14:29:01 +00:00
|
|
|
"logitech_mediaserver": "squeezebox",
|
2018-03-30 03:15:40 +00:00
|
|
|
}
|
|
|
|
|
2021-02-25 11:19:21 +00:00
|
|
|
# These have no config flows
|
2015-01-09 08:07:58 +00:00
|
|
|
SERVICE_HANDLERS = {
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_ENIGMA2: ("media_player", "enigma2"),
|
|
|
|
SERVICE_SABNZBD: ("sabnzbd", None),
|
|
|
|
"yamaha": ("media_player", "yamaha"),
|
|
|
|
"frontier_silicon": ("media_player", "frontier_silicon"),
|
|
|
|
"openhome": ("media_player", "openhome"),
|
|
|
|
"bose_soundtouch": ("media_player", "soundtouch"),
|
|
|
|
"bluesound": ("media_player", "bluesound"),
|
|
|
|
"lg_smart_device": ("media_player", "lg_soundbar"),
|
2015-01-09 08:07:58 +00:00
|
|
|
}
|
|
|
|
|
Config-flow for DLNA-DMR integration (#55267)
* Modernize dlna_dmr component: configflow, test, types
* Support config-flow with ssdp discovery
* Add unit tests
* Enforce strict typing
* Gracefully handle network devices (dis)appearing
* Fix Aiohttp mock response headers type to match actual response class
* Fixes from code review
* Fixes from code review
* Import device config in flow if unavailable at hass start
* Support SSDP advertisements
* Ignore bad BOOTID, fix ssdp:byebye handling
* Only listen for events on interface connected to device
* Release all listeners when entities are removed
* Warn about deprecated dlna_dmr configuration
* Use sublogger for dlna_dmr.config_flow for easier filtering
* Tests for dlna_dmr.data module
* Rewrite DMR tests for HA style
* Fix DMR strings: "Digital Media *Renderer*"
* Update DMR entity state and device info when changed
* Replace deprecated async_upnp_client State with TransportState
* supported_features are dynamic, based on current device state
* Cleanup fully when subscription fails
* Log warnings when device connection fails unexpectedly
* Set PARALLEL_UPDATES to unlimited
* Fix spelling
* Fixes from code review
* Simplify has & can checks to just can, which includes has
* Treat transitioning state as playing (not idle) to reduce UI jerking
* Test if device is usable
* Handle ssdp:update messages properly
* Fix _remove_ssdp_callbacks being shared by all DlnaDmrEntity instances
* Fix tests for transitioning state
* Mock DmrDevice.is_profile_device (added to support embedded devices)
* Use ST & NT SSDP headers to find DMR devices, not deviceType
The deviceType is extracted from the device's description XML, and will not
be what we want when dealing with embedded devices.
* Use UDN from SSDP headers, not device description, as unique_id
The SSDP headers have the UDN of the embedded device that we're interested
in, whereas the device description (`ATTR_UPNP_UDN`) field will always be
for the root device.
* Fix DMR string English localization
* Test config flow with UDN from SSDP headers
* Bump async-upnp-client==0.22.1, fix flake8 error
* fix test for remapping
* DMR HA Device connections based on root and embedded UDN
* DmrDevice's UpnpDevice is now named profile_device
* Use device type from SSDP headers, not device description
* Mark dlna_dmr constants as Final
* Use embedded device UDN and type for unique ID when connected via URL
* More informative connection error messages
* Also match SSDP messages on NT headers
The NT header is to ssdp:alive messages what ST is to M-SEARCH responses.
* Bump async-upnp-client==0.22.2
* fix merge
* Bump async-upnp-client==0.22.3
Co-authored-by: Steven Looman <steven.looman@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-09-27 20:47:01 +00:00
|
|
|
OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {}
|
2018-04-13 17:25:35 +00:00
|
|
|
|
2019-06-03 17:06:53 +00:00
|
|
|
MIGRATED_SERVICE_HANDLERS = [
|
2021-02-25 11:19:21 +00:00
|
|
|
SERVICE_APPLE_TV,
|
2019-07-31 19:25:30 +00:00
|
|
|
"axis",
|
|
|
|
"deconz",
|
2021-02-25 11:19:21 +00:00
|
|
|
SERVICE_DAIKIN,
|
2020-06-16 12:46:39 +00:00
|
|
|
"denonavr",
|
Config-flow for DLNA-DMR integration (#55267)
* Modernize dlna_dmr component: configflow, test, types
* Support config-flow with ssdp discovery
* Add unit tests
* Enforce strict typing
* Gracefully handle network devices (dis)appearing
* Fix Aiohttp mock response headers type to match actual response class
* Fixes from code review
* Fixes from code review
* Import device config in flow if unavailable at hass start
* Support SSDP advertisements
* Ignore bad BOOTID, fix ssdp:byebye handling
* Only listen for events on interface connected to device
* Release all listeners when entities are removed
* Warn about deprecated dlna_dmr configuration
* Use sublogger for dlna_dmr.config_flow for easier filtering
* Tests for dlna_dmr.data module
* Rewrite DMR tests for HA style
* Fix DMR strings: "Digital Media *Renderer*"
* Update DMR entity state and device info when changed
* Replace deprecated async_upnp_client State with TransportState
* supported_features are dynamic, based on current device state
* Cleanup fully when subscription fails
* Log warnings when device connection fails unexpectedly
* Set PARALLEL_UPDATES to unlimited
* Fix spelling
* Fixes from code review
* Simplify has & can checks to just can, which includes has
* Treat transitioning state as playing (not idle) to reduce UI jerking
* Test if device is usable
* Handle ssdp:update messages properly
* Fix _remove_ssdp_callbacks being shared by all DlnaDmrEntity instances
* Fix tests for transitioning state
* Mock DmrDevice.is_profile_device (added to support embedded devices)
* Use ST & NT SSDP headers to find DMR devices, not deviceType
The deviceType is extracted from the device's description XML, and will not
be what we want when dealing with embedded devices.
* Use UDN from SSDP headers, not device description, as unique_id
The SSDP headers have the UDN of the embedded device that we're interested
in, whereas the device description (`ATTR_UPNP_UDN`) field will always be
for the root device.
* Fix DMR string English localization
* Test config flow with UDN from SSDP headers
* Bump async-upnp-client==0.22.1, fix flake8 error
* fix test for remapping
* DMR HA Device connections based on root and embedded UDN
* DmrDevice's UpnpDevice is now named profile_device
* Use device type from SSDP headers, not device description
* Mark dlna_dmr constants as Final
* Use embedded device UDN and type for unique ID when connected via URL
* More informative connection error messages
* Also match SSDP messages on NT headers
The NT header is to ssdp:alive messages what ST is to M-SEARCH responses.
* Bump async-upnp-client==0.22.2
* fix merge
* Bump async-upnp-client==0.22.3
Co-authored-by: Steven Looman <steven.looman@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2021-09-27 20:47:01 +00:00
|
|
|
SERVICE_DLNA_DMR,
|
2019-07-31 19:25:30 +00:00
|
|
|
"esphome",
|
|
|
|
"google_cast",
|
2021-02-25 11:19:21 +00:00
|
|
|
SERVICE_HASS_IOS_APP,
|
|
|
|
SERVICE_HASSIO,
|
2019-06-12 23:08:08 +00:00
|
|
|
SERVICE_HEOS,
|
2020-03-19 16:29:51 +00:00
|
|
|
"harmony",
|
2019-07-31 19:25:30 +00:00
|
|
|
"homekit",
|
|
|
|
"ikea_tradfri",
|
2020-08-21 04:16:58 +00:00
|
|
|
"kodi",
|
2021-02-25 11:19:21 +00:00
|
|
|
SERVICE_KONNECTED,
|
|
|
|
SERVICE_MOBILE_APP,
|
2021-09-13 16:18:21 +00:00
|
|
|
SERVICE_NETGEAR,
|
2021-02-25 11:19:21 +00:00
|
|
|
SERVICE_OCTOPRINT,
|
2019-07-31 19:25:30 +00:00
|
|
|
"philips_hue",
|
2021-02-25 11:19:21 +00:00
|
|
|
SERVICE_SAMSUNG_PRINTER,
|
2019-07-31 19:25:30 +00:00
|
|
|
"sonos",
|
2020-05-06 22:52:33 +00:00
|
|
|
"songpal",
|
2019-06-03 17:06:53 +00:00
|
|
|
SERVICE_WEMO,
|
2021-02-25 11:19:21 +00:00
|
|
|
SERVICE_WINK,
|
2020-06-22 09:54:17 +00:00
|
|
|
SERVICE_XIAOMI_GW,
|
2020-07-27 07:19:19 +00:00
|
|
|
"volumio",
|
2020-08-31 14:40:56 +00:00
|
|
|
SERVICE_YEELIGHT,
|
2021-08-24 19:09:36 +00:00
|
|
|
"nanoleaf_aurora",
|
2019-06-03 17:06:53 +00:00
|
|
|
]
|
2019-05-29 21:34:44 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
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,
|
|
|
|
)
|
2016-09-30 02:02:22 +00:00
|
|
|
|
2015-01-09 08:07:58 +00:00
|
|
|
|
2018-03-17 03:27:05 +00:00
|
|
|
async def async_setup(hass, config):
|
2016-03-08 16:55:57 +00:00
|
|
|
"""Start a discovery service."""
|
2015-03-01 05:06:59 +00:00
|
|
|
|
2017-03-01 15:38:49 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
netdisco = NetworkDiscovery()
|
2017-03-03 21:11:40 +00:00
|
|
|
already_discovered = set()
|
2015-01-09 08:07:58 +00:00
|
|
|
|
2019-02-08 04:07:15 +00:00
|
|
|
if DOMAIN in config:
|
|
|
|
# Platforms ignore by config
|
|
|
|
ignored_platforms = config[DOMAIN][CONF_IGNORE]
|
2017-02-23 10:54:35 +00:00
|
|
|
|
2019-02-08 04:07:15 +00:00
|
|
|
# Optional platforms enabled by config
|
|
|
|
enabled_platforms = config[DOMAIN][CONF_ENABLE]
|
|
|
|
else:
|
|
|
|
ignored_platforms = []
|
|
|
|
enabled_platforms = []
|
2018-04-13 17:25:35 +00:00
|
|
|
|
2019-05-02 14:57:42 +00:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2020-06-30 00:34:38 +00:00
|
|
|
zeroconf_instance = await zeroconf.async_get_instance(hass)
|
2021-06-28 09:46:56 +00:00
|
|
|
# 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))
|
2020-06-30 00:34:38 +00:00
|
|
|
|
2018-03-17 03:27:05 +00:00
|
|
|
async def new_service_found(service, info):
|
2017-05-02 20:47:20 +00:00
|
|
|
"""Handle a new service if one is found."""
|
2019-05-29 21:34:44 +00:00
|
|
|
if service in MIGRATED_SERVICE_HANDLERS:
|
|
|
|
return
|
|
|
|
|
2017-02-23 10:54:35 +00:00
|
|
|
if service in ignored_platforms:
|
|
|
|
logger.info("Ignoring service: %s %s", service, info)
|
|
|
|
return
|
|
|
|
|
2018-03-30 03:15:40 +00:00
|
|
|
discovery_hash = json.dumps([service, info], sort_keys=True)
|
|
|
|
if discovery_hash in already_discovered:
|
2018-12-18 18:32:42 +00:00
|
|
|
logger.debug("Already discovered service %s %s.", service, info)
|
2018-03-30 03:15:40 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
already_discovered.add(discovery_hash)
|
|
|
|
|
|
|
|
if service in CONFIG_ENTRY_HANDLERS:
|
|
|
|
await hass.config_entries.flow.async_init(
|
|
|
|
CONFIG_ENTRY_HANDLERS[service],
|
2019-07-31 19:25:30 +00:00
|
|
|
context={"source": config_entries.SOURCE_DISCOVERY},
|
|
|
|
data=info,
|
2018-03-30 03:15:40 +00:00
|
|
|
)
|
|
|
|
return
|
|
|
|
|
2017-03-01 15:38:49 +00:00
|
|
|
comp_plat = SERVICE_HANDLERS.get(service)
|
2015-03-22 05:02:47 +00:00
|
|
|
|
2018-04-13 17:25:35 +00:00
|
|
|
if not comp_plat and service in enabled_platforms:
|
|
|
|
comp_plat = OPTIONAL_SERVICE_HANDLERS[service]
|
|
|
|
|
2017-03-01 15:38:49 +00:00
|
|
|
# We do not know how to handle this service.
|
|
|
|
if not comp_plat:
|
2020-07-07 22:29:37 +00:00
|
|
|
logger.debug("Unknown service discovered: %s %s", service, info)
|
2017-03-01 15:38:49 +00:00
|
|
|
return
|
|
|
|
|
2017-03-03 21:11:40 +00:00
|
|
|
logger.info("Found new service: %s %s", service, info)
|
|
|
|
|
2017-03-01 15:38:49 +00:00
|
|
|
component, platform = comp_plat
|
2015-01-09 08:07:58 +00:00
|
|
|
|
2017-03-01 15:38:49 +00:00
|
|
|
if platform is None:
|
2018-03-17 03:27:05 +00:00
|
|
|
await async_discover(hass, service, info, component, config)
|
2017-03-01 15:38:49 +00:00
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
await async_load_platform(hass, component, platform, info, config)
|
2015-01-09 08:07:58 +00:00
|
|
|
|
2018-03-17 03:27:05 +00:00
|
|
|
async def scan_devices(now):
|
2017-03-01 15:38:49 +00:00
|
|
|
"""Scan for devices."""
|
2018-12-29 22:55:43 +00:00
|
|
|
try:
|
2020-10-08 07:20:07 +00:00
|
|
|
results = await hass.async_add_executor_job(
|
2021-06-28 09:46:56 +00:00
|
|
|
_discover, netdisco, zeroconf_instance, zeroconf_types
|
2020-10-08 07:20:07 +00:00
|
|
|
)
|
2015-01-09 08:07:58 +00:00
|
|
|
|
2018-12-29 22:55:43 +00:00
|
|
|
for result in results:
|
|
|
|
hass.async_create_task(new_service_found(*result))
|
|
|
|
except OSError:
|
|
|
|
logger.error("Network is unreachable")
|
2015-01-09 08:07:58 +00:00
|
|
|
|
2018-12-18 18:32:42 +00:00
|
|
|
async_track_point_in_utc_time(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, scan_devices, dt_util.utcnow() + SCAN_INTERVAL
|
|
|
|
)
|
2015-01-09 08:07:58 +00:00
|
|
|
|
2017-04-09 08:05:34 +00:00
|
|
|
@callback
|
|
|
|
def schedule_first(event):
|
|
|
|
"""Schedule the first discovery when Home Assistant starts up."""
|
|
|
|
async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow())
|
|
|
|
|
2020-07-22 00:18:43 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, schedule_first)
|
2015-01-09 08:07:58 +00:00
|
|
|
|
|
|
|
return True
|
2017-03-01 15:38:49 +00:00
|
|
|
|
|
|
|
|
2021-06-28 09:46:56 +00:00
|
|
|
def _discover(netdisco, zeroconf_instance, zeroconf_types):
|
2017-03-01 15:38:49 +00:00
|
|
|
"""Discover devices."""
|
|
|
|
results = []
|
|
|
|
try:
|
2021-06-28 09:46:56 +00:00
|
|
|
netdisco.scan(
|
|
|
|
zeroconf_instance=zeroconf_instance, suppress_mdns_types=zeroconf_types
|
|
|
|
)
|
2017-03-01 15:38:49 +00:00
|
|
|
|
|
|
|
for disc in netdisco.discover():
|
|
|
|
for service in netdisco.get_info(disc):
|
|
|
|
results.append((disc, service))
|
2018-05-15 17:58:00 +00:00
|
|
|
|
2017-03-01 15:38:49 +00:00
|
|
|
finally:
|
|
|
|
netdisco.stop()
|
|
|
|
|
|
|
|
return results
|