Skip to content

Commit

Permalink
Update hostbrowser to only poll audio hosts (#549)
Browse files Browse the repository at this point in the history
  • Loading branch information
emontnemery authored Oct 18, 2021
1 parent 98953a8 commit 7b97be4
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 39 deletions.
29 changes: 8 additions & 21 deletions pychromecast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
stop_discovery,
)
from .dial import get_device_status, DeviceStatus
from .const import CAST_MANUFACTURERS, CAST_TYPES, CAST_TYPE_CHROMECAST
from .const import CAST_MANUFACTURERS, CAST_TYPES, CAST_TYPE_CHROMECAST # noqa: F401
from .controllers.media import STREAM_TYPE_BUFFERED # noqa: F401

__all__ = ("__version__", "__version_info__", "get_chromecasts", "Chromecast")
Expand All @@ -40,17 +40,9 @@ def get_chromecast_from_host(host, tries=None, retry_wait=None, timeout=None):
# Build device status from the mDNS info, this information is
# the primary source and the remaining will be fetched
# later on.
ip_address, port, uuid, model_name, friendly_name = host
_LOGGER.debug("_get_chromecast_from_host %s", host)
cast_type = CAST_TYPES.get(model_name.lower(), CAST_TYPE_CHROMECAST)
manufacturer = CAST_MANUFACTURERS.get(model_name.lower(), "Google Inc.")
device = DeviceStatus(
friendly_name=friendly_name,
model_name=model_name,
manufacturer=manufacturer,
uuid=uuid,
cast_type=cast_type,
)
ip_address, port, _uuid, _model_name, _friendly_name = host
_LOGGER.debug("get_chromecast_from_host %s", host)
device = get_device_status(host)
return Chromecast(
host=ip_address,
port=port,
Expand All @@ -74,15 +66,7 @@ def get_chromecast_from_cast_info(
# fetched later on.
services = cast_info.services
_LOGGER.debug("get_chromecast_from_cast_info %s", services)
cast_type = CAST_TYPES.get(cast_info.model_name.lower(), CAST_TYPE_CHROMECAST)
manufacturer = CAST_MANUFACTURERS.get(cast_info.model_name.lower(), "Google Inc.")
device = DeviceStatus(
friendly_name=cast_info.friendly_name,
model_name=cast_info.model_name,
manufacturer=manufacturer,
uuid=cast_info.uuid,
cast_type=cast_type,
)
device = get_device_status(None, services, zconf)
return Chromecast(
host=None,
device=device,
Expand Down Expand Up @@ -320,6 +304,9 @@ def __init__(self, host, port=None, device=None, **kwargs):
manufacturer=(device.manufacturer or dev_status.manufacturer),
uuid=(device.uuid or dev_status.uuid),
cast_type=(device.cast_type or dev_status.cast_type),
multizone_supported=(
device.multizone_supported or dev_status.multizone_supported
),
)
else:
self.device = device
Expand Down
57 changes: 46 additions & 11 deletions pychromecast/dial.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@

import zeroconf

from .const import CAST_TYPE_CHROMECAST, CAST_TYPES, SERVICE_TYPE_HOST
from .const import (
CAST_TYPE_AUDIO,
CAST_TYPE_CHROMECAST,
CAST_TYPE_GROUP,
SERVICE_TYPE_HOST,
)

XML_NS_UPNP_DEVICE = "{urn:schemas-upnp-org:device-1-0}"

Expand Down Expand Up @@ -97,7 +102,9 @@ def get_ssl_context():
return context


def get_device_status(host, services=None, zconf=None, timeout=30, context=None):
def get_device_status( # pylint: disable=too-many-locals
host, services=None, zconf=None, timeout=30, context=None
):
"""
:param host: Hostname or ip to fetch status from
:type host: str
Expand All @@ -110,28 +117,48 @@ def get_device_status(host, services=None, zconf=None, timeout=30, context=None)
host,
services,
zconf,
"/setup/eureka_info?options=detail",
"/setup/eureka_info?params=device_info,name",
True,
timeout,
context,
)

cast_type = CAST_TYPE_CHROMECAST
display_supported = True
friendly_name = status.get("name", "Unknown Chromecast")
model_name = "Unknown model name"
manufacturer = "Unknown manufacturer"
if "detail" in status:
model_name = status["detail"].get("model_name", model_name)
manufacturer = status["detail"].get("manufacturer", manufacturer)
model_name = "Unknown model name"
multizone_supported = False
udn = None

udn = status.get("ssdp_udn", None)
if "device_info" in status:
device_info = status["device_info"]

cast_type = CAST_TYPES.get(model_name.lower(), CAST_TYPE_CHROMECAST)
capabilities = device_info.get("capabilities", {})
display_supported = capabilities.get("display_supported", True)
multizone_supported = capabilities.get("multizone_supported", True)
friendly_name = device_info.get("name", friendly_name)
model_name = device_info.get("model_name", model_name)
manufacturer = device_info.get("manufacturer", manufacturer)
udn = device_info.get("ssdp_udn", None)

if not display_supported:
cast_type = CAST_TYPE_AUDIO
if model_name.lower() == "google cast group":
cast_type = CAST_TYPE_GROUP

uuid = None
if udn:
uuid = UUID(udn.replace("-", ""))

return DeviceStatus(friendly_name, model_name, manufacturer, uuid, cast_type)
return DeviceStatus(
friendly_name,
model_name,
manufacturer,
uuid,
cast_type,
multizone_supported,
)

except (urllib.error.HTTPError, urllib.error.URLError, OSError, ValueError):
return None
Expand Down Expand Up @@ -198,5 +225,13 @@ def get_multizone_status(host, services=None, zconf=None, timeout=30, context=No
MultizoneStatus = namedtuple("MultizoneStatus", ["dynamic_groups", "groups"])

DeviceStatus = namedtuple(
"DeviceStatus", ["friendly_name", "model_name", "manufacturer", "uuid", "cast_type"]
"DeviceStatus",
[
"friendly_name",
"model_name",
"manufacturer",
"uuid",
"cast_type",
"multizone_supported",
],
)
55 changes: 48 additions & 7 deletions pychromecast/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@

import zeroconf

from .const import SERVICE_TYPE_HOST, SERVICE_TYPE_MDNS
from .const import CAST_TYPE_AUDIO, SERVICE_TYPE_HOST, SERVICE_TYPE_MDNS
from .dial import get_device_status, get_multizone_status, get_ssl_context

DISCOVER_TIMEOUT = 5

# Models matching this list will only be polled once by the HostBrowser
HOST_BROWSER_BLOCKED_MODEL_PREFIXES = [
"HK", # Harman Kardon speakers crash if polled: https://github.com/home-assistant/core/issues/52020
"JBL", # JBL speakers crash if polled: https://github.com/home-assistant/core/issues/52020
]

ServiceInfo = namedtuple("ServiceInfo", ["type", "data"])
CastInfo = namedtuple(
"CastInfo", ["services", "uuid", "model_name", "friendly_name", "host", "port"]
Expand Down Expand Up @@ -53,6 +59,20 @@ def update_cast(self, uuid, service):
"""


def _is_blocked_from_host_browser(item, block_list, item_type):
for blocked_prefix in block_list:
if item.startswith(blocked_prefix):
_LOGGER.debug("%s %s is blocked from host based polling", item_type, item)
return True
return False


def _is_model_blocked_from_host_browser(model):
return _is_blocked_from_host_browser(
model, HOST_BROWSER_BLOCKED_MODEL_PREFIXES, "Model"
)


class SimpleCastListener(AbstractCastListener):
"""Helper for backwards compatibility."""

Expand Down Expand Up @@ -151,8 +171,7 @@ def get_value(key):
host = addresses[0] if addresses else service.server

# Store the host, in case mDNS stops working
if self._host_browser:
self._host_browser.add_hosts([host])
self._host_browser.add_hosts([host])

model_name = get_value("md")
uuid = get_value("id")
Expand Down Expand Up @@ -200,9 +219,10 @@ class HostStatus:

def __init__(self):
self.failcount = 0
self.no_polling = False


HOSTLISTENER_CYCLE_TIME = 5
HOSTLISTENER_CYCLE_TIME = 30
HOSTLISTENER_MAX_FAIL = 5


Expand Down Expand Up @@ -240,7 +260,7 @@ def update_hosts(self, known_hosts):

for host in list(self._known_hosts.keys()):
if host not in known_hosts:
_LOGGER.debug("Removied host %s", host)
_LOGGER.debug("Removed host %s", host)
self._known_hosts.pop(host)

def run(self):
Expand All @@ -265,13 +285,18 @@ def _poll_hosts(self):
uuids = []
if self.stop.is_set():
break
device_status = get_device_status(host, timeout=30, context=self._context)
try:
hoststatus = self._known_hosts[host]
except KeyError:
# The host has been removed by another thread
continue

if hoststatus.no_polling:
# This host should not be polled
continue

device_status = get_device_status(host, timeout=30, context=self._context)

if not device_status:
hoststatus.failcount += 1
if hoststatus.failcount == HOSTLISTENER_MAX_FAIL:
Expand All @@ -281,6 +306,18 @@ def _poll_hosts(self):
)
continue

if (
device_status.cast_type != CAST_TYPE_AUDIO
or _is_model_blocked_from_host_browser(device_status.model_name)
):
# Polling causes frame drops on some Android TVs,
# https://github.com/home-assistant/core/issues/55435
# Keep polling audio chromecasts to detect new speaker groups, but
# exclude some devices which crash when polled
# Note: This will not work well the IP is recycled to another cast
# device.
hoststatus.no_polling = True

# We got device_status, try to get multizone status, then update devices
hoststatus.failcount = 0
devices.append(
Expand All @@ -293,7 +330,11 @@ def _poll_hosts(self):
)
uuids.append(device_status.uuid)

multizone_status = get_multizone_status(host, context=self._context)
multizone_status = (
get_multizone_status(host, context=self._context)
if device_status.multizone_supported
else None
)

if multizone_status:
for group in itertools.chain(
Expand Down

0 comments on commit 7b97be4

Please sign in to comment.