From 4b8d8baa693a0c1763d36cda137c6792e24f4f44 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis 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)