Skip to content

Commit 67aeafa

Browse files
authored
Add advanced section for generic camera config flow (home-assistant#148430)
1 parent 3d71b6d commit 67aeafa

9 files changed

Lines changed: 276 additions & 123 deletions

File tree

homeassistant/components/generic/__init__.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22

33
from __future__ import annotations
44

5+
import logging
56
from typing import Any
67

8+
from homeassistant.components.stream import (
9+
CONF_RTSP_TRANSPORT,
10+
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
11+
)
712
from homeassistant.config_entries import ConfigEntry
8-
from homeassistant.const import Platform
13+
from homeassistant.const import CONF_AUTHENTICATION, CONF_VERIFY_SSL, Platform
914
from homeassistant.core import HomeAssistant, callback
1015
from homeassistant.helpers import entity_registry as er
1116

17+
from .const import CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, SECTION_ADVANCED
18+
1219
DOMAIN = "generic"
1320
PLATFORMS = [Platform.CAMERA]
21+
_LOGGER = logging.getLogger(__name__)
1422

1523

1624
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
@@ -47,3 +55,38 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
4755
"""Unload a config entry."""
4856

4957
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
58+
59+
60+
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
61+
"""Migrate entry."""
62+
_LOGGER.debug("Migrating from version %s:%s", entry.version, entry.minor_version)
63+
64+
if entry.version > 2:
65+
# This means the user has downgraded from a future version
66+
return False
67+
68+
if entry.version == 1:
69+
# Migrate to advanced section
70+
new_options = {**entry.options}
71+
advanced = new_options[SECTION_ADVANCED] = {
72+
CONF_FRAMERATE: new_options.pop(CONF_FRAMERATE),
73+
CONF_VERIFY_SSL: new_options.pop(CONF_VERIFY_SSL),
74+
}
75+
76+
# migrate optional fields
77+
for key in (
78+
CONF_RTSP_TRANSPORT,
79+
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
80+
CONF_AUTHENTICATION,
81+
CONF_LIMIT_REFETCH_TO_URL_CHANGE,
82+
):
83+
if key in new_options:
84+
advanced[key] = new_options.pop(key)
85+
86+
hass.config_entries.async_update_entry(entry, options=new_options, version=2)
87+
88+
_LOGGER.debug(
89+
"Migration to version %s:%s successful", entry.version, entry.minor_version
90+
)
91+
92+
return True

homeassistant/components/generic/camera.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
CONF_STILL_IMAGE_URL,
4242
CONF_STREAM_SOURCE,
4343
GET_IMAGE_TIMEOUT,
44+
SECTION_ADVANCED,
4445
)
4546

4647
_LOGGER = logging.getLogger(__name__)
@@ -62,9 +63,11 @@ def generate_auth(device_info: Mapping[str, Any]) -> httpx.Auth | None:
6263
"""Generate httpx.Auth object from credentials."""
6364
username: str | None = device_info.get(CONF_USERNAME)
6465
password: str | None = device_info.get(CONF_PASSWORD)
65-
authentication = device_info.get(CONF_AUTHENTICATION)
6666
if username and password:
67-
if authentication == HTTP_DIGEST_AUTHENTICATION:
67+
if (
68+
device_info[SECTION_ADVANCED].get(CONF_AUTHENTICATION)
69+
== HTTP_DIGEST_AUTHENTICATION
70+
):
6871
return httpx.DigestAuth(username=username, password=password)
6972
return httpx.BasicAuth(username=username, password=password)
7073
return None
@@ -99,14 +102,16 @@ def __init__(
99102
if self._stream_source:
100103
self._stream_source = Template(self._stream_source, hass)
101104
self._attr_supported_features = CameraEntityFeature.STREAM
102-
self._limit_refetch = device_info.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False)
103-
self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE]
105+
self._limit_refetch = device_info[SECTION_ADVANCED].get(
106+
CONF_LIMIT_REFETCH_TO_URL_CHANGE, False
107+
)
108+
self._attr_frame_interval = 1 / device_info[SECTION_ADVANCED][CONF_FRAMERATE]
104109
self.content_type = device_info[CONF_CONTENT_TYPE]
105-
self.verify_ssl = device_info[CONF_VERIFY_SSL]
106-
if device_info.get(CONF_RTSP_TRANSPORT):
107-
self.stream_options[CONF_RTSP_TRANSPORT] = device_info[CONF_RTSP_TRANSPORT]
110+
self.verify_ssl = device_info[SECTION_ADVANCED][CONF_VERIFY_SSL]
111+
if rtsp_transport := device_info[SECTION_ADVANCED].get(CONF_RTSP_TRANSPORT):
112+
self.stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport
108113
self._auth = generate_auth(device_info)
109-
if device_info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS):
114+
if device_info[SECTION_ADVANCED].get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS):
110115
self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True
111116

