Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The go2rtc component."""
2 
3 import logging
4 import shutil
5 
6 from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
7 from awesomeversion import AwesomeVersion
8 from go2rtc_client import Go2RtcRestClient
9 from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError
10 from go2rtc_client.ws import (
11  Go2RtcWsClient,
12  ReceiveMessages,
13  WebRTCAnswer,
14  WebRTCCandidate,
15  WebRTCOffer,
16  WsError,
17 )
18 import voluptuous as vol
19 from webrtc_models import RTCIceCandidateInit
20 
22  Camera,
23  CameraWebRTCProvider,
24  WebRTCAnswer as HAWebRTCAnswer,
25  WebRTCCandidate as HAWebRTCCandidate,
26  WebRTCError,
27  WebRTCMessage,
28  WebRTCSendMessage,
29  async_register_webrtc_provider,
30 )
31 from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN
32 from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
33 from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
34 from homeassistant.core import Event, HomeAssistant, callback
35 from homeassistant.exceptions import ConfigEntryNotReady
36 from homeassistant.helpers import (
37  config_validation as cv,
38  discovery_flow,
39  issue_registry as ir,
40 )
41 from homeassistant.helpers.aiohttp_client import async_get_clientsession
42 from homeassistant.helpers.typing import ConfigType
43 from homeassistant.util.hass_dict import HassKey
44 from homeassistant.util.package import is_docker_env
45 
46 from .const import (
47  CONF_DEBUG_UI,
48  DEBUG_UI_URL_MESSAGE,
49  DOMAIN,
50  HA_MANAGED_URL,
51  RECOMMENDED_VERSION,
52 )
53 from .server import Server
54 
55 _LOGGER = logging.getLogger(__name__)
56 
57 _SUPPORTED_STREAMS = frozenset(
58  (
59  "bubble",
60  "dvrip",
61  "expr",
62  "ffmpeg",
63  "gopro",
64  "homekit",
65  "http",
66  "https",
67  "httpx",
68  "isapi",
69  "ivideon",
70  "kasa",
71  "nest",
72  "onvif",
73  "roborock",
74  "rtmp",
75  "rtmps",
76  "rtmpx",
77  "rtsp",
78  "rtsps",
79  "rtspx",
80  "tapo",
81  "tcp",
82  "webrtc",
83  "webtorrent",
84  )
85 )
86 
87 CONFIG_SCHEMA = vol.Schema(
88  {
89  DOMAIN: vol.Schema(
90  {
91  vol.Exclusive(CONF_URL, DOMAIN, DEBUG_UI_URL_MESSAGE): cv.url,
92  vol.Exclusive(CONF_DEBUG_UI, DOMAIN, DEBUG_UI_URL_MESSAGE): cv.boolean,
93  }
94  )
95  },
96  extra=vol.ALLOW_EXTRA,
97 )
98 
99 _DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
100 _RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
101 
102 
103 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
104  """Set up WebRTC."""
105  url: str | None = None
106  if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config:
107  await _remove_go2rtc_entries(hass)
108  return True
109 
110  if not (configured_by_user := DOMAIN in config) or not (
111  url := config[DOMAIN].get(CONF_URL)
112  ):
113  if not is_docker_env():
114  if not configured_by_user:
115  # Remove config entry if it exists
116  await _remove_go2rtc_entries(hass)
117  return True
118  _LOGGER.warning("Go2rtc URL required in non-docker installs")
119  return False
120  if not (binary := await _get_binary(hass)):
121  _LOGGER.error("Could not find go2rtc docker binary")
122  return False
123 
124  # HA will manage the binary
125  server = Server(
126  hass, binary, enable_ui=config.get(DOMAIN, {}).get(CONF_DEBUG_UI, False)
127  )
128  try:
129  await server.start()
130  except Exception: # noqa: BLE001
131  _LOGGER.warning("Could not start go2rtc server", exc_info=True)
132  return False
133 
134  async def on_stop(event: Event) -> None:
135  await server.stop()
136 
137  hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
138 
139  url = HA_MANAGED_URL
140 
141  hass.data[_DATA_GO2RTC] = url
142  discovery_flow.async_create_flow(
143  hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
144  )
145  return True
146 
147 
148 async def _remove_go2rtc_entries(hass: HomeAssistant) -> None:
149  """Remove go2rtc config entries, if any."""
150  for entry in hass.config_entries.async_entries(DOMAIN):
151  await hass.config_entries.async_remove(entry.entry_id)
152 
153 
154 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
155  """Set up go2rtc from a config entry."""
156  url = hass.data[_DATA_GO2RTC]
157 
158  # Validate the server URL
159  try:
160  client = Go2RtcRestClient(async_get_clientsession(hass), url)
161  version = await client.validate_server_version()
162  if version < AwesomeVersion(RECOMMENDED_VERSION):
163  ir.async_create_issue(
164  hass,
165  DOMAIN,
166  "recommended_version",
167  is_fixable=False,
168  is_persistent=False,
169  severity=ir.IssueSeverity.WARNING,
170  translation_key="recommended_version",
171  translation_placeholders={
172  "recommended_version": RECOMMENDED_VERSION,
173  "current_version": str(version),
174  },
175  )
176  except Go2RtcClientError as err:
177  if isinstance(err.__cause__, _RETRYABLE_ERRORS):
178  raise ConfigEntryNotReady(
179  f"Could not connect to go2rtc instance on {url}"
180  ) from err
181  _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
182  return False
183  except Go2RtcVersionError as err:
184  raise ConfigEntryNotReady(
185  f"The go2rtc server version is not supported, {err}"
186  ) from err
187  except Exception as err: # noqa: BLE001
188  _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
189  return False
190 
191  provider = WebRTCProvider(hass, url)
192  async_register_webrtc_provider(hass, provider)
193  return True
194 
195 
196 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
197  """Unload a go2rtc config entry."""
198  return True
199 
200 
201 async def _get_binary(hass: HomeAssistant) -> str | None:
202  """Return the binary path if found."""
203  return await hass.async_add_executor_job(shutil.which, "go2rtc")
204 
205 
207  """WebRTC provider."""
208 
209  def __init__(self, hass: HomeAssistant, url: str) -> None:
210  """Initialize the WebRTC provider."""
211  self._hass_hass = hass
212  self._url_url = url
213  self._session_session = async_get_clientsession(hass)
214  self._rest_client_rest_client = Go2RtcRestClient(self._session_session, url)
215  self._sessions: dict[str, Go2RtcWsClient] = {}
216 
217  @property
218  def domain(self) -> str:
219  """Return the integration domain of the provider."""
220  return DOMAIN
221 
222  @callback
223  def async_is_supported(self, stream_source: str) -> bool:
224  """Return if this provider is supports the Camera as source."""
225  return stream_source.partition(":")[0] in _SUPPORTED_STREAMS
226 
228  self,
229  camera: Camera,
230  offer_sdp: str,
231  session_id: str,
232  send_message: WebRTCSendMessage,
233  ) -> None:
234  """Handle the WebRTC offer and return the answer via the provided callback."""
235  self._sessions[session_id] = ws_client = Go2RtcWsClient(
236  self._session_session, self._url_url, source=camera.entity_id
237  )
238 
239  if not (stream_source := await camera.stream_source()):
240  send_message(
241  WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source")
242  )
243  return
244 
245  streams = await self._rest_client_rest_client.streams.list()
246 
247  if (stream := streams.get(camera.entity_id)) is None or not any(
248  stream_source == producer.url for producer in stream.producers
249  ):
250  await self._rest_client_rest_client.streams.add(
251  camera.entity_id,
252  [
253  stream_source,
254  # We are setting any ffmpeg rtsp related logs to debug
255  # Connection problems to the camera will be logged by the first stream
256  # Therefore setting it to debug will not hide any important logs
257  f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
258  ],
259  )
260 
261  @callback
262  def on_messages(message: ReceiveMessages) -> None:
263  """Handle messages."""
264  value: WebRTCMessage
265  match message:
266  case WebRTCCandidate():
267  value = HAWebRTCCandidate(RTCIceCandidateInit(message.candidate))
268  case WebRTCAnswer():
269  value = HAWebRTCAnswer(message.sdp)
270  case WsError():
271  value = WebRTCError("go2rtc_webrtc_offer_failed", message.error)
272 
273  send_message(value)
274 
275  ws_client.subscribe(on_messages)
276  config = camera.async_get_webrtc_client_configuration()
277  await ws_client.send(WebRTCOffer(offer_sdp, config.configuration.ice_servers))
278 
280  self, session_id: str, candidate: RTCIceCandidateInit
281  ) -> None:
282  """Handle the WebRTC candidate."""
283 
284  if ws_client := self._sessions.get(session_id):
285  await ws_client.send(WebRTCCandidate(candidate.candidate))
286  else:
287  _LOGGER.debug("Unknown session %s. Ignoring candidate", session_id)
288 
289  @callback
290  def async_close_session(self, session_id: str) -> None:
291  """Close the session."""
292  ws_client = self._sessions.pop(session_id)
293  self._hass_hass.async_create_task(ws_client.close())
None __init__(self, HomeAssistant hass, str url)
Definition: __init__.py:209
None async_on_webrtc_candidate(self, str session_id, RTCIceCandidateInit candidate)
Definition: __init__.py:281
None async_close_session(self, str session_id)
Definition: __init__.py:290
bool async_is_supported(self, str stream_source)
Definition: __init__.py:223
None async_handle_async_webrtc_offer(self, Camera camera, str offer_sdp, str session_id, WebRTCSendMessage send_message)
Definition: __init__.py:233
Callable[[], None] async_register_webrtc_provider(HomeAssistant hass, CameraWebRTCProvider provider)
Definition: webrtc.py:182
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
str|None _get_binary(HomeAssistant hass)
Definition: __init__.py:201
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:196
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:154
None _remove_go2rtc_entries(HomeAssistant hass)
Definition: __init__.py:148
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:103
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)