1
mirror of https://github.com/home-assistant/core synced 2024-09-28 03:04:04 +02:00

Home Assistant Cast (#26566)

* Add backend support for Home Assistant Cast

* Update test reqs
This commit is contained in:
Paulus Schoutsen 2019-09-11 12:34:10 -06:00 committed by GitHub
parent 6eeb01edc4
commit adaa200935
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 153 additions and 6 deletions

View File

@ -1,6 +1,7 @@
"""Component to embed Google Cast."""
from homeassistant import config_entries
from . import home_assistant_cast
from .const import DOMAIN
@ -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")
)

View File

@ -21,3 +21,6 @@ SIGNAL_CAST_DISCOVERED = "cast_discovered"
# 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"

View File

@ -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",
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}),
)

View File

@ -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": []

View File

@ -9,6 +9,7 @@ from pychromecast.socket_client import (
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
@ -52,6 +53,7 @@ from .const import (
CAST_MULTIZONE_MANAGER_KEY,
DEFAULT_PORT,
SIGNAL_CAST_REMOVED,
SIGNAL_HASS_CAST_SHOW_VIEW,
)
from .helpers import (
ChromecastInfo,
@ -225,9 +227,11 @@ class CastDevice(MediaPlayerDevice):
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."""
@ -256,6 +260,10 @@ class CastDevice(MediaPlayerDevice):
)
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()
@ -265,8 +273,13 @@ class CastDevice(MediaPlayerDevice):
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."""
@ -453,6 +466,7 @@ class CastDevice(MediaPlayerDevice):
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
@ -932,3 +946,16 @@ class CastDevice(MediaPlayerDevice):
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)

View File

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

View File

@ -1110,7 +1110,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

View File

@ -279,7 +279,7 @@ pyMetno==0.4.6
pyblackbird==0.5
# homeassistant.components.cast
pychromecast==3.2.2
pychromecast==4.0.0
# homeassistant.components.deconz
pydeconz==62

View File

@ -1030,3 +1030,18 @@ def async_capture_events(hass, event_name):
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

View File

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