112117
self._last_url = None

homeassistant/components/generic/config_flow.py

Lines changed: 67 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,17 @@
5050
HTTP_DIGEST_AUTHENTICATION,
5151
)
5252
from homeassistant.core import HomeAssistant
53+
from homeassistant.data_entry_flow import section
5354
from homeassistant.exceptions import HomeAssistantError, TemplateError
5455
from homeassistant.helpers import config_validation as cv, template as template_helper
5556
from homeassistant.helpers.entity_platform import PlatformData
5657
from homeassistant.helpers.httpx_client import get_async_client
58+
from homeassistant.helpers.selector import (
59+
SelectOptionDict,
60+
SelectSelector,
61+
SelectSelectorConfig,
62+
SelectSelectorMode,
63+
)
5764
from homeassistant.util import slugify
5865

5966
from .camera import GenericCamera, generate_auth
@@ -67,17 +74,20 @@
6774
DEFAULT_NAME,
6875
DOMAIN,
6976
GET_IMAGE_TIMEOUT,
77+
SECTION_ADVANCED,
7078
)
7179

7280
_LOGGER = logging.getLogger(__name__)
7381

7482
DEFAULT_DATA = {
7583
CONF_NAME: DEFAULT_NAME,
76-
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
77-
CONF_LIMIT_REFETCH_TO_URL_CHANGE: False,
78-
CONF_FRAMERATE: 2,
79-
CONF_VERIFY_SSL: True,
80-
CONF_RTSP_TRANSPORT: "tcp",
84+
SECTION_ADVANCED: {
85+
CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION,
86+
CONF_LIMIT_REFETCH_TO_URL_CHANGE: False,
87+
CONF_FRAMERATE: 2,
88+
CONF_VERIFY_SSL: True,
89+
CONF_RTSP_TRANSPORT: "tcp",
90+
},
8191
}
8292

8393
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
@@ -94,58 +104,47 @@ def __init__(self, error: str, details: str | None = None) -> None:
94104

95105

