Remove deprecated YAML import from generic camera (#107992)

This commit is contained in:
Jan Bouwhuis 2024-01-14 09:36:00 +01:00 committed by GitHub
parent f48d057307
commit 4b8d8baa69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 132 additions and 301 deletions

View File

@ -8,28 +8,20 @@ import logging
from typing import Any from typing import Any
import httpx import httpx
import voluptuous as vol
import yarl import yarl
from homeassistant.components.camera import ( from homeassistant.components.camera import Camera, CameraEntityFeature
DEFAULT_CONTENT_TYPE,
PLATFORM_SCHEMA,
Camera,
CameraEntityFeature,
)
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,
RTSP_TRANSPORTS,
) )
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_AUTHENTICATION, CONF_AUTHENTICATION,
CONF_NAME, CONF_NAME,
CONF_PASSWORD, CONF_PASSWORD,
CONF_USERNAME, CONF_USERNAME,
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION,
) )
from homeassistant.core import HomeAssistant 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.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN from . import DOMAIN
from .const import ( from .const import (
@ -47,64 +38,12 @@ from .const import (
CONF_LIMIT_REFETCH_TO_URL_CHANGE, CONF_LIMIT_REFETCH_TO_URL_CHANGE,
CONF_STILL_IMAGE_URL, CONF_STILL_IMAGE_URL,
CONF_STREAM_SOURCE, CONF_STREAM_SOURCE,
DEFAULT_NAME,
GET_IMAGE_TIMEOUT, GET_IMAGE_TIMEOUT,
) )
_LOGGER = logging.getLogger(__name__) _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( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:

View File

@ -40,12 +40,11 @@ from homeassistant.const import (
HTTP_BASIC_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_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.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.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.util import slugify from homeassistant.util import slugify
from .camera import GenericCamera, generate_auth from .camera import GenericCamera, generate_auth
@ -379,47 +378,6 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
errors=None, 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): class GenericOptionsFlowHandler(OptionsFlow):
"""Handle Generic IP Camera options.""" """Handle Generic IP Camera options."""

View File

@ -2,6 +2,7 @@
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
from http import HTTPStatus from http import HTTPStatus
from typing import Any
from unittest.mock import patch from unittest.mock import patch
import aiohttp import aiohttp
@ -11,6 +12,7 @@ import pytest
import respx import respx
from homeassistant.components.camera import ( from homeassistant.components.camera import (
DEFAULT_CONTENT_TYPE,
async_get_mjpeg_stream, async_get_mjpeg_stream,
async_get_stream_source, 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.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.const import (
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL CONF_AUTHENTICATION,
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -33,6 +40,34 @@ from tests.common import Mock, MockConfigEntry
from tests.typing import ClientSessionGenerator, WebSocketGenerator 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 @respx.mock
async def test_fetching_url( async def test_fetching_url(
hass: HomeAssistant, hass_client: ClientSessionGenerator, fakeimgbytes_png hass: HomeAssistant, hass_client: ClientSessionGenerator, fakeimgbytes_png
@ -40,22 +75,16 @@ async def test_fetching_url(
"""Test that it fetches the given url.""" """Test that it fetches the given url."""
respx.get("http://example.com").respond(stream=fakeimgbytes_png) respx.get("http://example.com").respond(stream=fakeimgbytes_png)
await async_setup_component( options = {
hass, "name": "config_test",
"camera", "platform": "generic",
{ "still_image_url": "http://example.com",
"camera": { "username": "user",
"name": "config_test", "password": "pass",
"platform": "generic", "authentication": "basic",
"still_image_url": "http://example.com", "framerate": 20,
"username": "user", }
"password": "pass", await help_setup_mock_config_entry(hass, options)
"authentication": "basic",
"framerate": 20,
}
},
)
await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
@ -84,22 +113,16 @@ async def test_image_caching(
respx.get("http://example.com").respond(stream=fakeimgbytes_png) respx.get("http://example.com").respond(stream=fakeimgbytes_png)
framerate = 5 framerate = 5
await async_setup_component( options = {
hass, "name": "config_test",
"camera", "platform": "generic",
{ "still_image_url": "http://example.com",
"camera": { "username": "user",
"name": "config_test", "password": "pass",
"platform": "generic", "authentication": "basic",
"still_image_url": "http://example.com", "framerate": framerate,
"username": "user", }
"password": "pass", await help_setup_mock_config_entry(hass, options)
"authentication": "basic",
"framerate": framerate,
}
},
)
await hass.async_block_till_done()
client = await hass_client() 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.""" """Test that it fetches the given url when ssl verify is off."""
respx.get("https://example.com").respond(stream=fakeimgbytes_png) respx.get("https://example.com").respond(stream=fakeimgbytes_png)
await async_setup_component( options = {
hass, "name": "config_test",
"camera", "platform": "generic",
{ "still_image_url": "https://example.com",
"camera": { "username": "user",
"name": "config_test", "password": "pass",
"platform": "generic", "verify_ssl": "false",
"still_image_url": "https://example.com", }
"username": "user", await help_setup_mock_config_entry(hass, options)
"password": "pass",
"verify_ssl": "false",
}
},
)
await hass.async_block_till_done()
client = await hass_client() 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.""" """Test that it fetches the given url when ssl verify is explicitly on."""
respx.get("https://example.com").respond(stream=fakeimgbytes_png) respx.get("https://example.com").respond(stream=fakeimgbytes_png)
await async_setup_component( options = {
hass, "name": "config_test",
"camera", "platform": "generic",
{ "still_image_url": "https://example.com",
"camera": { "username": "user",
"name": "config_test", "password": "pass",
"platform": "generic", "verify_ssl": True,
"still_image_url": "https://example.com", }
"username": "user", await help_setup_mock_config_entry(hass, options)
"password": "pass",
"verify_ssl": "true",
}
},
)
await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
@ -223,19 +234,13 @@ async def test_limit_refetch(
hass.states.async_set("sensor.temp", "0") hass.states.async_set("sensor.temp", "0")
await async_setup_component( options = {
hass, "name": "config_test",
"camera", "platform": "generic",
{ "still_image_url": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
"camera": { "limit_refetch_to_url_change": True,
"name": "config_test", }
"platform": "generic", await help_setup_mock_config_entry(hass, options)
"still_image_url": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
"limit_refetch_to_url_change": True,
}
},
)
await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
@ -350,20 +355,15 @@ async def test_stream_source_error(
"""Test that the stream source has an error.""" """Test that the stream source has an error."""
respx.get("http://example.com").respond(stream=fakeimgbytes_png) respx.get("http://example.com").respond(stream=fakeimgbytes_png)
assert await async_setup_component( options = {
hass, "name": "config_test",
"camera", "platform": "generic",
{ "still_image_url": "http://example.com",
"camera": { # Does not exist
"name": "config_test", "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
"platform": "generic", "limit_refetch_to_url_change": True,
"still_image_url": "http://example.com", }
# Does not exist await help_setup_mock_config_entry(hass, options)
"stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
"limit_refetch_to_url_change": True,
},
},
)
assert await async_setup_component(hass, "stream", {}) assert await async_setup_component(hass, "stream", {})
await hass.async_block_till_done() 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.""" """Test that the stream source is setup with different config options."""
respx.get("https://example.com").respond(stream=fakeimgbytes_png) respx.get("https://example.com").respond(stream=fakeimgbytes_png)
assert await async_setup_component( options = {
hass, "name": "config_test",
"camera", "platform": "generic",
{ "still_image_url": "https://example.com",
"camera": { "authentication": "digest",
"name": "config_test", "username": "user",
"platform": "generic", "password": "pass",
"still_image_url": "https://example.com", "stream_source": "rtsp://example.com:554/rtsp/",
"authentication": "digest", "rtsp_transport": "udp",
"username": "user", }
"password": "pass", await help_setup_mock_config_entry(hass, options)
"stream_source": "rtsp://example.com:554/rtsp/",
"rtsp_transport": "udp",
},
},
)
await hass.async_block_till_done()
assert hass.states.get("camera.config_test") 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.""" """Test a stream request without stream source option set."""
respx.get("https://example.com").respond(stream=fakeimgbytes_png) respx.get("https://example.com").respond(stream=fakeimgbytes_png)
assert await async_setup_component( options = {
hass, "name": "config_test",
"camera", "platform": "generic",
{ "still_image_url": "https://example.com",
"camera": { "limit_refetch_to_url_change": True,
"name": "config_test", }
"platform": "generic", await help_setup_mock_config_entry(hass, options)
"still_image_url": "https://example.com",
"limit_refetch_to_url_change": True,
}
},
)
await hass.async_block_till_done()
with patch( with patch(
"homeassistant.components.camera.Stream.endpoint_url", "homeassistant.components.camera.Stream.endpoint_url",
@ -494,22 +482,9 @@ async def test_camera_content_type(
"framerate": 2, "framerate": 2,
"verify_ssl": True, "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() client = await hass_client()
resp_1 = await client.get("/api/camera_proxy/camera.config_test_svg") 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) respx.get("http://example.com").respond(stream=fakeimgbytes_png)
await async_setup_component( options = {
hass, "name": "config_test",
"camera", "platform": "generic",
{ "still_image_url": "http://example.com",
"camera": { "username": "user",
"name": "config_test", "password": "pass",
"platform": "generic", "framerate": 20,
"still_image_url": "http://example.com", }
"username": "user", await help_setup_mock_config_entry(hass, options)
"password": "pass",
"framerate": 20,
}
},
)
await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
@ -589,19 +558,13 @@ async def test_timeout_cancelled(
async def test_frame_interval_property(hass: HomeAssistant) -> None: async def test_frame_interval_property(hass: HomeAssistant) -> None:
"""Test that the frame interval is calculated and returned correctly.""" """Test that the frame interval is calculated and returned correctly."""
await async_setup_component( options = {
hass, "name": "config_test",
"camera", "platform": "generic",
{ "stream_source": "rtsp://example.com:554/rtsp/",
"camera": { "framerate": 5,
"name": "config_test", }
"platform": "generic", await help_setup_mock_config_entry(hass, options)
"stream_source": "rtsp://example.com:554/rtsp/",
"framerate": 5,
},
},
)
await hass.async_block_till_done()
request = Mock() request = Mock()
with patch( with patch(

View File

@ -34,9 +34,9 @@ from homeassistant.const import (
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
HTTP_BASIC_AUTHENTICATION, 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.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.common import MockConfigEntry
from tests.typing import ClientSessionGenerator from tests.typing import ClientSessionGenerator
@ -756,35 +756,6 @@ async def test_options_only_stream(
assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" 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: async def test_unload_entry(hass: HomeAssistant, fakeimg_png) -> None:
"""Test unloading the generic IP Camera entry.""" """Test unloading the generic IP Camera entry."""
mock_entry = MockConfigEntry(domain=DOMAIN, options=TESTDATA) mock_entry = MockConfigEntry(domain=DOMAIN, options=TESTDATA)