Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add config flow stream preview to generic camera #122563

Open
wants to merge 23 commits into
base: dev
Choose a base branch
from

Conversation

davet2001
Copy link
Contributor

@davet2001 davet2001 commented Jul 24, 2024

Proposed change

Add preview of video stream during config flow to allow user to verify stream settings when configuring.

stream_preview_2.mp4

The generic camera config flow was updated some time ago to show a preview of the still image before the config flow was complete, the aim was to help users get username/passwords correct without having to add a camera, then delete it if it didn't work. But so far that was only implemented on still images.

The real problem and most of the remaining questions that still occur on the forums are from users trying to set up cameras, and confusing rtsp settings, passwords, or simply not knowing how to find a valid stream URL for their camera.

This PR uses a custom preview element to create a view of the stream before the end of the config flow.

A corresponding PR for the frontend adds the preview element.

[ ] Todo: update pytests to 100% coverage

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.

To help with the load of incoming pull requests:

@davet2001 davet2001 changed the title Add config flow stream preview Add config flow stream preview to generic Jul 24, 2024
@davet2001 davet2001 changed the title Add config flow stream preview to generic Add config flow stream preview to generic camera Jul 24, 2024
@davet2001
Copy link
Contributor Author

This is a bit more tricky than normal to get full pytest coverage because of the websocket connection. Would value an initial review from someone before I invest time adding the tests.

@davet2001 davet2001 requested review from allenporter and uvjustin and removed request for allenporter July 26, 2024 21:49
entity_namespace=None,
)

stream = await cam.async_create_stream()
Copy link
Contributor

@allenporter allenporter Jul 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps camera._async_stream_endpoint_url should be exposed for this purpose since a lot of this setup is shared? I realize this is specific to generic so nice to not polute the apis, but also all the stream APIs are fairly low level so it may be worth having fewer special interactions with them.

Alternatively, could consider moving the preview webview into camera but not sure that other integrations should reuse it... But given the deep integration with creating an entity platform and all that, it seems very much tied to camera integration details.

Did you consider a direct stream integration here rather than a camera integration?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just re-read how async_test_stream works and that seems to me to argue for a direct stream integration since one already exists.

  • Whats the motivation for multiple ways to do that?
  • Whats going to happen to the image preview? seems like its a parallel path, but can it be combined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree on the camera._async_stream_endpoint_url, exposing this would avoid some duplication.

In general my starting approach was to simulate the creation of the final camera entity as much as possible, with all of its settings, so that the user gets real information on whether that configuration will actually work. This seemed the most reliable & future proof way of doing it. So I see the preview (still and stream) equally about testing the behaviour of the entity that will be created as showing an image/stream.

There are two phases:

  1. Verify that the settings are valid
    This happens when the user clicks submit on the first config flow screen.

  2. Show a preview
    Create something with those settings and show it. This happens when the websocket callback is triggered on the second screen.

The issue I found was that during 1) I'm not ready to create the preview. The settings could be wrong, in which case I need to bounce the user back to the first screen with an error next to the wrong parameter.

But via the websocket method, the notice to generate the preview only comes when we are already at step 2). If I find an error at that step, I need to bounce them back to step 1) again, but I couldn't see a way of doing that since the step 2) dialog is already showing.

So we end up with two hits to the external URLs, one for checking and one for the preview. Not a big issue in the case of the still image, but 2x the stream delay is very noticeable. I don't really like this and I'm open to ideas for a better approach. Possibly I could create the stream during the check and keep hold of it if the check was successful.

Regarding the image preview, yes you are right, I was planning to integrate that back into the same approach in a separate PR.

Side note: I did try for a while with a live preview at the bottom of the settings screen in the same way that time_date does it. But I stopped down that route because:

  1. the dialog became incredibly tall, so you couldn't actually see the preview without scrolling
  2. the external URLs were being hit and streams generated for every character typed, which felt likely to cause problems (e.g. authentication lockouts)

