mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Add visual image preview during generic camera config flow (#71269)
* Add visual preview during setup of generic camera * Code review: standardize preview url * Fix slug test * Refactor to use HomeAssistantView * Code review: simplify * Update manifest * Don't illegally access protected member * Increase test coverage * Prevent browser caching of preview images. * Code review:move incrementor to ?t=X + simplify * Discard old flow preview data * Increase test coverage * Code review: rename variables for clarity * Add timeout for image previews * Fix preview timeout tests * Simplify: store cam image preview in config_flow * Call step method to transition between flow steps * Only store user_input in flow, not CameraObject * Fix problem where test wouldn't run in isolation. * Simplify test * Don't move directly to another step's form * Remove unused constant * Simplify test Co-authored-by: Dave T <davet2001@users.noreply.github.com>
This commit is contained in:
parent
6111fb38a7
commit
6040c30b45
@ -3,17 +3,21 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from datetime import datetime
|
||||||
from errno import EHOSTUNREACH, EIO
|
from errno import EHOSTUNREACH, EIO
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import PIL
|
import PIL
|
||||||
|
from aiohttp import web
|
||||||
from async_timeout import timeout
|
from async_timeout import timeout
|
||||||
from httpx import HTTPStatusError, RequestError, TimeoutException
|
from httpx import HTTPStatusError, RequestError, TimeoutException
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yarl
|
import yarl
|
||||||
|
|
||||||
|
from homeassistant.components.camera import CAMERA_IMAGE_TIMEOUT, _async_get_image
|
||||||
|
from homeassistant.components.http.view import HomeAssistantView
|
||||||
from homeassistant.components.stream import (
|
from homeassistant.components.stream import (
|
||||||
CONF_RTSP_TRANSPORT,
|
CONF_RTSP_TRANSPORT,
|
||||||
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
|
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
|
||||||
@ -33,14 +37,15 @@ from homeassistant.const import (
|
|||||||
HTTP_DIGEST_AUTHENTICATION,
|
HTTP_DIGEST_AUTHENTICATION,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult, UnknownFlow
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers import config_validation as cv, template as template_helper
|
from homeassistant.helpers import config_validation as cv, template as template_helper
|
||||||
from homeassistant.helpers.httpx_client import get_async_client
|
from homeassistant.helpers.httpx_client import get_async_client
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
from .camera import generate_auth
|
from .camera import GenericCamera, generate_auth
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_CONFIRMED_OK,
|
||||||
CONF_CONTENT_TYPE,
|
CONF_CONTENT_TYPE,
|
||||||
CONF_FRAMERATE,
|
CONF_FRAMERATE,
|
||||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE,
|
CONF_LIMIT_REFETCH_TO_URL_CHANGE,
|
||||||
@ -62,6 +67,7 @@ DEFAULT_DATA = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
|
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
|
||||||
|
IMAGE_PREVIEWS_ACTIVE = "previews"
|
||||||
|
|
||||||
|
|
||||||
def build_schema(
|
def build_schema(
|
||||||
@ -190,6 +196,7 @@ def slug(
|
|||||||
hass: HomeAssistant, template: str | template_helper.Template | None
|
hass: HomeAssistant, template: str | template_helper.Template | None
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Convert a camera url into a string suitable for a camera name."""
|
"""Convert a camera url into a string suitable for a camera name."""
|
||||||
|
url = ""
|
||||||
if not template:
|
if not template:
|
||||||
return None
|
return None
|
||||||
if not isinstance(template, template_helper.Template):
|
if not isinstance(template, template_helper.Template):
|
||||||
@ -197,10 +204,8 @@ def slug(
|
|||||||
try:
|
try:
|
||||||
url = template.async_render(parse_result=False)
|
url = template.async_render(parse_result=False)
|
||||||
return slugify(yarl.URL(url).host)
|
return slugify(yarl.URL(url).host)
|
||||||
except TemplateError as err:
|
except (ValueError, TemplateError, TypeError) as err:
|
||||||
_LOGGER.error("Syntax error in '%s': %s", template.template, err)
|
_LOGGER.error("Syntax error in '%s': %s", template, err)
|
||||||
except (ValueError, TypeError) as err:
|
|
||||||
_LOGGER.error("Syntax error in '%s': %s", url, err)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -261,6 +266,16 @@ async def async_test_stream(
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def register_preview(hass: HomeAssistant):
|
||||||
|
"""Set up previews for camera feeds during config flow."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
|
if not hass.data[DOMAIN].get(IMAGE_PREVIEWS_ACTIVE):
|
||||||
|
_LOGGER.debug("Registering camera image preview handler")
|
||||||
|
hass.http.register_view(CameraImagePreview(hass))
|
||||||
|
hass.data[DOMAIN][IMAGE_PREVIEWS_ACTIVE] = True
|
||||||
|
|
||||||
|
|
||||||
class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Config flow for generic IP camera."""
|
"""Config flow for generic IP camera."""
|
||||||
|
|
||||||
@ -268,8 +283,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize Generic ConfigFlow."""
|
"""Initialize Generic ConfigFlow."""
|
||||||
self.cached_user_input: dict[str, Any] = {}
|
self.user_input: dict[str, Any] = {}
|
||||||
self.cached_title = ""
|
self.title = ""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
@ -314,19 +329,45 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
# The automatically generated still image that stream generates
|
# The automatically generated still image that stream generates
|
||||||
# is always jpeg
|
# is always jpeg
|
||||||
user_input[CONF_CONTENT_TYPE] = "image/jpeg"
|
user_input[CONF_CONTENT_TYPE] = "image/jpeg"
|
||||||
|
self.user_input = user_input
|
||||||
|
self.title = name
|
||||||
|
|
||||||
return self.async_create_entry(
|
# temporary preview for user to check the image
|
||||||
title=name, data={}, options=user_input
|
self.context["preview_cam"] = user_input
|
||||||
)
|
return await self.async_step_user_confirm_still()
|
||||||
|
elif self.user_input:
|
||||||
|
user_input = self.user_input
|
||||||
else:
|
else:
|
||||||
user_input = DEFAULT_DATA.copy()
|
user_input = DEFAULT_DATA.copy()
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
data_schema=build_schema(user_input),
|
data_schema=build_schema(user_input),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_user_confirm_still(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle user clicking confirm after still preview."""
|
||||||
|
if user_input:
|
||||||
|
if not user_input.get(CONF_CONFIRMED_OK):
|
||||||
|
return await self.async_step_user()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.title, data={}, options=self.user_input
|
||||||
|
)
|
||||||
|
register_preview(self.hass)
|
||||||
|
preview_url = f"/api/generic/preview_flow_image/{self.flow_id}?t={datetime.now().isoformat()}"
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user_confirm_still",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_CONFIRMED_OK, default=False): bool,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
description_placeholders={"preview_url": preview_url},
|
||||||
|
errors=None,
|
||||||
|
)
|
||||||
|
|
||||||
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
||||||
"""Handle config import from yaml."""
|
"""Handle config import from yaml."""
|
||||||
# abort if we've already got this one.
|
# abort if we've already got this one.
|
||||||
@ -410,3 +451,33 @@ class GenericOptionsFlowHandler(OptionsFlow):
|
|||||||
),
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CameraImagePreview(HomeAssistantView):
|
||||||
|
"""Camera view to temporarily serve an image."""
|
||||||
|
|
||||||
|
url = "/api/generic/preview_flow_image/{flow_id}"
|
||||||
|
name = "api:generic:preview_flow_image"
|
||||||
|
requires_auth = False
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialise."""
|
||||||
|
self.hass = hass
|
||||||
|
|
||||||
|
async def get(self, request: web.Request, flow_id: str) -> web.Response:
|
||||||
|
"""Start a GET request."""
|
||||||
|
_LOGGER.debug("processing GET request for flow_id=%s", flow_id)
|
||||||
|
try:
|
||||||
|
flow: FlowResult = self.hass.config_entries.flow.async_get(flow_id)
|
||||||
|
except UnknownFlow as exc:
|
||||||
|
raise web.HTTPNotFound() from exc
|
||||||
|
user_input = flow["context"]["preview_cam"]
|
||||||
|
camera = GenericCamera(self.hass, user_input, flow_id, "preview")
|
||||||
|
if not camera.is_on:
|
||||||
|
_LOGGER.debug("Camera is off")
|
||||||
|
raise web.HTTPServiceUnavailable()
|
||||||
|
image = await _async_get_image(
|
||||||
|
camera,
|
||||||
|
CAMERA_IMAGE_TIMEOUT,
|
||||||
|
)
|
||||||
|
return web.Response(body=image.content, content_type=image.content_type)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
DOMAIN = "generic"
|
DOMAIN = "generic"
|
||||||
DEFAULT_NAME = "Generic Camera"
|
DEFAULT_NAME = "Generic Camera"
|
||||||
|
CONF_CONFIRMED_OK = "confirmed_ok"
|
||||||
CONF_CONTENT_TYPE = "content_type"
|
CONF_CONTENT_TYPE = "content_type"
|
||||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change"
|
CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change"
|
||||||
CONF_STILL_IMAGE_URL = "still_image_url"
|
CONF_STILL_IMAGE_URL = "still_image_url"
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"name": "Generic Camera",
|
"name": "Generic Camera",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"requirements": ["ha-av==10.0.0b5", "pillow==9.2.0"],
|
"requirements": ["ha-av==10.0.0b5", "pillow==9.2.0"],
|
||||||
|
"dependencies": ["http"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/generic",
|
"documentation": "https://www.home-assistant.io/integrations/generic",
|
||||||
"codeowners": ["@davet2001"],
|
"codeowners": ["@davet2001"],
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
|
@ -40,8 +40,12 @@
|
|||||||
"content_type": "Content Type"
|
"content_type": "Content Type"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"confirm": {
|
"user_confirm_still": {
|
||||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
"title": "Preview",
|
||||||
|
"description": "",
|
||||||
|
"data": {
|
||||||
|
"confirmed_ok": "This image looks good."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -23,9 +23,6 @@
|
|||||||
"unknown": "Unexpected error"
|
"unknown": "Unexpected error"
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"confirm": {
|
|
||||||
"description": "Do you want to start set up?"
|
|
||||||
},
|
|
||||||
"content_type": {
|
"content_type": {
|
||||||
"data": {
|
"data": {
|
||||||
"content_type": "Content Type"
|
"content_type": "Content Type"
|
||||||
@ -45,6 +42,13 @@
|
|||||||
"verify_ssl": "Verify SSL certificate"
|
"verify_ssl": "Verify SSL certificate"
|
||||||
},
|
},
|
||||||
"description": "Enter the settings to connect to the camera."
|
"description": "Enter the settings to connect to the camera."
|
||||||
|
},
|
||||||
|
"user_confirm_still": {
|
||||||
|
"data": {
|
||||||
|
"confirmed_ok": "This image looks good."
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"title": "Preview"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -78,7 +78,6 @@ def mock_create_stream():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def user_flow(hass):
|
async def user_flow(hass):
|
||||||
"""Initiate a user flow."""
|
"""Initiate a user flow."""
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
@ -20,6 +20,7 @@ from homeassistant.components.generic.const import (
|
|||||||
CONF_STREAM_SOURCE,
|
CONF_STREAM_SOURCE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.stream.const import CONF_RTSP_TRANSPORT
|
||||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL
|
||||||
@ -209,6 +210,7 @@ async def test_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png
|
|||||||
CONF_VERIFY_SSL: False,
|
CONF_VERIFY_SSL: False,
|
||||||
CONF_USERNAME: "barney",
|
CONF_USERNAME: "barney",
|
||||||
CONF_PASSWORD: "betty",
|
CONF_PASSWORD: "betty",
|
||||||
|
CONF_RTSP_TRANSPORT: "http",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
mock_entry.add_to_hass(hass)
|
mock_entry.add_to_hass(hass)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
"""Test The generic (IP Camera) config flow."""
|
"""Test The generic (IP Camera) config flow."""
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
|
from http import HTTPStatus
|
||||||
import os.path
|
import os.path
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
@ -12,6 +13,7 @@ from homeassistant import config_entries, data_entry_flow
|
|||||||
from homeassistant.components.camera import async_get_image
|
from homeassistant.components.camera import async_get_image
|
||||||
from homeassistant.components.generic.config_flow import slug
|
from homeassistant.components.generic.config_flow import slug
|
||||||
from homeassistant.components.generic.const import (
|
from homeassistant.components.generic.const import (
|
||||||
|
CONF_CONFIRMED_OK,
|
||||||
CONF_CONTENT_TYPE,
|
CONF_CONTENT_TYPE,
|
||||||
CONF_FRAMERATE,
|
CONF_FRAMERATE,
|
||||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE,
|
CONF_LIMIT_REFETCH_TO_URL_CHANGE,
|
||||||
@ -58,16 +60,30 @@ TESTDATA_YAML = {
|
|||||||
|
|
||||||
|
|
||||||
@respx.mock
|
@respx.mock
|
||||||
async def test_form(hass, fakeimg_png, user_flow, mock_create_stream):
|
async def test_form(hass, fakeimgbytes_png, hass_client, user_flow, mock_create_stream):
|
||||||
"""Test the form with a normal set of settings."""
|
"""Test the form with a normal set of settings."""
|
||||||
|
|
||||||
|
respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png)
|
||||||
with mock_create_stream as mock_setup, patch(
|
with mock_create_stream as mock_setup, patch(
|
||||||
"homeassistant.components.generic.async_setup_entry", return_value=True
|
"homeassistant.components.generic.async_setup_entry", return_value=True
|
||||||
) as mock_setup_entry:
|
) as mock_setup_entry:
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
user_flow["flow_id"],
|
user_flow["flow_id"],
|
||||||
TESTDATA,
|
TESTDATA,
|
||||||
)
|
)
|
||||||
|
assert result1["type"] == data_entry_flow.FlowResultType.FORM
|
||||||
|
assert result1["step_id"] == "user_confirm_still"
|
||||||
|
client = await hass_client()
|
||||||
|
preview_id = result1["flow_id"]
|
||||||
|
# Check the preview image works.
|
||||||
|
resp = await client.get(f"/api/generic/preview_flow_image/{preview_id}?t=1")
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
assert await resp.read() == fakeimgbytes_png
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result1["flow_id"],
|
||||||
|
user_input={CONF_CONFIRMED_OK: True},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == "127_0_0_1"
|
assert result2["title"] == "127_0_0_1"
|
||||||
assert result2["options"] == {
|
assert result2["options"] == {
|
||||||
@ -83,6 +99,9 @@ async def test_form(hass, fakeimg_png, user_flow, mock_create_stream):
|
|||||||
}
|
}
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
# Check that the preview image is disabled after.
|
||||||
|
resp = await client.get(f"/api/generic/preview_flow_image/{preview_id}")
|
||||||
|
assert resp.status == HTTPStatus.NOT_FOUND
|
||||||
assert len(mock_setup.mock_calls) == 1
|
assert len(mock_setup.mock_calls) == 1
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
@ -99,11 +118,17 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow):
|
|||||||
data = TESTDATA.copy()
|
data = TESTDATA.copy()
|
||||||
data.pop(CONF_STREAM_SOURCE)
|
data.pop(CONF_STREAM_SOURCE)
|
||||||
with patch("homeassistant.components.generic.async_setup_entry", return_value=True):
|
with patch("homeassistant.components.generic.async_setup_entry", return_value=True):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
user_flow["flow_id"],
|
user_flow["flow_id"],
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
assert result1["type"] == data_entry_flow.FlowResultType.FORM
|
||||||
|
assert result1["step_id"] == "user_confirm_still"
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result1["flow_id"],
|
||||||
|
user_input={CONF_CONFIRMED_OK: True},
|
||||||
|
)
|
||||||
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == "127_0_0_1"
|
assert result2["title"] == "127_0_0_1"
|
||||||
assert result2["options"] == {
|
assert result2["options"] == {
|
||||||
@ -120,16 +145,65 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow):
|
|||||||
assert respx.calls.call_count == 1
|
assert respx.calls.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
async def test_form_reject_still_preview(
|
||||||
|
hass, fakeimgbytes_png, mock_create_stream, user_flow
|
||||||
|
):
|
||||||
|
"""Test we go back to the config screen if the user rejects the still preview."""
|
||||||
|
respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png)
|
||||||
|
with mock_create_stream:
|
||||||
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
|
user_flow["flow_id"],
|
||||||
|
TESTDATA,
|
||||||
|
)
|
||||||
|
assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result1["step_id"] == "user_confirm_still"
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result1["flow_id"],
|
||||||
|
user_input={CONF_CONFIRMED_OK: False},
|
||||||
|
)
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["step_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
@respx.mock
|
||||||
|
async def test_form_still_preview_cam_off(
|
||||||
|
hass, fakeimg_png, mock_create_stream, user_flow, hass_client
|
||||||
|
):
|
||||||
|
"""Test camera errors are triggered during preview."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.generic.camera.GenericCamera.is_on",
|
||||||
|
new_callable=PropertyMock(return_value=False),
|
||||||
|
), mock_create_stream:
|
||||||
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
|
user_flow["flow_id"],
|
||||||
|
TESTDATA,
|
||||||
|
)
|
||||||
|
assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result1["step_id"] == "user_confirm_still"
|
||||||
|
preview_id = result1["flow_id"]
|
||||||
|
# Try to view the image, should be unavailable.
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get(f"/api/generic/preview_flow_image/{preview_id}?t=1")
|
||||||
|
assert resp.status == HTTPStatus.SERVICE_UNAVAILABLE
|
||||||
|
|
||||||
|
|
||||||
@respx.mock
|
@respx.mock
|
||||||
async def test_form_only_stillimage_gif(hass, fakeimg_gif, user_flow):
|
async def test_form_only_stillimage_gif(hass, fakeimg_gif, user_flow):
|
||||||
"""Test we complete ok if the user wants a gif."""
|
"""Test we complete ok if the user wants a gif."""
|
||||||
data = TESTDATA.copy()
|
data = TESTDATA.copy()
|
||||||
data.pop(CONF_STREAM_SOURCE)
|
data.pop(CONF_STREAM_SOURCE)
|
||||||
with patch("homeassistant.components.generic.async_setup_entry", return_value=True):
|
with patch("homeassistant.components.generic.async_setup_entry", return_value=True):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
user_flow["flow_id"],
|
user_flow["flow_id"],
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
|
assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result1["step_id"] == "user_confirm_still"
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result1["flow_id"],
|
||||||
|
user_input={CONF_CONFIRMED_OK: True},
|
||||||
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
assert result2["options"][CONF_CONTENT_TYPE] == "image/gif"
|
assert result2["options"][CONF_CONTENT_TYPE] == "image/gif"
|
||||||
@ -143,11 +217,17 @@ async def test_form_only_svg_whitespace(hass, fakeimgbytes_svg, user_flow):
|
|||||||
data = TESTDATA.copy()
|
data = TESTDATA.copy()
|
||||||
data.pop(CONF_STREAM_SOURCE)
|
data.pop(CONF_STREAM_SOURCE)
|
||||||
with patch("homeassistant.components.generic.async_setup_entry", return_value=True):
|
with patch("homeassistant.components.generic.async_setup_entry", return_value=True):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
user_flow["flow_id"],
|
user_flow["flow_id"],
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
assert result1["type"] == data_entry_flow.FlowResultType.FORM
|
||||||
|
assert result1["step_id"] == "user_confirm_still"
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result1["flow_id"],
|
||||||
|
user_input={CONF_CONFIRMED_OK: True},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
@ -170,10 +250,16 @@ async def test_form_only_still_sample(hass, user_flow, image_file):
|
|||||||
data = TESTDATA.copy()
|
data = TESTDATA.copy()
|
||||||
data.pop(CONF_STREAM_SOURCE)
|
data.pop(CONF_STREAM_SOURCE)
|
||||||
with patch("homeassistant.components.generic.async_setup_entry", return_value=True):
|
with patch("homeassistant.components.generic.async_setup_entry", return_value=True):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
user_flow["flow_id"],
|
user_flow["flow_id"],
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
|
assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result1["step_id"] == "user_confirm_still"
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result1["flow_id"],
|
||||||
|
user_input={CONF_CONFIRMED_OK: True},
|
||||||
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
@ -186,31 +272,31 @@ async def test_form_only_still_sample(hass, user_flow, image_file):
|
|||||||
(
|
(
|
||||||
"http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png",
|
"http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png",
|
||||||
"http://localhost:8123/static/icons/favicon-apple-180x180.png",
|
"http://localhost:8123/static/icons/favicon-apple-180x180.png",
|
||||||
data_entry_flow.FlowResultType.CREATE_ENTRY,
|
"user_confirm_still",
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"{% if 1 %}https://bla{% else %}https://yo{% endif %}",
|
"{% if 1 %}https://bla{% else %}https://yo{% endif %}",
|
||||||
"https://bla/",
|
"https://bla/",
|
||||||
data_entry_flow.FlowResultType.CREATE_ENTRY,
|
"user_confirm_still",
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"http://{{example.org",
|
"http://{{example.org",
|
||||||
"http://example.org",
|
"http://example.org",
|
||||||
data_entry_flow.FlowResultType.FORM,
|
"user",
|
||||||
{"still_image_url": "template_error"},
|
{"still_image_url": "template_error"},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"invalid1://invalid:4\\1",
|
"invalid1://invalid:4\\1",
|
||||||
"invalid1://invalid:4%5c1",
|
"invalid1://invalid:4%5c1",
|
||||||
data_entry_flow.FlowResultType.FORM,
|
"user",
|
||||||
{"still_image_url": "malformed_url"},
|
{"still_image_url": "malformed_url"},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"relative/urls/are/not/allowed.jpg",
|
"relative/urls/are/not/allowed.jpg",
|
||||||
"relative/urls/are/not/allowed.jpg",
|
"relative/urls/are/not/allowed.jpg",
|
||||||
data_entry_flow.FlowResultType.FORM,
|
"user",
|
||||||
{"still_image_url": "relative_url"},
|
{"still_image_url": "relative_url"},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -229,7 +315,7 @@ async def test_still_template(
|
|||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert result2["type"] == expected_result
|
assert result2["step_id"] == expected_result
|
||||||
assert result2.get("errors") == expected_errors
|
assert result2.get("errors") == expected_errors
|
||||||
|
|
||||||
|
|
||||||
@ -242,10 +328,15 @@ async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream):
|
|||||||
with mock_create_stream as mock_setup, patch(
|
with mock_create_stream as mock_setup, patch(
|
||||||
"homeassistant.components.generic.async_setup_entry", return_value=True
|
"homeassistant.components.generic.async_setup_entry", return_value=True
|
||||||
):
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
user_flow["flow_id"], data
|
user_flow["flow_id"], data
|
||||||
)
|
)
|
||||||
assert "errors" not in result2, f"errors={result2['errors']}"
|
assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result1["step_id"] == "user_confirm_still"
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result1["flow_id"],
|
||||||
|
user_input={CONF_CONFIRMED_OK: True},
|
||||||
|
)
|
||||||
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == "127_0_0_1"
|
assert result2["title"] == "127_0_0_1"
|
||||||
assert result2["options"] == {
|
assert result2["options"] == {
|
||||||
@ -265,21 +356,23 @@ async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream):
|
|||||||
assert len(mock_setup.mock_calls) == 1
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_form_only_stream(hass, fakeimgbytes_jpg, mock_create_stream):
|
async def test_form_only_stream(hass, fakeimgbytes_jpg, user_flow, mock_create_stream):
|
||||||
"""Test we complete ok if the user wants stream only."""
|
"""Test we complete ok if the user wants stream only."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
||||||
)
|
|
||||||
data = TESTDATA.copy()
|
data = TESTDATA.copy()
|
||||||
data.pop(CONF_STILL_IMAGE_URL)
|
data.pop(CONF_STILL_IMAGE_URL)
|
||||||
data[CONF_STREAM_SOURCE] = "rtsp://user:pass@127.0.0.1/testurl/2"
|
data[CONF_STREAM_SOURCE] = "rtsp://user:pass@127.0.0.1/testurl/2"
|
||||||
with mock_create_stream as mock_setup:
|
with mock_create_stream as mock_setup:
|
||||||
result3 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
user_flow["flow_id"],
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
|
assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result1["step_id"] == "user_confirm_still"
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result1["flow_id"],
|
||||||
|
user_input={CONF_CONFIRMED_OK: True},
|
||||||
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
assert result3["title"] == "127_0_0_1"
|
assert result3["title"] == "127_0_0_1"
|
||||||
assert result3["options"] == {
|
assert result3["options"] == {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user