Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions homeassistant/components/cast/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""Component to embed Google Cast."""
from homeassistant import config_entries
from homeassistant.helpers import config_validation as cv

from . import home_assistant_cast
from .const import DOMAIN

# Deprecated from 2021.4, remove in 2021.6
CONFIG_SCHEMA = cv.deprecated(DOMAIN)

@balloob balloob Mar 22, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, we had no config schema to begin with and just passed that to import? 🤔



async def async_setup(hass, config):
"""Set up the Cast component."""
Expand Down
95 changes: 73 additions & 22 deletions homeassistant/components/cast/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
from homeassistant import config_entries
from homeassistant.helpers import config_validation as cv

from .const import CONF_KNOWN_HOSTS, DOMAIN
from .const import CONF_IGNORE_CEC, CONF_KNOWN_HOSTS, CONF_UUID, DOMAIN
from .media_player import ENTITY_SCHEMA

IGNORE_CEC_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
KNOWN_HOSTS_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))
WANTED_UUID_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string]))


class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
Expand All @@ -17,7 +20,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):

def __init__(self):
"""Initialize flow."""
self._known_hosts = None
self._ignore_cec = []
self._known_hosts = []
self._wanted_uuid = []

@staticmethod
def async_get_options_flow(config_entry):
Expand All @@ -28,7 +33,22 @@ async def async_step_import(self, import_data=None):
"""Import data."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
data = {CONF_KNOWN_HOSTS: self._known_hosts}

media_player_config = self.hass.data[DOMAIN].get("media_player") or {}
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
if not isinstance(media_player_config, list):
media_player_config = [media_player_config]
for cfg in media_player_config:
try:
cfg = ENTITY_SCHEMA(cfg)
if CONF_IGNORE_CEC in cfg:
self._ignore_cec.extend(cfg[CONF_IGNORE_CEC])
if CONF_UUID in cfg:
self._wanted_uuid.append(cfg[CONF_UUID])
except vol.Error:
# Invalid config, ignore it
pass
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated

data = self._get_data()
return self.async_create_entry(title="Google Cast", data=data)

async def async_step_user(self, user_input=None):
Expand Down Expand Up @@ -62,7 +82,8 @@ async def async_step_config(self, user_input=None):
errors["base"] = "invalid_known_hosts"
bad_hosts = True
else:
data[CONF_KNOWN_HOSTS] = known_hosts
self._known_hosts = known_hosts
data = self._get_data()
if not bad_hosts:
return self.async_create_entry(title="Google Cast", data=data)

Expand All @@ -76,13 +97,20 @@ async def async_step_config(self, user_input=None):
async def async_step_confirm(self, user_input=None):
"""Confirm the setup."""

data = {CONF_KNOWN_HOSTS: self._known_hosts}
data = self._get_data()

if user_input is not None:
return self.async_create_entry(title="Google Cast", data=data)

return self.async_show_form(step_id="confirm")

def _get_data(self):
return {
CONF_IGNORE_CEC: self._ignore_cec,
CONF_KNOWN_HOSTS: self._known_hosts,
CONF_UUID: self._wanted_uuid,
}


class CastOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Google Cast options."""
Expand All @@ -102,35 +130,58 @@ async def async_step_options(self, user_input=None):
errors = {}
current_config = self.config_entry.data
if user_input is not None:
bad_hosts = False
bad_cec, ignore_cec = _string_to_list(
user_input.get(CONF_IGNORE_CEC, ""), IGNORE_CEC_SCHEMA
)
bad_hosts, known_hosts = _string_to_list(
user_input.get(CONF_KNOWN_HOSTS, ""), KNOWN_HOSTS_SCHEMA
)
bad_uuid, wanted_uuid = _string_to_list(
user_input.get(CONF_UUID, ""), WANTED_UUID_SCHEMA
)

known_hosts = user_input.get(CONF_KNOWN_HOSTS, "")
known_hosts = [x.strip() for x in known_hosts.split(",") if x.strip()]
try:
known_hosts = KNOWN_HOSTS_SCHEMA(known_hosts)
except vol.Invalid:
errors["base"] = "invalid_known_hosts"
bad_hosts = True
if not bad_hosts:
if not bad_cec and not bad_hosts and not bad_uuid:
updated_config = {}
updated_config[CONF_IGNORE_CEC] = ignore_cec
updated_config[CONF_KNOWN_HOSTS] = known_hosts
updated_config[CONF_UUID] = wanted_uuid
self.hass.config_entries.async_update_entry(
self.config_entry, data=updated_config
)
return self.async_create_entry(title="", data=None)

fields = {}
known_hosts_string = ""
if current_config.get(CONF_KNOWN_HOSTS):
known_hosts_string = ",".join(current_config.get(CONF_KNOWN_HOSTS))
fields[
vol.Optional(
"known_hosts", description={"suggested_value": known_hosts_string}
)
] = str
suggested_value = _list_to_string(current_config.get(CONF_KNOWN_HOSTS))
_add_with_suggestion(fields, CONF_KNOWN_HOSTS, suggested_value)
suggested_value = _list_to_string(current_config.get(CONF_UUID))
_add_with_suggestion(fields, CONF_UUID, suggested_value)
suggested_value = _list_to_string(current_config.get(CONF_IGNORE_CEC))
_add_with_suggestion(fields, CONF_IGNORE_CEC, suggested_value)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We know if this flow is requested by an advanced user. Let's hide UUID and IGNORE CEC unless users are advanced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4487596


return self.async_show_form(
step_id="options",
data_schema=vol.Schema(fields),
errors=errors,
)


def _list_to_string(items):
comma_separated_string = ""
if items:
comma_separated_string = ",".join(items)
return comma_separated_string


def _string_to_list(string, schema):
invalid = False
items = [x.strip() for x in string.split(",") if x.strip()]
try:
items = schema(items)
except vol.Invalid:
invalid = True

return invalid, items


def _add_with_suggestion(fields, key, suggested_value):
fields[vol.Optional(key, description={"suggested_value": suggested_value})] = str
5 changes: 2 additions & 3 deletions homeassistant/components/cast/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@

# Stores a threading.Lock that is held by the internal pychromecast discovery.
INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running"
# Stores all ChromecastInfo we encountered through discovery or config as a set
# If we find a chromecast with a new host, the old one will be removed again.
KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts"
# Stores UUIDs of cast devices that were added as entities. Doesn't store
# None UUIDs.
ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices"
Expand All @@ -27,4 +24,6 @@
# Dispatcher signal fired when a Chromecast should show a Home Assistant Cast view.
SIGNAL_HASS_CAST_SHOW_VIEW = "cast_show_view"

CONF_IGNORE_CEC = "ignore_cec"
CONF_KNOWN_HOSTS = "known_hosts"
CONF_UUID = "uuid"
7 changes: 1 addition & 6 deletions homeassistant/components/cast/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
CONF_KNOWN_HOSTS,
DEFAULT_PORT,
INTERNAL_DISCOVERY_RUNNING_KEY,
KNOWN_CHROMECAST_INFO_KEY,
SIGNAL_CAST_DISCOVERED,
SIGNAL_CAST_REMOVED,
)
Expand All @@ -38,12 +37,8 @@ def discover_chromecast(hass: HomeAssistant, device_info):
return

info = info.fill_out_missing_chromecast_info()
if info.uuid in hass.data[KNOWN_CHROMECAST_INFO_KEY]:
_LOGGER.debug("Discovered update for known chromecast %s", info)
else:
_LOGGER.debug("Discovered chromecast %s", info)
_LOGGER.debug("Discovered new or updated chromecast %s", info)

hass.data[KNOWN_CHROMECAST_INFO_KEY][info.uuid] = info
dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info)


Expand Down
55 changes: 11 additions & 44 deletions homeassistant/components/cast/media_player.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Provide functionality to interact with Cast devices on the network."""
from __future__ import annotations

import asyncio
from datetime import timedelta
import functools as ft
import json
Expand Down Expand Up @@ -51,19 +50,19 @@
STATE_PLAYING,
)
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.dt as dt_util
from homeassistant.util.logging import async_create_catching_coro

from .const import (
ADDED_CAST_DEVICES_KEY,
CAST_MULTIZONE_MANAGER_KEY,
CONF_IGNORE_CEC,
CONF_UUID,
DOMAIN as CAST_DOMAIN,
KNOWN_CHROMECAST_INFO_KEY,
SIGNAL_CAST_DISCOVERED,
SIGNAL_CAST_REMOVED,
SIGNAL_HASS_CAST_SHOW_VIEW,
Expand All @@ -73,8 +72,6 @@

_LOGGER = logging.getLogger(__name__)

CONF_IGNORE_CEC = "ignore_cec"
CONF_UUID = "uuid"
CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png"

SUPPORT_CAST = (
Expand Down Expand Up @@ -128,57 +125,27 @@ def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo):

async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Cast from a config entry."""
config = hass.data[CAST_DOMAIN].get("media_player") or {}
if not isinstance(config, list):
config = [config]

# no pending task
done, _ = await asyncio.wait(
[
_async_setup_platform(
hass, ENTITY_SCHEMA(cfg), async_add_entities, config_entry
)
for cfg in config
]
)
if any(task.exception() for task in done):
exceptions = [task.exception() for task in done]
for exception in exceptions:
_LOGGER.debug("Failed to setup chromecast", exc_info=exception)
raise PlatformNotReady


async def _async_setup_platform(
hass: HomeAssistantType, config: ConfigType, async_add_entities, config_entry
):
"""Set up the cast platform."""
# Import CEC IGNORE attributes
pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, [])
hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set())
hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, {})

wanted_uuid = None
if CONF_UUID in config:
wanted_uuid = config[CONF_UUID]
# Import CEC IGNORE attributes
pychromecast.IGNORE_CEC += config_entry.data.get(CONF_IGNORE_CEC) or []

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit weird that we're modifying a constant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but that's how the lib is designed. It could be refactored to a more sane API, with a setter function, but not in this PR.


wanted_uuids = config_entry.data.get(CONF_UUID) or None

@callback
def async_cast_discovered(discover: ChromecastInfo) -> None:
"""Handle discovery of a new chromecast."""
# If wanted_uuid is set, we're handling a specific cast device identified by UUID
if wanted_uuid is not None and wanted_uuid != discover.uuid:
# UUID not matching, this is not it.
# If wanted_uuids is set, we're only accepting specific cast devices identified
# by UUID
if wanted_uuids is not None and discover.uuid not in wanted_uuids:
# UUID not matching, ignore.
return

cast_device = _async_create_cast_device(hass, discover)
if cast_device is not None:
async_add_entities([cast_device])

async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered)
# Re-play the callback for all past chromecasts, store the objects in
# a list to avoid concurrent modification resulting in exception.
for chromecast in hass.data[KNOWN_CHROMECAST_INFO_KEY].values():
async_cast_discovered(chromecast)

ChromeCastZeroconf.set_zeroconf(await zeroconf.async_get_instance(hass))
hass.async_add_executor_job(setup_internal_discovery, hass, config_entry)

Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/cast/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"options": {
"description": "Please enter the Google Cast configuration.",
"data": {
"known_hosts": "Optional list of known hosts if mDNS discovery is not working."
"ignore_cec": "Optional list which will be passed to pychromecast.IGNORE_CEC.",
"known_hosts": "Optional list of known hosts if mDNS discovery is not working.",
"uuid": "Optional list of UUIDs. Casts not listed will not be added."
}
}
},
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/cast/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"step": {
"options": {
"data": {
"known_hosts": "Optional list of known hosts if mDNS discovery is not working."
"ignore_cec": "Optional list which will be passed to pychromecast.IGNORE_CEC.",
"known_hosts": "Optional list of known hosts if mDNS discovery is not working.",
"uuid": "Optional list of UUIDs. Casts not listed will not be added."
},
"description": "Please enter the Google Cast configuration."
}
Expand Down
1 change: 1 addition & 0 deletions tests/components/cast/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def mz_mock():
def pycast_mock(castbrowser_mock, castbrowser_constructor_mock):
"""Mock pychromecast."""
pycast_mock = MagicMock()
pycast_mock.IGNORE_CEC = []
pycast_mock.discovery.CastBrowser = castbrowser_constructor_mock
pycast_mock.discovery.CastBrowser.return_value = castbrowser_mock
pycast_mock.discovery.AbstractCastListener = (
Expand Down
6 changes: 1 addition & 5 deletions tests/components/cast/test_home_assistant_cast.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,8 @@ async def test_remove_entry(hass, mock_zeroconf):
entry.add_to_hass(hass)

with patch(
"homeassistant.components.cast.media_player._async_setup_platform"
), patch(
"pychromecast.discovery.discover_chromecasts", return_value=(True, None)
), patch(
"pychromecast.discovery.stop_discovery"
):
), patch("pychromecast.discovery.stop_discovery"):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert "cast" in hass.config.components
Expand Down
Loading