From 4b8d8baa693a0c1763d36cda137c6792e24f4f44 Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Date: Sun, 14 Jan 2024 09:36:00 +0100
Subject: [PATCH] Remove deprecated YAML import from generic camera (#107992)

---
 homeassistant/components/generic/camera.py    |  65 +---
 .../components/generic/config_flow.py         |  44 +--
 tests/components/generic/test_camera.py       | 291 ++++++++----------
 tests/components/generic/test_config_flow.py  |  33 +-
 4 files changed, 132 insertions(+), 301 deletions(-)

diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py
index f4c02a2ab9f..171497f479b 100644
--- a/homeassistant/components/generic/camera.py
+++ b/homeassistant/components/generic/camera.py
@@ -8,28 +8,20 @@ import logging
 from typing import Any
 
 import httpx
-import voluptuous as vol
 import yarl
 
-from homeassistant.components.camera import (
-    DEFAULT_CONTENT_TYPE,
-    PLATFORM_SCHEMA,
-    Camera,
-    CameraEntityFeature,
-)
+from homeassistant.components.camera import Camera, CameraEntityFeature
 from homeassistant.components.stream import (
     CONF_RTSP_TRANSPORT,
     CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
-    RTSP_TRANSPORTS,
 )
-from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
     CONF_AUTHENTICATION,
     CONF_NAME,
     CONF_PASSWORD,
     CONF_USERNAME,
     CONF_VERIFY_SSL,
-    HTTP_BASIC_AUTHENTICATION,
     HTTP_DIGEST_AUTHENTICATION,
 )
 from homeassistant.core import HomeAssistant
@@ -38,7 +30,6 @@ from homeassistant.helpers import config_validation as cv, template as template_
 from homeassistant.helpers.device_registry import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.httpx_client import get_async_client
-from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
 from . import DOMAIN
 from .const import (
@@ -47,64 +38,12 @@ from .const import (
     CONF_LIMIT_REFETCH_TO_URL_CHANGE,
     CONF_STILL_IMAGE_URL,
     CONF_STREAM_SOURCE,
-    DEFAULT_NAME,
     GET_IMAGE_TIMEOUT,
 )
 
 _LOGGER = logging.getLogger(__name__)
 
 
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
-    {
-        vol.Required(vol.Any(CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE)): cv.template,
-        vol.Optional(vol.Any(CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE)): cv.template,
-        vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In(
-            [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
-        ),
-        vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean,
-        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
-        vol.Optional(CONF_PASSWORD): cv.string,
-        vol.Optional(CONF_USERNAME): cv.string,
-        vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
-        vol.Optional(CONF_FRAMERATE, default=2): vol.Any(
-            cv.small_float, cv.positive_int
-        ),
-        vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
-        vol.Optional(CONF_RTSP_TRANSPORT): vol.In(RTSP_TRANSPORTS),
-    }
-)
-
-
-async def async_setup_platform(
-    hass: HomeAssistant,
-    config: ConfigType,
-    async_add_entities: AddEntitiesCallback,
-    discovery_info: DiscoveryInfoType | None = None,
-) -> None:
-    """Set up a generic IP Camera."""
-
-    image = config.get(CONF_STILL_IMAGE_URL)
-    stream = config.get(CONF_STREAM_SOURCE)
-    config_new = {
-        CONF_NAME: config[CONF_NAME],
-        CONF_STILL_IMAGE_URL: image.template if image is not None else None,
-        CONF_STREAM_SOURCE: stream.template if stream is not None else None,
-        CONF_AUTHENTICATION: config.get(CONF_AUTHENTICATION),
-        CONF_USERNAME: config.get(CONF_USERNAME),
-        CONF_PASSWORD: config.get(CONF_PASSWORD),
-        CONF_LIMIT_REFETCH_TO_URL_CHANGE: config.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE),
-        CONF_CONTENT_TYPE: config.get(CONF_CONTENT_TYPE),
-        CONF_FRAMERATE: config.get(CONF_FRAMERATE),
-        CONF_VERIFY_SSL: config.get(CONF_VERIFY_SSL),
-    }
-
-    hass.async_create_task(
-        hass.config_entries.flow.async_init(
-            DOMAIN, context={"source": SOURCE_IMPORT}, data=config_new
-        )
-    )
-
-
 async def async_setup_entry(
     hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
 ) -> None:
diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py
index 67ff5a84ed9..af3ff414ac5 100644
--- a/homeassistant/components/generic/config_flow.py
+++ b/homeassistant/components/generic/config_flow.py
@@ -40,12 +40,11 @@ from homeassistant.const import (
     HTTP_BASIC_AUTHENTICATION,
     HTTP_DIGEST_AUTHENTICATION,
 )
-from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
+from homeassistant.core import HomeAssistant
 from homeassistant.data_entry_flow import FlowResult, UnknownFlow
 from homeassistant.exceptions import TemplateError
 from homeassistant.helpers import config_validation as cv, template as template_helper
 from homeassistant.helpers.httpx_client import get_async_client
-from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
 from homeassistant.util import slugify
 
 from .camera import GenericCamera, generate_auth
@@ -379,47 +378,6 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
             errors=None,
         )
 
-    async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
-        """Handle config import from yaml."""
-
-        _LOGGER.warning(
-            "Loading generic IP camera via configuration.yaml is deprecated, "
-            "it will be automatically imported.  Once you have confirmed correct "
-            "operation, please remove 'generic' (IP camera) section(s) from "
-            "configuration.yaml"
-        )
-
-        async_create_issue(
-            self.hass,
-            HOMEASSISTANT_DOMAIN,
-            f"deprecated_yaml_{DOMAIN}",
-            breaks_in_ha_version="2024.2.0",
-            is_fixable=False,
-            issue_domain=DOMAIN,
-            severity=IssueSeverity.WARNING,
-            translation_key="deprecated_yaml",
-            translation_placeholders={
-                "domain": DOMAIN,
-                "integration_title": "Generic IP Camera",
-            },
-        )
-        # abort if we've already got this one.
-        if self.check_for_existing(import_config):
-            return self.async_abort(reason="already_exists")
-        # Don't bother testing the still or stream details on yaml import.
-        still_url = import_config.get(CONF_STILL_IMAGE_URL)
-        stream_url = import_config.get(CONF_STREAM_SOURCE)
-        name = import_config.get(
-            CONF_NAME,
-            slug(self.hass, still_url) or slug(self.hass, stream_url) or DEFAULT_NAME,
-        )
-
-        if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config:
-            import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False
-        still_format = import_config.get(CONF_CONTENT_TYPE, "image/jpeg")
-        import_config[CONF_CONTENT_TYPE] = still_format
-        return self.async_create_entry(title=name, data={}, options=import_config)
-
 
 class GenericOptionsFlowHandler(OptionsFlow):
     """Handle Generic IP Camera options."""
diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py
index 70746f70c9a..5a4bae22e9f 100644
--- a/tests/components/generic/test_camera.py
+++ b/tests/components/generic/test_camera.py
@@ -2,6 +2,7 @@
 import asyncio
 from datetime import timedelta
 from http import HTTPStatus
+from typing import Any
 from unittest.mock import patch
 
 import aiohttp
@@ -11,6 +12,7 @@ import pytest
 import respx
 
 from homeassistant.components.camera import (
+    DEFAULT_CONTENT_TYPE,
     async_get_mjpeg_stream,
     async_get_stream_source,
 )
@@ -24,8 +26,13 @@ from homeassistant.components.generic.const import (
 )
 from homeassistant.components.stream.const import CONF_RTSP_TRANSPORT
 from homeassistant.components.websocket_api.const import TYPE_RESULT
-from homeassistant.config_entries import SOURCE_IMPORT
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL
+from homeassistant.const import (
+    CONF_AUTHENTICATION,
+    CONF_NAME,
+    CONF_PASSWORD,
+    CONF_USERNAME,
+    CONF_VERIFY_SSL,
+)
 from homeassistant.core import HomeAssistant
 from homeassistant.setup import async_setup_component
 
@@ -33,6 +40,34 @@ from tests.common import Mock, MockConfigEntry
 from tests.typing import ClientSessionGenerator, WebSocketGenerator
 
 
