From 6ae7b928ea96c23a3f5a3d5f3f563ef381255a57 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 19 Dec 2021 19:42:37 -0800 Subject: [PATCH] Add a camera specific logger to help diagnose stream errors (#61647) * Add a camera specific logger to help diagnose stream errors Add a camera specific logger to help users associate stream errors with a particular camera. Issue #54659 * Apply code review feedback * Update package name based on manual testing --- homeassistant/components/camera/__init__.py | 7 +++- homeassistant/components/stream/__init__.py | 42 +++++++++++++++------ tests/components/stream/test_worker.py | 6 +-- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 5d170eece4c..286c7957169 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -462,7 +462,12 @@ class Camera(Entity): source = await self.stream_source() if not source: return None - self.stream = create_stream(self.hass, source, options=self.stream_options) + self.stream = create_stream( + self.hass, + source, + options=self.stream_options, + stream_label=self.entity_id, + ) self.stream.set_update_callback(self.async_write_ha_state) return self.stream diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 2d2e4058460..0556bc2c7a9 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -69,12 +69,17 @@ def redact_credentials(data: str) -> str: def create_stream( - hass: HomeAssistant, stream_source: str, options: dict[str, str] + hass: HomeAssistant, + stream_source: str, + options: dict[str, str], + stream_label: str | None = None, ) -> Stream: """Create a stream with the specified identfier based on the source url. - The stream_source is typically an rtsp url and options are passed into - pyav / ffmpeg as options. + The stream_source is typically an rtsp url (though any url accepted by ffmpeg is fine) and + options are passed into pyav / ffmpeg as options. + + The stream_label is a string used as an additional message in logging. """ if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") @@ -87,7 +92,7 @@ def create_stream( **options, } - stream = Stream(hass, stream_source, options=options) + stream = Stream(hass, stream_source, options=options, stream_label=stream_label) hass.data[DOMAIN][ATTR_STREAMS].append(stream) return stream @@ -192,12 +197,17 @@ class Stream: """Represents a single stream.""" def __init__( - self, hass: HomeAssistant, source: str, options: dict[str, str] + self, + hass: HomeAssistant, + source: str, + options: dict[str, str], + stream_label: str | None = None, ) -> None: """Initialize a stream.""" self.hass = hass self.source = source self.options = options + self._stream_label = stream_label self.keepalive = False self.access_token: str | None = None self._thread: threading.Thread | None = None @@ -206,6 +216,11 @@ class Stream: self._fast_restart_once = False self._available: bool = True self._update_callback: Callable[[], None] | None = None + self._logger = ( + logging.getLogger(f"{__package__}.stream.{stream_label}") + if stream_label + else _LOGGER + ) def endpoint_url(self, fmt: str) -> str: """Start the stream and returns a url for the output format.""" @@ -285,11 +300,13 @@ class Stream: target=self._run_worker, ) self._thread.start() - _LOGGER.info("Started stream: %s", redact_credentials(str(self.source))) + self._logger.info( + "Started stream: %s", redact_credentials(str(self.source)) + ) def update_source(self, new_source: str) -> None: """Restart the stream with a new stream source.""" - _LOGGER.debug("Updating stream source %s", new_source) + self._logger.debug("Updating stream source %s", new_source) self.source = new_source self._fast_restart_once = True self._thread_quit.set() @@ -313,7 +330,8 @@ class Stream: self._thread_quit, ) except StreamWorkerError as err: - _LOGGER.error("Error from stream worker: %s", str(err)) + self._logger.error("Error from stream worker: %s", str(err)) + self._available = False stream_state.discontinuity() if not self.keepalive or self._thread_quit.is_set(): @@ -332,7 +350,7 @@ class Stream: if time.time() - start_time > STREAM_RESTART_RESET_TIME: wait_timeout = 0 wait_timeout += STREAM_RESTART_INCREMENT - _LOGGER.debug( + self._logger.debug( "Restarting stream worker in %d seconds: %s", wait_timeout, self.source, @@ -363,7 +381,9 @@ class Stream: self._thread_quit.set() self._thread.join() self._thread = None - _LOGGER.info("Stopped stream: %s", redact_credentials(str(self.source))) + self._logger.info( + "Stopped stream: %s", redact_credentials(str(self.source)) + ) async def async_record( self, video_path: str, duration: int = 30, lookback: int = 5 @@ -390,7 +410,7 @@ class Stream: recorder.video_path = video_path self.start() - _LOGGER.debug("Started a stream recording of %s seconds", duration) + self._logger.debug("Started a stream recording of %s seconds", duration) # Take advantage of lookback hls: HlsStreamOutput = cast(HlsStreamOutput, self.outputs().get(HLS_PROVIDER)) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 48144219994..f05b2ece829 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -724,7 +724,7 @@ async def test_durations(hass, record_worker_sync): ) source = generate_h264_video(duration=SEGMENT_DURATION + 1) - stream = create_stream(hass, source, {}) + stream = create_stream(hass, source, {}, stream_label="camera") # use record_worker_sync to grab output segments with patch.object(hass.config, "is_allowed_path", return_value=True): @@ -797,7 +797,7 @@ async def test_has_keyframe(hass, record_worker_sync, h264_video): }, ) - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, stream_label="camera") # use record_worker_sync to grab output segments with patch.object(hass.config, "is_allowed_path", return_value=True): @@ -836,7 +836,7 @@ async def test_h265_video_is_hvc1(hass, record_worker_sync): ) source = generate_h265_video() - stream = create_stream(hass, source, {}) + stream = create_stream(hass, source, {}, stream_label="camera") # use record_worker_sync to grab output segments with patch.object(hass.config, "is_allowed_path", return_value=True):