mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +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."""
|
"""Component to embed Google Cast."""
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
|
||||||
|
from . import home_assistant_cast
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
@ -20,8 +21,10 @@ async def async_setup(hass, config):
|
|||||||
return True
|
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."""
|
"""Set up Cast from a config entry."""
|
||||||
|
await home_assistant_cast.async_setup_ha_cast(hass, entry)
|
||||||
|
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
hass.config_entries.async_forward_entry_setup(entry, "media_player")
|
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
|
# Dispatcher signal fired with a ChromecastInfo every time a Chromecast is
|
||||||
# removed
|
# removed
|
||||||
SIGNAL_CAST_REMOVED = "cast_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",
|
"name": "Cast",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/components/cast",
|
"documentation": "https://www.home-assistant.io/components/cast",
|
||||||
"requirements": [
|
"requirements": ["pychromecast==4.0.0"],
|
||||||
"pychromecast==3.2.2"
|
|
||||||
],
|
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"zeroconf": ["_googlecast._tcp.local."],
|
"zeroconf": ["_googlecast._tcp.local."],
|
||||||
"codeowners": []
|
"codeowners": []
|
||||||
|
@ -9,6 +9,7 @@ from pychromecast.socket_client import (
|
|||||||
CONNECTION_STATUS_DISCONNECTED,
|
CONNECTION_STATUS_DISCONNECTED,
|
||||||
)
|
)
|
||||||
from pychromecast.controllers.multizone import MultizoneManager
|
from pychromecast.controllers.multizone import MultizoneManager
|
||||||
|
from pychromecast.controllers.homeassistant import HomeAssistantController
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
|
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
|
||||||
@ -52,6 +53,7 @@ from .const import (
|
|||||||
CAST_MULTIZONE_MANAGER_KEY,
|
CAST_MULTIZONE_MANAGER_KEY,
|
||||||
DEFAULT_PORT,
|
DEFAULT_PORT,
|
||||||
SIGNAL_CAST_REMOVED,
|
SIGNAL_CAST_REMOVED,
|
||||||
|
SIGNAL_HASS_CAST_SHOW_VIEW,
|
||||||
)
|
)
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
ChromecastInfo,
|
ChromecastInfo,
|
||||||
@ -225,9 +227,11 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
self._dynamic_group_status_listener: Optional[
|
self._dynamic_group_status_listener: Optional[
|
||||||
DynamicGroupCastStatusListener
|
DynamicGroupCastStatusListener
|
||||||
] = None
|
] = None
|
||||||
|
self._hass_cast_controller: Optional[HomeAssistantController] = None
|
||||||
|
|
||||||
self._add_remove_handler = None
|
self._add_remove_handler = None
|
||||||
self._del_remove_handler = None
|
self._del_remove_handler = None
|
||||||
|
self._cast_view_remove_handler = None
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Create chromecast object when added to hass."""
|
"""Create chromecast object when added to hass."""
|
||||||
@ -256,6 +260,10 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
)
|
)
|
||||||
break
|
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:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""Disconnect Chromecast object when removed."""
|
"""Disconnect Chromecast object when removed."""
|
||||||
await self._async_disconnect()
|
await self._async_disconnect()
|
||||||
@ -265,8 +273,13 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid)
|
self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid)
|
||||||
if self._add_remove_handler:
|
if self._add_remove_handler:
|
||||||
self._add_remove_handler()
|
self._add_remove_handler()
|
||||||
|
self._add_remove_handler = None
|
||||||
if self._del_remove_handler:
|
if self._del_remove_handler:
|
||||||
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):
|
async def async_set_cast_info(self, cast_info):
|
||||||
"""Set the cast information and set up the chromecast object."""
|
"""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 = {}
|
||||||
self.mz_media_status_received = {}
|
self.mz_media_status_received = {}
|
||||||
self.mz_mgr = None
|
self.mz_mgr = None
|
||||||
|
self._hass_cast_controller = None
|
||||||
if self._status_listener is not None:
|
if self._status_listener is not None:
|
||||||
self._status_listener.invalidate()
|
self._status_listener.invalidate()
|
||||||
self._status_listener = None
|
self._status_listener = None
|
||||||
@ -932,3 +946,16 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
async def _async_stop(self, event):
|
async def _async_stop(self, event):
|
||||||
"""Disconnect socket on Home Assistant stop."""
|
"""Disconnect socket on Home Assistant stop."""
|
||||||
await self._async_disconnect()
|
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
|
pychannels==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.cast
|
# homeassistant.components.cast
|
||||||
pychromecast==3.2.2
|
pychromecast==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.cmus
|
# homeassistant.components.cmus
|
||||||
pycmus==0.1.1
|
pycmus==0.1.1
|
||||||
|
@ -279,7 +279,7 @@ pyMetno==0.4.6
|
|||||||
pyblackbird==0.5
|
pyblackbird==0.5
|
||||||
|
|
||||||
# homeassistant.components.cast
|
# homeassistant.components.cast
|
||||||
pychromecast==3.2.2
|
pychromecast==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==62
|
pydeconz==62
|
||||||
|
@ -1030,3 +1030,18 @@ def async_capture_events(hass, event_name):
|
|||||||
hass.bus.async_listen(event_name, capture_events)
|
hass.bus.async_listen(event_name, capture_events)
|
||||||
|
|
||||||
return 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