96106
def build_schema(
97-
user_input: Mapping[str, Any],
98107
is_options_flow: bool = False,
99108
show_advanced_options: bool = False,
100109
) -> vol.Schema:
101110
"""Create schema for camera config setup."""
111+
rtsp_options = [
112+
SelectOptionDict(
113+
value=value,
114+
label=name,
115+
)
116+
for value, name in RTSP_TRANSPORTS.items()
117+
]
118+
119+
advanced_section = {
120+
vol.Required(CONF_FRAMERATE): vol.All(
121+
vol.Range(min=0, min_included=False), cv.positive_float
122+
),
123+
vol.Required(CONF_VERIFY_SSL): bool,
124+
vol.Optional(CONF_RTSP_TRANSPORT): SelectSelector(
125+
SelectSelectorConfig(
126+
options=rtsp_options,
127+
mode=SelectSelectorMode.DROPDOWN,
128+
)
129+
),
130+
vol.Optional(CONF_AUTHENTICATION): vol.In(
131+
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
132+
),
133+
}
102134
spec = {
103-
vol.Optional(
104-
CONF_STILL_IMAGE_URL,
105-
description={"suggested_value": user_input.get(CONF_STILL_IMAGE_URL, "")},
106-
): str,
107-
vol.Optional(
108-
CONF_STREAM_SOURCE,
109-
description={"suggested_value": user_input.get(CONF_STREAM_SOURCE, "")},
110-
): str,
111-
vol.Optional(
112-
CONF_RTSP_TRANSPORT,
113-
description={"suggested_value": user_input.get(CONF_RTSP_TRANSPORT)},
114-
): vol.In(RTSP_TRANSPORTS),
115-
vol.Optional(
116-
CONF_AUTHENTICATION,
117-
description={"suggested_value": user_input.get(CONF_AUTHENTICATION)},
118-
): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
119-
vol.Optional(
120-
CONF_USERNAME,
121-
description={"suggested_value": user_input.get(CONF_USERNAME, "")},
122-
): str,
123-
vol.Optional(
124-
CONF_PASSWORD,
125-
description={"suggested_value": user_input.get(CONF_PASSWORD, "")},
126-
): str,
127-
vol.Required(
128-
CONF_FRAMERATE,
129-
description={"suggested_value": user_input.get(CONF_FRAMERATE, 2)},
130-
): vol.All(vol.Range(min=0, min_included=False), cv.positive_float),
131-
vol.Required(
132-
CONF_VERIFY_SSL, default=user_input.get(CONF_VERIFY_SSL, True)
133-
): bool,
135+
vol.Optional(CONF_STREAM_SOURCE): str,
136+
vol.Optional(CONF_STILL_IMAGE_URL): str,
137+
vol.Optional(CONF_USERNAME): str,
138+
vol.Optional(CONF_PASSWORD): str,
139+
vol.Required(SECTION_ADVANCED): section(
140+
vol.Schema(advanced_section), {"collapsed": True}
141+
),
134142
}
135143
if is_options_flow:
136-
spec[
137-
vol.Required(
138-
CONF_LIMIT_REFETCH_TO_URL_CHANGE,
139-
default=user_input.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False),
140-
)
141-
] = bool
144+
advanced_section[vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE)] = bool
142145
if show_advanced_options:
143-
spec[
144-
vol.Required(
145-
CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
146-
default=user_input.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False),
147-
)
148-
] = bool
146+
advanced_section[vol.Optional(CONF_USE_WALLCLOCK_AS_TIMESTAMPS)] = bool
147+
149148
return vol.Schema(spec)
150149

151150

@@ -187,7 +186,7 @@ async def async_test_still(
187186
return {CONF_STILL_IMAGE_URL: "malformed_url"}, None
188187
if not yarl_url.is_absolute():
189188
return {CONF_STILL_IMAGE_URL: "relative_url"}, None
190-
verify_ssl = info[CONF_VERIFY_SSL]
189+
verify_ssl = info[SECTION_ADVANCED][CONF_VERIFY_SSL]
191190
auth = generate_auth(info)
192191
try:
193192
async_client = get_async_client(hass, verify_ssl=verify_ssl)
@@ -268,9 +267,9 @@ async def async_test_and_preview_stream(
268267
_LOGGER.warning("Problem rendering template %s: %s", stream_source, err)
269268
raise InvalidStreamException("template_error") from err
270269
stream_options: dict[str, str | bool | float] = {}
271-
if rtsp_transport := info.get(CONF_RTSP_TRANSPORT):
270+
if rtsp_transport := info[SECTION_ADVANCED].get(CONF_RTSP_TRANSPORT):
272271
stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport
273-
if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS):
272+
if info[SECTION_ADVANCED].get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS):
274273
stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True
275274

276275
try:
@@ -326,7 +325,7 @@ def register_still_preview(hass: HomeAssistant) -> None:
326325
class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
327326
"""Config flow for generic IP camera."""
328327

329-
VERSION = 1
328+
VERSION = 2
330329