There might be a potential architecture improvement here, because I imagine most integrations will face similar issues, and will want to run a preview without interfering with the real hass object (also during options flow).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line of thought around making an entity is a good one to just do an end to end test, though I have a couple doubts. My thinking now is (1) i'm not that familiar with making fake entities we don't actually use anywhere, and implications of doing that e.g. entity may write its state as there are status updates on the stream (discussed below) and (2) we already have decided when doing validation to create the stream object directly anyway, so it may be easiest to just reuse that (though i didn't look at all the ways it can diverge based on a setting in camera that isn't just passed 1:1 to stream)

Maybe there are two things i might tease apart here:

  • code reuse: i think it makes sense to have one code path for creating the stream and that code path could be reused for testing settings and making the preview
  • object reuse: there is a possible performance win from reusing the same actual stream object

Perhaps we can tease these apart, and there may be an incremental path for first having reusable code, then second iteration on having reusable state.

I realize it may not be that simple so happy to keep discussing if that path isn't as simple as i'm making it seem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenporter Ok, I've progressed it quite a bit further now. The stream test and the preview are now combined, this makes it a lot faster too.

Left over streams are now auto-stopped after 10minutes

I've tidied up the rest of the code and removed several things that were unnecessary.

Since I've 'sort of' subclassed Stream anyway now to do the timeout, that could possibly open up the option for blocking async_write_ha_state.

Either way I would like your thoughts on this before going further. Thanks in advance!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this approach a lot and it seems very simple and easy to read/understand. I really like that we don't need to use the entity anymore here and can just have one way to use the stream.

Any other specific input now or review needed? It generally looks good and i wanted to give quick feedback, but happy to do the last round of review when you are ready.

(The part where it loads the platform to ensure translations are loaded is something i'll want to give a closer look at for prior art/ alternatives)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @allenporter I'll get the tests implemented, then go for final review.

I will put the options flow on a separate PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may have missed before that the image preview works by using the camera entity, so now the image preview and stream preview take different strategies.

await prefs.async_load()
hass.data[DATA_CAMERA_PREFS] = prefs

# Create a camera but don't add it to the hass object.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a problem that stream may call async_write_ha_state on this if its not really added to hass?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it gets called, then I think it will write something to the hass data object. But is stream likely to do that?

Either way I don't see it causing a problem other than a data entry for something that will no longer exist in the future. I have looked at a few ways but think it could be hard to decouple this.

Possibly could do something like
self._platform_state == EntityPlatformState.REMOVED
to block the write but that is a bit of a hack. Maybe there is a better way of doing this?

Possibly subclass it and modify async_write_ha_state? Maybe there is a general case for this type of throw away temporary entity to handle previews in config/option flows?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allenporter I've done some experimentation, and haven't seen the stream writing _async_write_ha_state so far in my logs. I couldn't see a mechanism where this would happen other than one being added via set_update_callback(), and nothing seems to do that.

Please let me know if I'm missing something!

I think this is working ok now from the core side.

homeassistant/components/generic/config_flow.py Outdated Show resolved Hide resolved
entity_namespace=None,
)

stream = await cam.async_create_stream()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just re-read how async_test_stream works and that seems to me to argue for a direct stream integration since one already exists.

  • Whats the motivation for multiple ways to do that?
  • Whats going to happen to the image preview? seems like its a parallel path, but can it be combined?

@home-assistant
Copy link

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant home-assistant bot marked this pull request as draft July 27, 2024 19:00
@allenporter
Copy link
Contributor

This is a bit more tricky than normal to get full pytest coverage because of the websocket connection. Would value an initial review from someone before I invest time adding the tests.

Makes sense, gave my initial thoughts and happy to brainstorm / discuss more here. I could see this also being upleveled to an architecture discussion if it seems worthwhile to try to centralize some of the preview stuff a bit more into streamlined APIs

