Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion homeassistant/components/cast/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Component to embed Google Cast."""
from homeassistant import config_entries

from . import home_assistant_cast
from .const import DOMAIN


Expand All @@ -20,8 +21,10 @@ async def async_setup(hass, config):
return True


async def async_setup_entry(hass, entry):
async def async_setup_entry(hass, entry: config_entries.ConfigEntry):
"""Set up Cast from a config entry."""
await home_assistant_cast.async_setup_ha_cast(hass, entry)

hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "media_player")
)
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/cast/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@
# Dispatcher signal fired with a ChromecastInfo every time a Chromecast is
# removed
SIGNAL_CAST_REMOVED = "cast_removed"

# Dispatcher signal fired when a Chromecast should show a Home Assistant Cast view.
SIGNAL_HASS_CAST_SHOW_VIEW = "cast_show_view"
64 changes: 64 additions & 0 deletions homeassistant/components/cast/home_assistant_cast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Home Assistant Cast integration for Cast."""
from typing import Optional

import voluptuous as vol

from pychromecast.controllers.homeassistant import HomeAssistantController

from homeassistant import auth, config_entries, core
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.helpers import config_validation as cv, dispatcher

from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW

SERVICE_SHOW_VIEW = "show_lovelace_view"
ATTR_VIEW_PATH = "view_path"