+async def help_setup_mock_config_entry(
+    hass: HomeAssistant, options: dict[str, Any], unique_id: Any | None = None
+) -> MockConfigEntry:
+    """Help setting up a generic camera config entry."""
+    entry_options = {
+        CONF_STILL_IMAGE_URL: options.get(CONF_STILL_IMAGE_URL),
+        CONF_STREAM_SOURCE: options.get(CONF_STREAM_SOURCE),
+        CONF_AUTHENTICATION: options.get(CONF_AUTHENTICATION),
+        CONF_USERNAME: options.get(CONF_USERNAME),
+        CONF_PASSWORD: options.get(CONF_PASSWORD),
+        CONF_LIMIT_REFETCH_TO_URL_CHANGE: options.get(
+            CONF_LIMIT_REFETCH_TO_URL_CHANGE, False
+        ),
+        CONF_CONTENT_TYPE: options.get(CONF_CONTENT_TYPE, DEFAULT_CONTENT_TYPE),
+        CONF_FRAMERATE: options.get(CONF_FRAMERATE, 2),
+        CONF_VERIFY_SSL: options.get(CONF_VERIFY_SSL),
+    }
+    entry = MockConfigEntry(
+        domain="generic",
+        title=options[CONF_NAME],
+        options=entry_options,
+        unique_id=unique_id,
+    )
+    entry.add_to_hass(hass)
+    assert await hass.config_entries.async_setup(entry.entry_id)
+    return entry
+
+
 @respx.mock
 async def test_fetching_url(
     hass: HomeAssistant, hass_client: ClientSessionGenerator, fakeimgbytes_png
@@ -40,22 +75,16 @@ async def test_fetching_url(
     """Test that it fetches the given url."""
     respx.get("http://example.com").respond(stream=fakeimgbytes_png)
 
-    await async_setup_component(
-        hass,
-        "camera",
-        {
-            "camera": {
-                "name": "config_test",
-                "platform": "generic",
-                "still_image_url": "http://example.com",
-                "username": "user",
-                "password": "pass",
-                "authentication": "basic",
-                "framerate": 20,
-            }
-        },
-    )
-    await hass.async_block_till_done()
+    options = {
+        "name": "config_test",
+        "platform": "generic",
+        "still_image_url": "http://example.com",
+        "username": "user",
+        "password": "pass",
+        "authentication": "basic",
+        "framerate": 20,
+    }
+    await help_setup_mock_config_entry(hass, options)
 
     client = await hass_client()
 
@@ -84,22 +113,16 @@ async def test_image_caching(
     respx.get("http://example.com").respond(stream=fakeimgbytes_png)
 
     framerate = 5
-    await async_setup_component(
-        hass,
-        "camera",
-        {
-            "camera": {
-                "name": "config_test",
-                "platform": "generic",
-                "still_image_url": "http://example.com",
-                "username": "user",
-                "password": "pass",
-                "authentication": "basic",
-                "framerate": framerate,
-            }
-        },
-    )
-    await hass.async_block_till_done()
+    options = {
+        "name": "config_test",
+        "platform": "generic",
+        "still_image_url": "http://example.com",
+        "username": "user",
+        "password": "pass",
+        "authentication": "basic",
+        "framerate": framerate,
+    }
+    await help_setup_mock_config_entry(hass, options)
 
     client = await hass_client()
 
@@ -154,21 +177,15 @@ async def test_fetching_without_verify_ssl(
     """Test that it fetches the given url when ssl verify is off."""
     respx.get("https://example.com").respond(stream=fakeimgbytes_png)
 
-    await async_setup_component(
-        hass,
-        "camera",
-        {
-            "camera": {
-                "name": "config_test",
-                "platform": "generic",
-                "still_image_url": "https://example.com",
-                "username": "user",
-                "password": "pass",
-                "verify_ssl": "false",
-            }
-        },
-    )
-    await hass.async_block_till_done()
+    options = {
+        "name": "config_test",
+        "platform": "generic",
+        "still_image_url": "https://example.com",
+        "username": "user",
+        "password": "pass",
+        "verify_ssl": "false",
+    }
+    await help_setup_mock_config_entry(hass, options)
 
     client = await hass_client()
 
@@ -184,21 +201,15 @@ async def test_fetching_url_with_verify_ssl(
     """Test that it fetches the given url when ssl verify is explicitly on."""
     respx.get("https://example.com").respond(stream=fakeimgbytes_png)
 
-    await async_setup_component(
-        hass,
-        "camera",
-        {
-            "camera": {
-                "name": "config_test",
-                "platform": "generic",
-                "still_image_url": "https://example.com",
-                "username": "user",
-                "password": "pass",
-                "verify_ssl": "true",
-            }
-        },
-    )
-    await hass.async_block_till_done()
+    options = {
+        "name": "config_test",
+        "platform": "generic",
+        "still_image_url": "https://example.com",
+        "username": "user",
+        "password": "pass",
+        "verify_ssl": True,
+    }
+    await help_setup_mock_config_entry(hass, options)
 
     client = await hass_client()
 
@@ -223,19 +234,13 @@ async def test_limit_refetch(
 
     hass.states.async_set("sensor.temp", "0")
 
-    await async_setup_component(
-        hass,
-        "camera",
-        {
-            "camera": {
-                "name": "config_test",
-                "platform": "generic",
-                "still_image_url": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
-                "limit_refetch_to_url_change": True,
-            }
-        },
-    )
-    await hass.async_block_till_done()
+    options = {
+        "name": "config_test",
+        "platform": "generic",
+        "still_image_url": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
+        "limit_refetch_to_url_change": True,
+    }
+    await help_setup_mock_config_entry(hass, options)
 
     client = await hass_client()
 
@@ -350,20 +355,15 @@ async def test_stream_source_error(
     """Test that the stream source has an error."""
     respx.get("http://example.com").respond(stream=fakeimgbytes_png)
 
-    assert await async_setup_component(
-        hass,
-        "camera",
-        {
-            "camera": {
-                "name": "config_test",
-                "platform": "generic",
-                "still_image_url": "http://example.com",
-                # Does not exist
-                "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
-                "limit_refetch_to_url_change": True,
-            },
-        },
-    )
+    options = {
+        "name": "config_test",
+        "platform": "generic",
+        "still_image_url": "http://example.com",
+        # Does not exist
+        "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
+        "limit_refetch_to_url_change": True,
+    }
+    await help_setup_mock_config_entry(hass, options)
     assert await async_setup_component(hass, "stream", {})
     await hass.async_block_till_done()
 
@@ -397,23 +397,17 @@ async def test_setup_alternative_options(
     """Test that the stream source is setup with different config options."""
     respx.get("https://example.com").respond(stream=fakeimgbytes_png)
 
-    assert await async_setup_component(
-        hass,
-        "camera",
-        {
-            "camera": {
-                "name": "config_test",
-                "platform": "generic",
-                "still_image_url": "https://example.com",
-                "authentication": "digest",
-                "username": "user",
-                "password": "pass",
-                "stream_source": "rtsp://example.com:554/rtsp/",
-                "rtsp_transport": "udp",
-            },
-        },
-    )
-    await hass.async_block_till_done()
+    options = {
+        "name": "config_test",
+        "platform": "generic",
+        "still_image_url": "https://example.com",
+        "authentication": "digest",
+        "username": "user",
+        "password": "pass",
+        "stream_source": "rtsp://example.com:554/rtsp/",
+        "rtsp_transport": "udp",
+    }
+    await help_setup_mock_config_entry(hass, options)
     assert hass.states.get("camera.config_test")
 
 
@@ -427,19 +421,13 @@ async def test_no_stream_source(
     """Test a stream request without stream source option set."""
     respx.get("https://example.com").respond(stream=fakeimgbytes_png)
 
-    assert await async_setup_component(
-        hass,
-        "camera",
-        {
-            "camera": {
-                "name": "config_test",
-                "platform": "generic",
-                "still_image_url": "https://example.com",
-                "limit_refetch_to_url_change": True,
-            }
-        },
-    )
-    await hass.async_block_till_done()
+    options = {
+        "name": "config_test",
+        "platform": "generic",
+        "still_image_url": "https://example.com",
+        "limit_refetch_to_url_change": True,
+    }
+    await help_setup_mock_config_entry(hass, options)
 
     with patch(
         "homeassistant.components.camera.Stream.endpoint_url",
@@ -494,22 +482,9 @@ async def test_camera_content_type(
         "framerate": 2,
         "verify_ssl": True,
     }
+    await help_setup_mock_config_entry(hass, cam_config_jpg, unique_id=12345)
+    await help_setup_mock_config_entry(hass, cam_config_svg, unique_id=54321)
 
-    result1 = await hass.config_entries.flow.async_init(
-        "generic",
-        data=cam_config_jpg,
-        context={"source": SOURCE_IMPORT, "unique_id": 12345},
-    )
-    await hass.async_block_till_done()
-    result2 = await hass.config_entries.flow.async_init(
-        "generic",
-        data=cam_config_svg,
-        context={"source": SOURCE_IMPORT, "unique_id": 54321},
-    )
-    await hass.async_block_till_done()
-
-    assert result1["type"] == "create_entry"
-    assert result2["type"] == "create_entry"
     client = await hass_client()
 
     resp_1 = await client.get("/api/camera_proxy/camera.config_test_svg")
@@ -538,21 +513,15 @@ async def test_timeout_cancelled(
 
     respx.get("http://example.com").respond(stream=fakeimgbytes_png)
 
-    await async_setup_component(
-        hass,
-        "camera",
-        {
-            "camera": {
-                "name": "config_test",
-                "platform": "generic",
-                "still_image_url": "http://example.com",
-                "username": "user",
-                "password": "pass",
-                "framerate": 20,
-            }
-        },
-    )
-    await hass.async_block_till_done()
+    options = {
+        "name": "config_test",
+        "platform": "generic",
+        "still_image_url": "http://example.com",
+        "username": "user",
+        "password": "pass",
+        "framerate": 20,
+    }
+    await help_setup_mock_config_entry(hass, options)
 
     client = await hass_client()
 
@@ -589,19 +558,13 @@ async def test_timeout_cancelled(
 async def test_frame_interval_property(hass: HomeAssistant) -> None:
     """Test that the frame interval is calculated and returned correctly."""
 
-    await async_setup_component(
-        hass,
-        "camera",
-        {
-            "camera": {
-                "name": "config_test",
-                "platform": "generic",
-                "stream_source": "rtsp://example.com:554/rtsp/",
-                "framerate": 5,
-            },
-        },
-    )
-    await hass.async_block_till_done()
+    options = {
+        "name": "config_test",
+        "platform": "generic",
+        "stream_source": "rtsp://example.com:554/rtsp/",
+        "framerate": 5,
+    }
+    await help_setup_mock_config_entry(hass, options)
 
     request = Mock()
     with patch(
diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py
index c4d11d4af22..86bd552bcf3 100644
--- a/tests/components/generic/test_config_flow.py
+++ b/tests/components/generic/test_config_flow.py
@@ -34,9 +34,9 @@ from homeassistant.const import (
     CONF_VERIFY_SSL,
     HTTP_BASIC_AUTHENTICATION,
 )
-from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
+from homeassistant.core import HomeAssistant
 from homeassistant.data_entry_flow import FlowResultType
-from homeassistant.helpers import entity_registry as er, issue_registry as ir
+from homeassistant.helpers import entity_registry as er
 
 from tests.common import MockConfigEntry
 from tests.typing import ClientSessionGenerator
@@ -756,35 +756,6 @@ async def test_options_only_stream(
     assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg"
 
 
-# These below can be deleted after deprecation period is finished.
-@respx.mock
-async def test_import(hass: HomeAssistant, fakeimg_png) -> None:
-    """Test configuration.yaml import used during migration."""
-    result = await hass.config_entries.flow.async_init(
-        DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML
-    )
-    # duplicate import should be aborted
-    result2 = await hass.config_entries.flow.async_init(
-        DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML
-    )
-    assert result["type"] == FlowResultType.CREATE_ENTRY
-    assert result["title"] == "Yaml Defined Name"
-    await hass.async_block_till_done()
-
-    issue_registry = ir.async_get(hass)
-    issue = issue_registry.async_get_issue(
-        HOMEASSISTANT_DOMAIN, "deprecated_yaml_generic"
-    )
-    assert issue.translation_key == "deprecated_yaml"
-
-    # Any name defined in yaml should end up as the entity id.
-    assert hass.states.get("camera.yaml_defined_name")
-    assert result2["type"] == FlowResultType.ABORT
-
-
-# These above can be deleted after deprecation period is finished.
-
-
 async def test_unload_entry(hass: HomeAssistant, fakeimg_png) -> None:
     """Test unloading the generic IP Camera entry."""
     mock_entry = MockConfigEntry(domain=DOMAIN, options=TESTDATA)
-- 
GitLab