331330
def __init__(self) -> None:
332331
"""Initialize Generic ConfigFlow."""
@@ -381,7 +380,7 @@ async def async_step_user(
381380
user_input = DEFAULT_DATA.copy()
382381
return self.async_show_form(
383382
step_id="user",
384-
data_schema=build_schema(user_input),
383+
data_schema=self.add_suggested_values_to_schema(build_schema(), user_input),
385384
errors=errors,
386385
)
387386

@@ -449,13 +448,19 @@ async def async_step_init(
449448
self.preview_stream = None
450449
if not errors:
451450
data = {
452-
CONF_USE_WALLCLOCK_AS_TIMESTAMPS: self.config_entry.options.get(
453-
CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False
454-
),
455451
**user_input,
456452
CONF_CONTENT_TYPE: still_format
457453
or self.config_entry.options.get(CONF_CONTENT_TYPE),
458454
}
455+
if (
456+
CONF_USE_WALLCLOCK_AS_TIMESTAMPS
457+
not in user_input[SECTION_ADVANCED]
458+
):
459+
data[SECTION_ADVANCED][CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = (
460+
self.config_entry.options[SECTION_ADVANCED].get(
461+
CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False
462+
)
463+
)
459464
self.user_input = data
460465
# temporary preview for user to check the image
461466
self.preview_image_settings = data
@@ -464,10 +469,12 @@ async def async_step_init(
464469
user_input = self.user_input
465470
return self.async_show_form(
466471
step_id="init",
467-
data_schema=build_schema(
472+
data_schema=self.add_suggested_values_to_schema(
473+
build_schema(
474+
True,
475+
self.show_advanced_options,
476+
),
468477
user_input or self.config_entry.options,
469-
True,
470-
self.show_advanced_options,
471478
),
472479
errors=errors,
473480
)

homeassistant/components/generic/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
CONF_STREAM_SOURCE = "stream_source"
1010
CONF_FRAMERATE = "framerate"
1111
GET_IMAGE_TIMEOUT = 10
12+
SECTION_ADVANCED = "advanced"

homeassistant/components/generic/strings.json

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,24 @@
2626
"step": {
2727
"user": {
2828
"data": {
29-
"authentication": "Authentication",
30-
"framerate": "Frame rate (Hz)",
31-
"limit_refetch_to_url_change": "Limit refetch to URL change",
3229
"password": "[%key:common::config_flow::data::password%]",
33-
"rtsp_transport": "RTSP transport protocol",
3430
"still_image_url": "Still image URL (e.g. http://...)",
3531
"stream_source": "Stream source URL (e.g. rtsp://...)",
36-
"username": "[%key:common::config_flow::data::username%]",
37-
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
32+
"username": "[%key:common::config_flow::data::username%]"
3833
},
39-
"description": "Enter the settings to connect to the camera."
34+
"sections": {
35+
"advanced": {
36+
"data": {
37+
"authentication": "Authentication",
38+
"framerate": "Frame rate (Hz)",
39+
"limit_refetch_to_url_change": "Limit refetch to URL change",
40+
"rtsp_transport": "RTSP transport protocol",
41+
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
42+
},
43+
"description": "Advanced settings are only needed for special cases. Leave them unchanged unless you know what you are doing.",
44+
"name": "Advanced settings"
45+
}
46+
}
4047
},
4148
"user_confirm": {
4249
"data": {
@@ -70,19 +77,27 @@
7077
"step": {
7178
"init": {
7279
"data": {
73-
"authentication": "[%key:component::generic::config::step::user::data::authentication%]",
74-
"framerate": "[%key:component::generic::config::step::user::data::framerate%]",
75-
"limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]",
7680
"password": "[%key:common::config_flow::data::password%]",
77-
"rtsp_transport": "[%key:component::generic::config::step::user::data::rtsp_transport%]",
7881
"still_image_url": "[%key:component::generic::config::step::user::data::still_image_url%]",
7982
"stream_source": "[%key:component::generic::config::step::user::data::stream_source%]",
80-
"use_wallclock_as_timestamps": "Use wallclock as timestamps",
81-
"username": "[%key:common::config_flow::data::username%]",
82-
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
83+
"username": "[%key:common::config_flow::data::username%]"
8384
},
84-
"data_description": {
85-
"use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras"
85+
"sections": {
86+
"advanced": {
87+
"data": {
88+
"authentication": "[%key:component::generic::config::step::user::sections::advanced::data::authentication%]",
89+
"framerate": "[%key:component::generic::config::step::user::sections::advanced::data::framerate%]",
90+
"limit_refetch_to_url_change": "[%key:component::generic::config::step::user::sections::advanced::data::limit_refetch_to_url_change%]",
91+
"rtsp_transport": "[%key:component::generic::config::step::user::sections::advanced::data::rtsp_transport%]",
92+
"use_wallclock_as_timestamps": "Use wallclock as timestamps",
93+
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
94+
},
95+
"data_description": {
96+
"use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras"
97+
},
98+
"description": "[%key:component::generic::config::step::user::sections::advanced::description%]",
99+
"name": "[%key:component::generic::config::step::user::sections::advanced::name%]"
100+
}
86101
}
87102
},
88103
"user_confirm": {

0 commit comments

Comments
 (0)