diff --git a/homeassistant/components/generic/__init__.py b/homeassistant/components/generic/__init__.py index 3de664dd734..5fdb27ce516 100644 --- a/homeassistant/components/generic/__init__.py +++ b/homeassistant/components/generic/__init__.py @@ -2,15 +2,23 @@ from __future__ import annotations +import logging from typing import Any +from homeassistant.components.stream import ( + CONF_RTSP_TRANSPORT, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_AUTHENTICATION, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er +from .const import CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, SECTION_ADVANCED + DOMAIN = "generic" PLATFORMS = [Platform.CAMERA] +_LOGGER = logging.getLogger(__name__) async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -47,3 +55,38 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate entry.""" + _LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version) + + if entry.version > 2: + # This means the user has downgraded from a future version + return False + + if entry.version == 1: + # Migrate to advanced section + new_options = {**entry.options} + advanced = new_options[SECTION_ADVANCED] = { + CONF_FRAMERATE: new_options.pop(CONF_FRAMERATE), + CONF_VERIFY_SSL: new_options.pop(CONF_VERIFY_SSL), + } + + # migrate optional fields + for key in ( + CONF_RTSP_TRANSPORT, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + CONF_AUTHENTICATION, + CONF_LIMIT_REFETCH_TO_URL_CHANGE, + ): + if key in new_options: + advanced[key] = new_options.pop(key) + + hass.config_entries.async_update_entry(entry, options=new_options, version=2) + + _LOGGER.debug( + "Migration to version %s:%s successful", entry.version, entry.minor_version + ) + + return True diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 6821300fadf..530d9a0bb9a 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -41,6 +41,7 @@ from .const import ( CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, GET_IMAGE_TIMEOUT, + SECTION_ADVANCED, ) _LOGGER = logging.getLogger(__name__) @@ -62,9 +63,11 @@ def generate_auth(device_info: Mapping[str, Any]) -> httpx.Auth | None: """Generate httpx.Auth object from credentials.""" username: str | None = device_info.get(CONF_USERNAME) password: str | None = device_info.get(CONF_PASSWORD) - authentication = device_info.get(CONF_AUTHENTICATION) if username and password: - if authentication == HTTP_DIGEST_AUTHENTICATION: + if ( + device_info[SECTION_ADVANCED].get(CONF_AUTHENTICATION) + == HTTP_DIGEST_AUTHENTICATION + ): return httpx.DigestAuth(username=username, password=password) return httpx.BasicAuth(username=username, password=password) return None @@ -99,14 +102,16 @@ class GenericCamera(Camera): if self._stream_source: self._stream_source = Template(self._stream_source, hass) self._attr_supported_features = CameraEntityFeature.STREAM - self._limit_refetch = device_info.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False) - self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE] + self._limit_refetch = device_info[SECTION_ADVANCED].get( + CONF_LIMIT_REFETCH_TO_URL_CHANGE, False + ) + self._attr_frame_interval = 1 / device_info[SECTION_ADVANCED][CONF_FRAMERATE] self.content_type = device_info[CONF_CONTENT_TYPE] - self.verify_ssl = device_info[CONF_VERIFY_SSL] - if device_info.get(CONF_RTSP_TRANSPORT): - self.stream_options[CONF_RTSP_TRANSPORT] = device_info[CONF_RTSP_TRANSPORT] + self.verify_ssl = device_info[SECTION_ADVANCED][CONF_VERIFY_SSL] + if rtsp_transport := device_info[SECTION_ADVANCED].get(CONF_RTSP_TRANSPORT): + self.stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport self._auth = generate_auth(device_info) - if device_info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + if device_info[SECTION_ADVANCED].get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True self._last_url = None diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index b20793fe060..95acb810890 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -50,10 +50,17 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import section from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.helpers import config_validation as cv, template as template_helper from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) from homeassistant.setup import async_prepare_setup_platform from homeassistant.util import slugify @@ -68,16 +75,19 @@ from .const import ( DEFAULT_NAME, DOMAIN, GET_IMAGE_TIMEOUT, + SECTION_ADVANCED, ) _LOGGER = logging.getLogger(__name__) DEFAULT_DATA = { CONF_NAME: DEFAULT_NAME, - CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, - CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, - CONF_FRAMERATE: 2, - CONF_VERIFY_SSL: True, + SECTION_ADVANCED: { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_FRAMERATE: 2, + CONF_VERIFY_SSL: True, + }, } SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"} @@ -94,58 +104,47 @@ class InvalidStreamException(HomeAssistantError): def build_schema( - user_input: Mapping[str, Any], is_options_flow: bool = False, show_advanced_options: bool = False, ) -> vol.Schema: """Create schema for camera config setup.""" + rtsp_options = [ + SelectOptionDict( + value=value, + label=name, + ) + for value, name in RTSP_TRANSPORTS.items() + ] + + advanced_section = { + vol.Required(CONF_FRAMERATE): vol.All( + vol.Range(min=0, min_included=False), cv.positive_float + ), + vol.Required(CONF_VERIFY_SSL): bool, + vol.Optional(CONF_RTSP_TRANSPORT): SelectSelector( + SelectSelectorConfig( + options=rtsp_options, + mode=SelectSelectorMode.DROPDOWN, + ) + ), + vol.Optional(CONF_AUTHENTICATION): vol.In( + [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] + ), + } spec = { - vol.Optional( - CONF_STILL_IMAGE_URL, - description={"suggested_value": user_input.get(CONF_STILL_IMAGE_URL, "")}, - ): str, - vol.Optional( - CONF_STREAM_SOURCE, - description={"suggested_value": user_input.get(CONF_STREAM_SOURCE, "")}, - ): str, - vol.Optional( - CONF_RTSP_TRANSPORT, - description={"suggested_value": user_input.get(CONF_RTSP_TRANSPORT)}, - ): vol.In(RTSP_TRANSPORTS), - vol.Optional( - CONF_AUTHENTICATION, - description={"suggested_value": user_input.get(CONF_AUTHENTICATION)}, - ): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), - vol.Optional( - CONF_USERNAME, - description={"suggested_value": user_input.get(CONF_USERNAME, "")}, - ): str, - vol.Optional( - CONF_PASSWORD, - description={"suggested_value": user_input.get(CONF_PASSWORD, "")}, - ): str, - vol.Required( - CONF_FRAMERATE, - description={"suggested_value": user_input.get(CONF_FRAMERATE, 2)}, - ): vol.All(vol.Range(min=0, min_included=False), cv.positive_float), - vol.Required( - CONF_VERIFY_SSL, default=user_input.get(CONF_VERIFY_SSL, True) - ): bool, + vol.Optional(CONF_STREAM_SOURCE): str, + vol.Optional(CONF_STILL_IMAGE_URL): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(SECTION_ADVANCED): section( + vol.Schema(advanced_section), {"collapsed": True} + ), } if is_options_flow: - spec[ - vol.Required( - CONF_LIMIT_REFETCH_TO_URL_CHANGE, - default=user_input.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False), - ) - ] = bool + advanced_section[vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE)] = bool if show_advanced_options: - spec[ - vol.Required( - CONF_USE_WALLCLOCK_AS_TIMESTAMPS, - default=user_input.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False), - ) - ] = bool + advanced_section[vol.Optional(CONF_USE_WALLCLOCK_AS_TIMESTAMPS)] = bool + return vol.Schema(spec) @@ -187,7 +186,7 @@ async def async_test_still( return {CONF_STILL_IMAGE_URL: "malformed_url"}, None if not yarl_url.is_absolute(): return {CONF_STILL_IMAGE_URL: "relative_url"}, None - verify_ssl = info[CONF_VERIFY_SSL] + verify_ssl = info[SECTION_ADVANCED][CONF_VERIFY_SSL] auth = generate_auth(info) try: async_client = get_async_client(hass, verify_ssl=verify_ssl) @@ -266,9 +265,9 @@ async def async_test_and_preview_stream( _LOGGER.warning("Problem rendering template %s: %s", stream_source, err) raise InvalidStreamException("template_error") from err stream_options: dict[str, str | bool | float] = {} - if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): + if rtsp_transport := info[SECTION_ADVANCED].get(CONF_RTSP_TRANSPORT): stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport - if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + if info[SECTION_ADVANCED].get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True try: @@ -324,7 +323,7 @@ def register_still_preview(hass: HomeAssistant) -> None: class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for generic IP camera.""" - VERSION = 1 + VERSION = 2 def __init__(self) -> None: """Initialize Generic ConfigFlow.""" @@ -379,7 +378,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): user_input = DEFAULT_DATA.copy() return self.async_show_form( step_id="user", - data_schema=build_schema(user_input), + data_schema=self.add_suggested_values_to_schema(build_schema(), user_input), errors=errors, ) @@ -447,13 +446,19 @@ class GenericOptionsFlowHandler(OptionsFlow): self.preview_stream = None if not errors: data = { - CONF_USE_WALLCLOCK_AS_TIMESTAMPS: self.config_entry.options.get( - CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False - ), **user_input, CONF_CONTENT_TYPE: still_format or self.config_entry.options.get(CONF_CONTENT_TYPE), } + if ( + CONF_USE_WALLCLOCK_AS_TIMESTAMPS + not in user_input[SECTION_ADVANCED] + ): + user_input[SECTION_ADVANCED][ + CONF_USE_WALLCLOCK_AS_TIMESTAMPS + ] = self.config_entry.options[SECTION_ADVANCED].get( + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False + ) self.user_input = data # temporary preview for user to check the image self.preview_image_settings = data @@ -462,10 +467,12 @@ class GenericOptionsFlowHandler(OptionsFlow): user_input = self.user_input return self.async_show_form( step_id="init", - data_schema=build_schema( + data_schema=self.add_suggested_values_to_schema( + build_schema( + True, + self.show_advanced_options, + ), user_input or self.config_entry.options, - True, - self.show_advanced_options, ), errors=errors, ) diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index 4fd600db381..fa1037e5781 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -9,3 +9,4 @@ CONF_STILL_IMAGE_URL = "still_image_url" CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" GET_IMAGE_TIMEOUT = 10 +SECTION_ADVANCED = "advanced" diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 4a5d672fcde..a582520b8ea 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -30,13 +30,21 @@ "data": { "still_image_url": "Still image URL (e.g. http://...)", "stream_source": "Stream source URL (e.g. rtsp://...)", - "rtsp_transport": "RTSP transport protocol", - "authentication": "Authentication", - "limit_refetch_to_url_change": "Limit refetch to URL change", "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]", - "framerate": "Frame rate (Hz)", - "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + "username": "[%key:common::config_flow::data::username%]" + }, + "sections": { + "advanced": { + "name": "Advanced settings", + "description": "Advanced settings are only needed for special cases. Leave them unchanged unless you know what you are doing.", + "data": { + "rtsp_transport": "RTSP transport protocol", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", + "framerate": "Frame rate (Hz)", + "limit_refetch_to_url_change": "Limit refetch to URL change", + "authentication": "Authentication" + } + } } }, "user_confirm": { @@ -54,17 +62,25 @@ "data": { "still_image_url": "[%key:component::generic::config::step::user::data::still_image_url%]", "stream_source": "[%key:component::generic::config::step::user::data::stream_source%]", - "rtsp_transport": "[%key:component::generic::config::step::user::data::rtsp_transport%]", - "authentication": "[%key:component::generic::config::step::user::data::authentication%]", - "limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]", "password": "[%key:common::config_flow::data::password%]", - "use_wallclock_as_timestamps": "Use wallclock as timestamps", - "username": "[%key:common::config_flow::data::username%]", - "framerate": "[%key:component::generic::config::step::user::data::framerate%]", - "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + "username": "[%key:common::config_flow::data::username%]" }, - "data_description": { - "use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras" + "sections": { + "advanced": { + "name": "[%key:component::generic::config::step::user::sections::advanced::name%]", + "description": "[%key:component::generic::config::step::user::sections::advanced::description%]", + "data": { + "rtsp_transport": "[%key:component::generic::config::step::user::sections::advanced::data::rtsp_transport%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", + "framerate": "[%key:component::generic::config::step::user::sections::advanced::data::framerate%]", + "limit_refetch_to_url_change": "[%key:component::generic::config::step::user::sections::advanced::data::limit_refetch_to_url_change%]", + "authentication": "[%key:component::generic::config::step::user::sections::advanced::data::authentication%]", + "use_wallclock_as_timestamps": "Use wallclock as timestamps" + }, + "data_description": { + "use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras" + } + } } }, "user_confirm": { diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index 96cdfe41d0d..85ba34fde09 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -129,13 +129,15 @@ def config_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: "stream_source": "http://janebloggs:letmein2@example.com/stream", "username": "johnbloggs", "password": "letmein123", - "limit_refetch_to_url_change": False, - "authentication": "basic", - "framerate": 2.0, - "verify_ssl": True, "content_type": "image/jpeg", + "advanced": { + "framerate": 2.0, + "verify_ssl": True, + "limit_refetch_to_url_change": False, + "authentication": "basic", + }, }, - version=1, + version=2, ) entry.add_to_hass(hass) return entry diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 19af6cd7a09..4a08a8ca69f 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Generator import contextlib +from copy import deepcopy import errno from http import HTTPStatus import os.path @@ -24,6 +25,7 @@ from homeassistant.components.generic.const import ( CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, DOMAIN, + SECTION_ADVANCED, ) from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, @@ -49,16 +51,13 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator TESTDATA = { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", - CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_USERNAME: "fred_flintstone", CONF_PASSWORD: "bambam", - CONF_FRAMERATE: 5, - CONF_VERIFY_SSL: False, -} - -TESTDATA_OPTIONS = { - CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, - **TESTDATA, + SECTION_ADVANCED: { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + }, } TESTDATA_YAML = { @@ -114,12 +113,14 @@ async def test_form( assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", - CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_USERNAME: "fred_flintstone", CONF_PASSWORD: "bambam", CONF_CONTENT_TYPE: "image/png", - CONF_FRAMERATE: 5.0, - CONF_VERIFY_SSL: False, + SECTION_ADVANCED: { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_FRAMERATE: 5.0, + CONF_VERIFY_SSL: False, + }, } # Check that the preview image is disabled after. @@ -152,12 +153,14 @@ async def test_form_only_stillimage( assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", - CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_USERNAME: "fred_flintstone", CONF_PASSWORD: "bambam", CONF_CONTENT_TYPE: "image/png", - CONF_FRAMERATE: 5.0, - CONF_VERIFY_SSL: False, + SECTION_ADVANCED: { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_FRAMERATE: 5.0, + CONF_VERIFY_SSL: False, + }, } assert respx.calls.call_count == 1 @@ -385,8 +388,8 @@ async def test_form_rtsp_mode( mock_setup_entry: _patch[MagicMock], ) -> None: """Test we complete ok if the user enters a stream url.""" - data = TESTDATA.copy() - data[CONF_RTSP_TRANSPORT] = "tcp" + data = deepcopy(TESTDATA) + data[SECTION_ADVANCED][CONF_RTSP_TRANSPORT] = "tcp" data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2" result1 = await hass.config_entries.flow.async_configure(user_flow["flow_id"], data) assert result1["type"] is FlowResultType.FORM @@ -399,14 +402,16 @@ async def test_form_rtsp_mode( assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", - CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_STREAM_SOURCE: "rtsp://127.0.0.1/testurl/2", - CONF_RTSP_TRANSPORT: "tcp", CONF_USERNAME: "fred_flintstone", CONF_PASSWORD: "bambam", CONF_CONTENT_TYPE: "image/png", - CONF_FRAMERATE: 5.0, - CONF_VERIFY_SSL: False, + SECTION_ADVANCED: { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_FRAMERATE: 5.0, + CONF_VERIFY_SSL: False, + CONF_RTSP_TRANSPORT: "tcp", + }, } @@ -433,13 +438,15 @@ async def test_form_only_stream( assert result2["title"] == "127_0_0_1" assert result2["options"] == { - CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_STREAM_SOURCE: "rtsp://user:pass@127.0.0.1/testurl/2", CONF_USERNAME: "fred_flintstone", CONF_PASSWORD: "bambam", CONF_CONTENT_TYPE: "image/jpeg", - CONF_FRAMERATE: 5.0, - CONF_VERIFY_SSL: False, + SECTION_ADVANCED: { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_FRAMERATE: 5.0, + CONF_VERIFY_SSL: False, + }, } with patch( @@ -457,9 +464,11 @@ async def test_form_still_and_stream_not_provided( result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], { - CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, - CONF_FRAMERATE: 5, - CONF_VERIFY_SSL: False, + SECTION_ADVANCED: { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + }, }, ) assert result2["type"] is FlowResultType.FORM @@ -879,8 +888,17 @@ async def test_migrate_existing_ids( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test that existing ids are migrated for issue #70568.""" + test_data = { + CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", + CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_FRAMERATE: 5, + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_VERIFY_SSL: False, + } - test_data = TESTDATA_OPTIONS.copy() test_data[CONF_CONTENT_TYPE] = "image/png" old_unique_id = "54321" entity_id = "camera.sample_camera" @@ -926,9 +944,12 @@ async def test_options_use_wallclock_as_timestamps( ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" + + data = deepcopy(TESTDATA) + data[SECTION_ADVANCED][CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True result2 = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, + user_input=data, ) assert result2["type"] is FlowResultType.FORM @@ -958,7 +979,7 @@ async def test_options_use_wallclock_as_timestamps( assert result3["step_id"] == "init" result4 = await hass.config_entries.options.async_configure( result3["flow_id"], - user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, + user_input=data, ) assert result4["type"] is FlowResultType.FORM assert result4["step_id"] == "user_confirm" diff --git a/tests/components/generic/test_diagnostics.py b/tests/components/generic/test_diagnostics.py index 80fa5fd4d4e..afd0bd0c42d 100644 --- a/tests/components/generic/test_diagnostics.py +++ b/tests/components/generic/test_diagnostics.py @@ -26,11 +26,13 @@ async def test_entry_diagnostics( "stream_source": "http://****:****@example.com/****", "username": REDACTED, "password": REDACTED, - "limit_refetch_to_url_change": False, - "authentication": "basic", - "framerate": 2.0, - "verify_ssl": True, "content_type": "image/jpeg", + "advanced": { + "limit_refetch_to_url_change": False, + "authentication": "basic", + "framerate": 2.0, + "verify_ssl": True, + }, }, } diff --git a/tests/components/generic/test_init.py b/tests/components/generic/test_init.py index faa00ee9144..d9a2665c915 100644 --- a/tests/components/generic/test_init.py +++ b/tests/components/generic/test_init.py @@ -2,7 +2,23 @@ import pytest +from homeassistant.components.generic.const import ( + CONF_CONTENT_TYPE, + CONF_FRAMERATE, + CONF_LIMIT_REFETCH_TO_URL_CHANGE, + CONF_STILL_IMAGE_URL, + CONF_STREAM_SOURCE, + DOMAIN, + SECTION_ADVANCED, +) from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, +) from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -35,3 +51,44 @@ async def test_reload_on_title_change( assert ( hass.states.get("camera.test_camera").attributes["friendly_name"] == "New Title" ) + + +@pytest.mark.usefixtures("fakeimg_png") +async def test_migration_to_version_2(hass: HomeAssistant) -> None: + """Test the File sensor with JSON entries.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Test Camera", + unique_id="abc123", + data={}, + options={ + CONF_STILL_IMAGE_URL: "http://joebloggs:letmein1@example.com/secret1/file.jpg?pw=qwerty", + CONF_STREAM_SOURCE: "http://janebloggs:letmein2@example.com/stream", + CONF_USERNAME: "johnbloggs", + CONF_PASSWORD: "letmein123", + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_FRAMERATE: 2.0, + CONF_VERIFY_SSL: True, + CONF_CONTENT_TYPE: "image/jpeg", + }, + version=1, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state is ConfigEntryState.LOADED + assert entry.version == 2 + assert entry.options == { + CONF_STILL_IMAGE_URL: "http://joebloggs:letmein1@example.com/secret1/file.jpg?pw=qwerty", + CONF_STREAM_SOURCE: "http://janebloggs:letmein2@example.com/stream", + CONF_USERNAME: "johnbloggs", + CONF_PASSWORD: "letmein123", + CONF_CONTENT_TYPE: "image/jpeg", + SECTION_ADVANCED: { + CONF_FRAMERATE: 2.0, + CONF_VERIFY_SSL: True, + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + }, + }