@davet2001 davet2001 force-pushed the generic_camera_stream_preview_3 branch from ee52452 to acbec89 Compare August 7, 2024 06:47
) -> dict[str, str] | PreviewStream:
"""Verify that the stream is valid before we create an entity.

Returns a dict with errors if any, or the stream object if valid.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's time to switch to raising a specific exception here

entity_namespace=None,
)

stream = await cam.async_create_stream()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this approach a lot and it seems very simple and easy to read/understand. I really like that we don't need to use the entity anymore here and can just have one way to use the stream.

Any other specific input now or review needed? It generally looks good and i wanted to give quick feedback, but happy to do the last round of review when you are ready.

(The part where it loads the platform to ensure translations are loaded is something i'll want to give a closer look at for prior art/ alternatives)

@davet2001 davet2001 force-pushed the generic_camera_stream_preview_3 branch from acbec89 to 67a141f Compare September 7, 2024 07:40
@davet2001 davet2001 force-pushed the generic_camera_stream_preview_3 branch 2 times, most recently from 640ee52 to ca044fd Compare October 7, 2024 19:40
@davet2001 davet2001 marked this pull request as ready for review October 8, 2024 12:25
@davet2001 davet2001 requested a review from a team as a code owner October 8, 2024 12:25
@davet2001 davet2001 force-pushed the generic_camera_stream_preview_3 branch from f611f36 to e3e5e4e Compare November 23, 2024 12:16
@davet2001 davet2001 marked this pull request as ready for review November 23, 2024 18:07
@home-assistant home-assistant bot requested a review from allenporter November 23, 2024 18:07
Copy link
Contributor

@allenporter allenporter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is something broken with test coverage for the websocket function? I see tests, but the coverage doesn't get registered

if not await hls_provider.part_recv(timeout=SOURCE_TIMEOUT):
hass.async_create_task(stream.stop())
return {CONF_STREAM_SOURCE: "timeout"}
await stream.stop()
except StreamWorkerError as err:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I very much like the goal of giving helpful error messages and I think it is a good idea. However, I'm just wondering if its even possible for these errors to be thrown. I'm not sure it is possible for a StreamWorkerError, PermissionError, OSError etc to get raised here. My impression is none of these exceptions can be raised directly here at the moment.


async def _timeout() -> None:
_LOGGER.debug("Starting preview stream with timeout %ss", timeout)
await asyncio.sleep(timeout)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, sounds good.

Regarding the delay between steps, i'm trying to say that calling stream.add_provider is idempotent and it can be called before the stream is shown to add a new idle timer if the stream already timed out because the delay between steps was too long.

I think its reasonable to not have a test verify this and trust the stream component will handle the timeout, so that sounds fine.

@davet2001
Copy link
Contributor Author

davet2001 commented Nov 24, 2024

Is something broken with test coverage for the websocket function? I see tests, but the coverage doesn't get registered

Yes, the test needed changing to reflect the removed WS parameters.
Fixed in 928766f.

@davet2001
Copy link
Contributor Author

@allenporter I think this is done now from my point of view.
The only remaining error on this PR is 'waiting for frontend'.
The frontend's PR's only error is 'waiting for backend'.

Not sure how catch 22s like this are solved. Any ideas?

@edenhaus
Copy link
Contributor

The only remaining error on this PR is 'waiting for frontend'.
The home-assistant/frontend#21463 only error is 'waiting for backend'.

The labels will be removed, when both PRs are approved

@edenhaus
Copy link
Contributor

I have a question on your video: Can we disable the checkbox until the stream is loaded?
I think some users will click it immediately as it takes some time until the preview is loaded. It takes some time, even the loading screen is showed


async def _timeout() -> None:
_LOGGER.debug("Starting preview stream with timeout %ss", timeout)
await asyncio.sleep(timeout)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was still expecting this PR would remove this timeout behavior and rely on the existing stream timeout.

@home-assistant home-assistant bot marked this pull request as draft November 27, 2024 16:29
@davet2001
Copy link
Contributor Author

davet2001 commented Dec 2, 2024

I have a question on your video: Can we disable the checkbox until the stream is loaded? I think some users will click it immediately as it takes some time until the preview is loaded. It takes some time, even the loading screen is showed

I thought about that, and I would prefer to do it that way but it is not at all easy.

The checkbox comes from the conventional config_flow.py defined in python. Everything below the word 'Preview' comes from javascript. So I could add a checkbox there but it would have to communicate via WebSocket to tell the config flow what to do next, and if anything went wrong with making the websocket connection, it would not be possible to complete the config flow.

In reality the checkbox is a bit of a workaround for not having the option of a back button in config flow.

Happy to put in place another solution if you can see a good way of doing it.

@davet2001 davet2001 marked this pull request as ready for review December 2, 2024 22:24
@home-assistant home-assistant bot requested a review from allenporter December 2, 2024 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants