mirror of
https://github.com/home-assistant/core.git
synced 2025-07-31 17:18:23 +00:00
Add advanced section for generic camera config flow
This commit is contained in:
parent
a35299d94c
commit
c5a0eec441
@ -2,15 +2,23 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Any
|
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.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.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from .const import CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, SECTION_ADVANCED
|
||||||
|
|
||||||
DOMAIN = "generic"
|
DOMAIN = "generic"
|
||||||
PLATFORMS = [Platform.CAMERA]
|
PLATFORMS = [Platform.CAMERA]
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
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."""
|
"""Unload a config entry."""
|
||||||
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
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
|
||||||
|
@ -41,6 +41,7 @@ from .const import (
|
|||||||
CONF_STILL_IMAGE_URL,
|
CONF_STILL_IMAGE_URL,
|
||||||
CONF_STREAM_SOURCE,
|
CONF_STREAM_SOURCE,
|
||||||
GET_IMAGE_TIMEOUT,
|
GET_IMAGE_TIMEOUT,
|
||||||
|
SECTION_ADVANCED,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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."""
|
"""Generate httpx.Auth object from credentials."""
|
||||||
username: str | None = device_info.get(CONF_USERNAME)
|
username: str | None = device_info.get(CONF_USERNAME)
|
||||||
password: str | None = device_info.get(CONF_PASSWORD)
|
password: str | None = device_info.get(CONF_PASSWORD)
|
||||||
authentication = device_info.get(CONF_AUTHENTICATION)
|
|
||||||
if username and password:
|
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.DigestAuth(username=username, password=password)
|
||||||
return httpx.BasicAuth(username=username, password=password)
|
return httpx.BasicAuth(username=username, password=password)
|
||||||
return None
|
return None
|
||||||
@ -99,14 +102,16 @@ class GenericCamera(Camera):
|
|||||||
if self._stream_source:
|
if self._stream_source:
|
||||||
self._stream_source = Template(self._stream_source, hass)
|
self._stream_source = Template(self._stream_source, hass)
|
||||||
self._attr_supported_features = CameraEntityFeature.STREAM
|
self._attr_supported_features = CameraEntityFeature.STREAM
|
||||||
self._limit_refetch = device_info.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False)
|
self._limit_refetch = device_info[SECTION_ADVANCED].get(
|
||||||
self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE]
|
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.content_type = device_info[CONF_CONTENT_TYPE]
|
||||||
self.verify_ssl = device_info[CONF_VERIFY_SSL]
|
self.verify_ssl = device_info[SECTION_ADVANCED][CONF_VERIFY_SSL]
|
||||||
if device_info.get(CONF_RTSP_TRANSPORT):
|
if rtsp_transport := device_info[SECTION_ADVANCED].get(CONF_RTSP_TRANSPORT):
|
||||||
self.stream_options[CONF_RTSP_TRANSPORT] = device_info[CONF_RTSP_TRANSPORT]
|
self.stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport
|
||||||
self._auth = generate_auth(device_info)
|
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.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True
|
||||||
|
|
||||||
self._last_url = None
|
self._last_url = None
|
||||||
|
@ -50,10 +50,17 @@ 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 section
|
||||||
from homeassistant.exceptions import HomeAssistantError, TemplateError
|
from homeassistant.exceptions import HomeAssistantError, 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.entity_platform import EntityPlatform
|
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||||
from homeassistant.helpers.httpx_client import get_async_client
|
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.setup import async_prepare_setup_platform
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
@ -68,16 +75,19 @@ from .const import (
|
|||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
GET_IMAGE_TIMEOUT,
|
GET_IMAGE_TIMEOUT,
|
||||||
|
SECTION_ADVANCED,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_DATA = {
|
DEFAULT_DATA = {
|
||||||
CONF_NAME: DEFAULT_NAME,
|
CONF_NAME: DEFAULT_NAME,
|
||||||
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
SECTION_ADVANCED: {
|
||||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE: False,
|
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
||||||
CONF_FRAMERATE: 2,
|
CONF_LIMIT_REFETCH_TO_URL_CHANGE: False,
|
||||||
CONF_VERIFY_SSL: True,
|
CONF_FRAMERATE: 2,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
|
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
|
||||||
@ -94,58 +104,47 @@ class InvalidStreamException(HomeAssistantError):
|
|||||||
|
|
||||||
|
|
||||||
def build_schema(
|
def build_schema(
|
||||||
user_input: Mapping[str, Any],
|
|
||||||
is_options_flow: bool = False,
|
is_options_flow: bool = False,
|
||||||
show_advanced_options: bool = False,
|
show_advanced_options: bool = False,
|
||||||
) -> vol.Schema:
|
) -> vol.Schema:
|
||||||
"""Create schema for camera config setup."""
|
"""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 = {
|
spec = {
|
||||||
vol.Optional(
|
vol.Optional(CONF_STREAM_SOURCE): str,
|
||||||
CONF_STILL_IMAGE_URL,
|
vol.Optional(CONF_STILL_IMAGE_URL): str,
|
||||||
description={"suggested_value": user_input.get(CONF_STILL_IMAGE_URL, "")},
|
vol.Optional(CONF_USERNAME): str,
|
||||||
): str,
|
vol.Optional(CONF_PASSWORD): str,
|
||||||
vol.Optional(
|
vol.Required(SECTION_ADVANCED): section(
|
||||||
CONF_STREAM_SOURCE,
|
vol.Schema(advanced_section), {"collapsed": True}
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
if is_options_flow:
|
if is_options_flow:
|
||||||
spec[
|
advanced_section[vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE)] = bool
|
||||||
vol.Required(
|
|
||||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE,
|
|
||||||
default=user_input.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False),
|
|
||||||
)
|
|
||||||
] = bool
|
|
||||||
if show_advanced_options:
|
if show_advanced_options:
|
||||||
spec[
|
advanced_section[vol.Optional(CONF_USE_WALLCLOCK_AS_TIMESTAMPS)] = bool
|
||||||
vol.Required(
|
|
||||||
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
|
|
||||||
default=user_input.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False),
|
|
||||||
)
|
|
||||||
] = bool
|
|
||||||
return vol.Schema(spec)
|
return vol.Schema(spec)
|
||||||
|
|
||||||
|
|
||||||
@ -187,7 +186,7 @@ async def async_test_still(
|
|||||||
return {CONF_STILL_IMAGE_URL: "malformed_url"}, None
|
return {CONF_STILL_IMAGE_URL: "malformed_url"}, None
|
||||||
if not yarl_url.is_absolute():
|
if not yarl_url.is_absolute():
|
||||||
return {CONF_STILL_IMAGE_URL: "relative_url"}, None
|
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)
|
auth = generate_auth(info)
|
||||||
try:
|
try:
|
||||||
async_client = get_async_client(hass, verify_ssl=verify_ssl)
|
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)
|
_LOGGER.warning("Problem rendering template %s: %s", stream_source, err)
|
||||||
raise InvalidStreamException("template_error") from err
|
raise InvalidStreamException("template_error") from err
|
||||||
stream_options: dict[str, str | bool | float] = {}
|
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
|
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
|
stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -324,7 +323,7 @@ def register_still_preview(hass: HomeAssistant) -> None:
|
|||||||
class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Config flow for generic IP camera."""
|
"""Config flow for generic IP camera."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 2
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize Generic ConfigFlow."""
|
"""Initialize Generic ConfigFlow."""
|
||||||
@ -379,7 +378,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
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=self.add_suggested_values_to_schema(build_schema(), user_input),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -447,13 +446,19 @@ class GenericOptionsFlowHandler(OptionsFlow):
|
|||||||
self.preview_stream = None
|
self.preview_stream = None
|
||||||
if not errors:
|
if not errors:
|
||||||
data = {
|
data = {
|
||||||
CONF_USE_WALLCLOCK_AS_TIMESTAMPS: self.config_entry.options.get(
|
|
||||||
CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False
|
|
||||||
),
|
|
||||||
**user_input,
|
**user_input,
|
||||||
CONF_CONTENT_TYPE: still_format
|
CONF_CONTENT_TYPE: still_format
|
||||||
or self.config_entry.options.get(CONF_CONTENT_TYPE),
|
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
|
self.user_input = data
|
||||||
# temporary preview for user to check the image
|
# temporary preview for user to check the image
|
||||||
self.preview_image_settings = data
|
self.preview_image_settings = data
|
||||||
@ -462,10 +467,12 @@ class GenericOptionsFlowHandler(OptionsFlow):
|
|||||||
user_input = self.user_input
|
user_input = self.user_input
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init",
|
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,
|
user_input or self.config_entry.options,
|
||||||
True,
|
|
||||||
self.show_advanced_options,
|
|
||||||
),
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
@ -9,3 +9,4 @@ CONF_STILL_IMAGE_URL = "still_image_url"
|
|||||||
CONF_STREAM_SOURCE = "stream_source"
|
CONF_STREAM_SOURCE = "stream_source"
|
||||||
CONF_FRAMERATE = "framerate"
|
CONF_FRAMERATE = "framerate"
|
||||||
GET_IMAGE_TIMEOUT = 10
|
GET_IMAGE_TIMEOUT = 10
|
||||||
|
SECTION_ADVANCED = "advanced"
|
||||||
|
@ -30,13 +30,21 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"still_image_url": "Still image URL (e.g. http://...)",
|
"still_image_url": "Still image URL (e.g. http://...)",
|
||||||
"stream_source": "Stream source URL (e.g. rtsp://...)",
|
"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%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
"framerate": "Frame rate (Hz)",
|
},
|
||||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
"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": {
|
"user_confirm": {
|
||||||
@ -54,17 +62,25 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"still_image_url": "[%key:component::generic::config::step::user::data::still_image_url%]",
|
"still_image_url": "[%key:component::generic::config::step::user::data::still_image_url%]",
|
||||||
"stream_source": "[%key:component::generic::config::step::user::data::stream_source%]",
|
"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%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"use_wallclock_as_timestamps": "Use wallclock as timestamps",
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
"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%]"
|
|
||||||
},
|
},
|
||||||
"data_description": {
|
"sections": {
|
||||||
"use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras"
|
"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": {
|
"user_confirm": {
|
||||||
|
@ -129,13 +129,15 @@ def config_entry_fixture(hass: HomeAssistant) -> MockConfigEntry:
|
|||||||
"stream_source": "http://janebloggs:letmein2@example.com/stream",
|
"stream_source": "http://janebloggs:letmein2@example.com/stream",
|
||||||
"username": "johnbloggs",
|
"username": "johnbloggs",
|
||||||
"password": "letmein123",
|
"password": "letmein123",
|
||||||
"limit_refetch_to_url_change": False,
|
|
||||||
"authentication": "basic",
|
|
||||||
"framerate": 2.0,
|
|
||||||
"verify_ssl": True,
|
|
||||||
"content_type": "image/jpeg",
|
"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)
|
entry.add_to_hass(hass)
|
||||||
return entry
|
return entry
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from copy import deepcopy
|
||||||
import errno
|
import errno
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import os.path
|
import os.path
|
||||||
@ -24,6 +25,7 @@ from homeassistant.components.generic.const import (
|
|||||||
CONF_STILL_IMAGE_URL,
|
CONF_STILL_IMAGE_URL,
|
||||||
CONF_STREAM_SOURCE,
|
CONF_STREAM_SOURCE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
SECTION_ADVANCED,
|
||||||
)
|
)
|
||||||
from homeassistant.components.stream import (
|
from homeassistant.components.stream import (
|
||||||
CONF_RTSP_TRANSPORT,
|
CONF_RTSP_TRANSPORT,
|
||||||
@ -49,16 +51,13 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
|||||||
TESTDATA = {
|
TESTDATA = {
|
||||||
CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1",
|
CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1",
|
||||||
CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2",
|
CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2",
|
||||||
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
|
||||||
CONF_USERNAME: "fred_flintstone",
|
CONF_USERNAME: "fred_flintstone",
|
||||||
CONF_PASSWORD: "bambam",
|
CONF_PASSWORD: "bambam",
|
||||||
CONF_FRAMERATE: 5,
|
SECTION_ADVANCED: {
|
||||||
CONF_VERIFY_SSL: False,
|
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
||||||
}
|
CONF_FRAMERATE: 5,
|
||||||
|
CONF_VERIFY_SSL: False,
|
||||||
TESTDATA_OPTIONS = {
|
},
|
||||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE: False,
|
|
||||||
**TESTDATA,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TESTDATA_YAML = {
|
TESTDATA_YAML = {
|
||||||
@ -114,12 +113,14 @@ async def test_form(
|
|||||||
assert result2["options"] == {
|
assert result2["options"] == {
|
||||||
CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1",
|
CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1",
|
||||||
CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2",
|
CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2",
|
||||||
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
|
||||||
CONF_USERNAME: "fred_flintstone",
|
CONF_USERNAME: "fred_flintstone",
|
||||||
CONF_PASSWORD: "bambam",
|
CONF_PASSWORD: "bambam",
|
||||||
CONF_CONTENT_TYPE: "image/png",
|
CONF_CONTENT_TYPE: "image/png",
|
||||||
CONF_FRAMERATE: 5.0,
|
SECTION_ADVANCED: {
|
||||||
CONF_VERIFY_SSL: False,
|
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
||||||
|
CONF_FRAMERATE: 5.0,
|
||||||
|
CONF_VERIFY_SSL: False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check that the preview image is disabled after.
|
# 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["title"] == "127_0_0_1"
|
||||||
assert result2["options"] == {
|
assert result2["options"] == {
|
||||||
CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1",
|
CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1",
|
||||||
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
|
||||||
CONF_USERNAME: "fred_flintstone",
|
CONF_USERNAME: "fred_flintstone",
|
||||||
CONF_PASSWORD: "bambam",
|
CONF_PASSWORD: "bambam",
|
||||||
CONF_CONTENT_TYPE: "image/png",
|
CONF_CONTENT_TYPE: "image/png",
|
||||||
CONF_FRAMERATE: 5.0,
|
SECTION_ADVANCED: {
|
||||||
CONF_VERIFY_SSL: False,
|
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
||||||
|
CONF_FRAMERATE: 5.0,
|
||||||
|
CONF_VERIFY_SSL: False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
assert respx.calls.call_count == 1
|
assert respx.calls.call_count == 1
|
||||||
@ -385,8 +388,8 @@ async def test_form_rtsp_mode(
|
|||||||
mock_setup_entry: _patch[MagicMock],
|
mock_setup_entry: _patch[MagicMock],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we complete ok if the user enters a stream url."""
|
"""Test we complete ok if the user enters a stream url."""
|
||||||
data = TESTDATA.copy()
|
data = deepcopy(TESTDATA)
|
||||||
data[CONF_RTSP_TRANSPORT] = "tcp"
|
data[SECTION_ADVANCED][CONF_RTSP_TRANSPORT] = "tcp"
|
||||||
data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2"
|
data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2"
|
||||||
result1 = await hass.config_entries.flow.async_configure(user_flow["flow_id"], data)
|
result1 = await hass.config_entries.flow.async_configure(user_flow["flow_id"], data)
|
||||||
assert result1["type"] is FlowResultType.FORM
|
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["title"] == "127_0_0_1"
|
||||||
assert result2["options"] == {
|
assert result2["options"] == {
|
||||||
CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1",
|
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_STREAM_SOURCE: "rtsp://127.0.0.1/testurl/2",
|
||||||
CONF_RTSP_TRANSPORT: "tcp",
|
|
||||||
CONF_USERNAME: "fred_flintstone",
|
CONF_USERNAME: "fred_flintstone",
|
||||||
CONF_PASSWORD: "bambam",
|
CONF_PASSWORD: "bambam",
|
||||||
CONF_CONTENT_TYPE: "image/png",
|
CONF_CONTENT_TYPE: "image/png",
|
||||||
CONF_FRAMERATE: 5.0,
|
SECTION_ADVANCED: {
|
||||||
CONF_VERIFY_SSL: False,
|
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["title"] == "127_0_0_1"
|
||||||
assert result2["options"] == {
|
assert result2["options"] == {
|
||||||
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
|
||||||
CONF_STREAM_SOURCE: "rtsp://user:pass@127.0.0.1/testurl/2",
|
CONF_STREAM_SOURCE: "rtsp://user:pass@127.0.0.1/testurl/2",
|
||||||
CONF_USERNAME: "fred_flintstone",
|
CONF_USERNAME: "fred_flintstone",
|
||||||
CONF_PASSWORD: "bambam",
|
CONF_PASSWORD: "bambam",
|
||||||
CONF_CONTENT_TYPE: "image/jpeg",
|
CONF_CONTENT_TYPE: "image/jpeg",
|
||||||
CONF_FRAMERATE: 5.0,
|
SECTION_ADVANCED: {
|
||||||
CONF_VERIFY_SSL: False,
|
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
||||||
|
CONF_FRAMERATE: 5.0,
|
||||||
|
CONF_VERIFY_SSL: False,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
@ -457,9 +464,11 @@ async def test_form_still_and_stream_not_provided(
|
|||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
user_flow["flow_id"],
|
user_flow["flow_id"],
|
||||||
{
|
{
|
||||||
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
SECTION_ADVANCED: {
|
||||||
CONF_FRAMERATE: 5,
|
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
|
||||||
CONF_VERIFY_SSL: False,
|
CONF_FRAMERATE: 5,
|
||||||
|
CONF_VERIFY_SSL: False,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert result2["type"] is FlowResultType.FORM
|
assert result2["type"] is FlowResultType.FORM
|
||||||
@ -879,8 +888,17 @@ async def test_migrate_existing_ids(
|
|||||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that existing ids are migrated for issue #70568."""
|
"""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"
|
test_data[CONF_CONTENT_TYPE] = "image/png"
|
||||||
old_unique_id = "54321"
|
old_unique_id = "54321"
|
||||||
entity_id = "camera.sample_camera"
|
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["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "init"
|
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(
|
result2 = await hass.config_entries.options.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA},
|
user_input=data,
|
||||||
)
|
)
|
||||||
assert result2["type"] is FlowResultType.FORM
|
assert result2["type"] is FlowResultType.FORM
|
||||||
|
|
||||||
@ -958,7 +979,7 @@ async def test_options_use_wallclock_as_timestamps(
|
|||||||
assert result3["step_id"] == "init"
|
assert result3["step_id"] == "init"
|
||||||
result4 = await hass.config_entries.options.async_configure(
|
result4 = await hass.config_entries.options.async_configure(
|
||||||
result3["flow_id"],
|
result3["flow_id"],
|
||||||
user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA},
|
user_input=data,
|
||||||
)
|
)
|
||||||
assert result4["type"] is FlowResultType.FORM
|
assert result4["type"] is FlowResultType.FORM
|
||||||
assert result4["step_id"] == "user_confirm"
|
assert result4["step_id"] == "user_confirm"
|
||||||
|
@ -26,11 +26,13 @@ async def test_entry_diagnostics(
|
|||||||
"stream_source": "http://****:****@example.com/****",
|
"stream_source": "http://****:****@example.com/****",
|
||||||
"username": REDACTED,
|
"username": REDACTED,
|
||||||
"password": REDACTED,
|
"password": REDACTED,
|
||||||
"limit_refetch_to_url_change": False,
|
|
||||||
"authentication": "basic",
|
|
||||||
"framerate": 2.0,
|
|
||||||
"verify_ssl": True,
|
|
||||||
"content_type": "image/jpeg",
|
"content_type": "image/jpeg",
|
||||||
|
"advanced": {
|
||||||
|
"limit_refetch_to_url_change": False,
|
||||||
|
"authentication": "basic",
|
||||||
|
"framerate": 2.0,
|
||||||
|
"verify_ssl": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,23 @@
|
|||||||
|
|
||||||
import pytest
|
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.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 homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
@ -35,3 +51,44 @@ async def test_reload_on_title_change(
|
|||||||
assert (
|
assert (
|
||||||
hass.states.get("camera.test_camera").attributes["friendly_name"] == "New Title"
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user