async def async_setup_ha_cast(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Set up Home Assistant Cast."""
user_id: Optional[str] = entry.data.get("user_id")
user: Optional[auth.models.User] = None

if user_id is not None:
user = await hass.auth.async_get_user(user_id)

if user is None:
user = await hass.auth.async_create_system_user(
"Home Assistant Cast", [auth.GROUP_ID_ADMIN]
)
hass.config_entries.async_update_entry(
entry, data={**entry.data, "user_id": user.id}
)

if user.refresh_tokens:
refresh_token: auth.models.RefreshToken = list(user.refresh_tokens.values())[0]
else:
refresh_token = await hass.auth.async_create_refresh_token(user)

async def handle_show_view(call: core.ServiceCall):
"""Handle a Show View service call."""
controller = HomeAssistantController(
# If you are developing Home Assistant Cast, uncomment and set to your dev app id.
# app_id="5FE44367",
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.

Is this a random app_id?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

hass_url=hass.config.api.base_url,
client_id=None,
refresh_token=refresh_token.token,
)

dispatcher.async_dispatcher_send(
hass,
SIGNAL_HASS_CAST_SHOW_VIEW,
controller,
call.data[ATTR_ENTITY_ID],
call.data[ATTR_VIEW_PATH],
)

hass.helpers.service.async_register_admin_service(
DOMAIN,
SERVICE_SHOW_VIEW,
handle_show_view,
vol.Schema({ATTR_ENTITY_ID: cv.entity_id, ATTR_VIEW_PATH: str}),
)
4 changes: 1 addition & 3 deletions homeassistant/components/cast/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
"name": "Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/cast",
"requirements": [
"pychromecast==3.2.2"
],
"requirements": ["pychromecast==4.0.0"],
"dependencies": [],
"zeroconf": ["_googlecast._tcp.local."],
"codeowners": []
Expand Down
27 changes: 27 additions & 0 deletions homeassistant/components/cast/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
CONNECTION_STATUS_DISCONNECTED,
)
from pychromecast.controllers.multizone import MultizoneManager
from pychromecast.controllers.homeassistant import HomeAssistantController
import voluptuous as vol

from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
Expand Down Expand Up @@ -52,6 +53,7 @@
CAST_MULTIZONE_MANAGER_KEY,
DEFAULT_PORT,
SIGNAL_CAST_REMOVED,
SIGNAL_HASS_CAST_SHOW_VIEW,
)
from .helpers import (
ChromecastInfo,
Expand Down Expand Up @@ -225,9 +227,11 @@ def __init__(self, cast_info: ChromecastInfo):
self._dynamic_group_status_listener: Optional[
DynamicGroupCastStatusListener
] = None
self._hass_cast_controller: Optional[HomeAssistantController] = None

self._add_remove_handler = None
self._del_remove_handler = None
self._cast_view_remove_handler = None

async def async_added_to_hass(self):
"""Create chromecast object when added to hass."""
Expand Down Expand Up @@ -256,6 +260,10 @@ async def async_added_to_hass(self):
)
break

self._cast_view_remove_handler = async_dispatcher_connect(
self.hass, SIGNAL_HASS_CAST_SHOW_VIEW, self._handle_signal_show_view
)

async def async_will_remove_from_hass(self) -> None:
"""Disconnect Chromecast object when removed."""
await self._async_disconnect()
Expand All @@ -265,8 +273,13 @@ async def async_will_remove_from_hass(self) -> None:
self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid)
if self._add_remove_handler:
self._add_remove_handler()
self._add_remove_handler = None
if self._del_remove_handler:
self._del_remove_handler()
self._del_remove_handler = None
if self._cast_view_remove_handler:
self._cast_view_remove_handler()
self._cast_view_remove_handler = None

async def async_set_cast_info(self, cast_info):
"""Set the cast information and set up the chromecast object."""
Expand Down Expand Up @@ -453,6 +466,7 @@ def _invalidate(self):
self.mz_media_status = {}
self.mz_media_status_received = {}
self.mz_mgr = None
self._hass_cast_controller = None
if self._status_listener is not None:
self._status_listener.invalidate()
self._status_listener = None
Expand Down Expand Up @@ -932,3 +946,16 @@ async def _async_cast_removed(self, discover: ChromecastInfo):
async def _async_stop(self, event):
"""Disconnect socket on Home Assistant stop."""
await self._async_disconnect()

def _handle_signal_show_view(
self, controller: HomeAssistantController, entity_id: str, view_path: str
):
"""Handle a show view signal."""
if entity_id != self.entity_id:
return

if self._hass_cast_controller is None:
self._hass_cast_controller = controller
self._chromecast.register_handler(controller)

self._hass_cast_controller.show_lovelace_view(view_path)
9 changes: 9 additions & 0 deletions homeassistant/components/cast/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
show_lovelace_view:
description: Show a Lovelace view on a Chromecast.
fields:
entity_id:
description: Media Player entity to show the Lovelace view on.
example: "media_player.kitchen"
view_path:
description: The path of the Lovelace view to show.
example: downstairs
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@ pycfdns==0.0.1
pychannels==1.0.0

# homeassistant.components.cast
pychromecast==3.2.2
pychromecast==4.0.0

# homeassistant.components.cmus
pycmus==0.1.1
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ pyMetno==0.4.6
pyblackbird==0.5

# homeassistant.components.cast
pychromecast==3.2.2
pychromecast==4.0.0

# homeassistant.components.deconz
pydeconz==62
Expand Down
15 changes: 15 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1030,3 +1030,18 @@ def capture_events(event):
hass.bus.async_listen(event_name, capture_events)

return events


@ha.callback
def async_mock_signal(hass, signal):
"""Catch all dispatches to a signal."""
calls = []

@ha.callback
def mock_signal_handler(*args):
"""Mock service call."""
calls.append(args)

hass.helpers.dispatcher.async_dispatcher_connect(signal, mock_signal_handler)

return calls
28 changes: 28 additions & 0 deletions tests/components/cast/test_home_assistant_cast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Test Home Assistant Cast."""
from unittest.mock import Mock
from homeassistant.components.cast import home_assistant_cast

from tests.common import MockConfigEntry, async_mock_signal


async def test_service_show_view(hass):
"""Test we don't set app id in prod."""
hass.config.api = Mock(base_url="http://example.com")
await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry())
calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW)

await hass.services.async_call(
"cast",
"show_lovelace_view",
{"entity_id": "media_player.kitchen", "view_path": "mock_path"},
blocking=True,
)

assert len(calls) == 1
controller, entity_id, view_path = calls[0]
assert controller.hass_url == "http://example.com"
assert controller.client_id is None
# Verify user did not accidentally submit their dev app id
assert controller.supporting_app_id == "B12CE3CA"
assert entity_id == "media_player.kitchen"
assert view_path == "mock_path"