mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Home Assistant Cast (#26566)
* Add backend support for Home Assistant Cast * Update test reqs
This commit is contained in:
parent
6eeb01edc4
commit
adaa200935
@ -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")
|
||||
)
|
||||
|
@ -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"
|
||||
|
64
homeassistant/components/cast/home_assistant_cast.py
Normal file
64
homeassistant/components/cast/home_assistant_cast.py
Normal 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}),
|
||||
)
|
@ -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": []
|
||||
|
@ -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)
|
||||
|
9
homeassistant/components/cast/services.yaml
Normal file
9
homeassistant/components/cast/services.yaml
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
28
tests/components/cast/test_home_assistant_cast.py
Normal file
28
tests/components/cast/test_home_assistant_cast.py
Normal 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"
|
Loading…
x
Reference in New Issue
Block a user