Skip to content

Commit c67247b

Browse files
HarlemSquirrelCopilotjoostlek
authored
Add config flow for Vivotek integration (home-assistant#154801)
Co-authored-by: Copilot <[email protected]> Co-authored-by: Joostlek <[email protected]>
1 parent 18b5ffd commit c67247b

File tree

13 files changed

+751
-28
lines changed

13 files changed

+751
-28
lines changed

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,70 @@
11
"""The Vivotek camera component."""
2+
3+
import logging
4+
from typing import Any
5+
6+
from libpyvivotek.vivotek import VivotekCamera, VivotekCameraError
7+
8+
from homeassistant.config_entries import ConfigEntry
9+
from homeassistant.const import (
10+
CONF_AUTHENTICATION,
11+
CONF_IP_ADDRESS,
12+
CONF_PASSWORD,
13+
CONF_PORT,
14+
CONF_USERNAME,
15+
CONF_VERIFY_SSL,
16+
HTTP_DIGEST_AUTHENTICATION,
17+
Platform,
18+
)
19+
from homeassistant.core import HomeAssistant
20+
from homeassistant.exceptions import ConfigEntryError
21+
22+
from .const import CONF_SECURITY_LEVEL
23+
24+
_LOGGER = logging.getLogger(__name__)
25+
26+
PLATFORMS = [Platform.CAMERA]
27+
28+
type VivotekConfigEntry = ConfigEntry[VivotekCamera]
29+
30+
31+
def build_cam_client(data: dict[str, Any]) -> VivotekCamera:
32+
"""Build the Vivotek camera client from the provided configuration data."""
33+
return VivotekCamera(
34+
host=data[CONF_IP_ADDRESS],
35+
port=data[CONF_PORT],
36+
verify_ssl=data[CONF_VERIFY_SSL],
37+
usr=data[CONF_USERNAME],
38+
pwd=data[CONF_PASSWORD],
39+
digest_auth=(data[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION),
40+
sec_lvl=data[CONF_SECURITY_LEVEL],
41+
)
42+
43+
44+
async def async_build_and_test_cam_client(
45+
hass: HomeAssistant, data: dict[str, Any]
46+
) -> VivotekCamera:
47+
"""Build the client and test if the provided configuration is valid."""
48+
cam_client = build_cam_client(data)
49+
await hass.async_add_executor_job(cam_client.get_mac)
50+
51+
return cam_client
52+
53+
54+
async def async_setup_entry(hass: HomeAssistant, entry: VivotekConfigEntry) -> bool:
55+
"""Set up the Vivotek component from a config entry."""
56+
57+
try:
58+
cam_client = await async_build_and_test_cam_client(hass, dict(entry.data))
59+
except VivotekCameraError as err:
60+
raise ConfigEntryError("Failed to connect to camera") from err
61+
62+
entry.runtime_data = cam_client
63+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
64+
65+
return True
66+
67+
68+
async def async_unload_entry(hass: HomeAssistant, entry: VivotekConfigEntry) -> bool:
69+
"""Unload a config entry."""
70+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

homeassistant/components/vivotek/camera.py

Lines changed: 100 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
from __future__ import annotations
44

5-
from libpyvivotek import VivotekCamera
5+
import logging
6+
from typing import TYPE_CHECKING
7+
8+
from libpyvivotek.vivotek import VivotekCamera
69
import voluptuous as vol
710

811
from homeassistant.components.camera import (
912
PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA,
1013
Camera,
1114
CameraEntityFeature,
1215
)
16+
from homeassistant.config_entries import SOURCE_IMPORT
1317
from homeassistant.const import (
1418
CONF_AUTHENTICATION,
1519
CONF_IP_ADDRESS,
@@ -21,18 +25,30 @@
2125
HTTP_BASIC_AUTHENTICATION,
2226
HTTP_DIGEST_AUTHENTICATION,
2327
)
24-
from homeassistant.core import HomeAssistant
25-
from homeassistant.helpers import config_validation as cv
26-
from homeassistant.helpers.entity_platform import AddEntitiesCallback
28+
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
29+
from homeassistant.data_entry_flow import FlowResultType
30+
from homeassistant.helpers import config_validation as cv, issue_registry as ir
31+
from homeassistant.helpers.entity_platform import (
32+
AddConfigEntryEntitiesCallback,
33+
AddEntitiesCallback,
34+
)
2735
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
2836

29-
CONF_FRAMERATE = "framerate"
30-
CONF_SECURITY_LEVEL = "security_level"
31-
CONF_STREAM_PATH = "stream_path"
37+
from . import VivotekConfigEntry
38+
from .const import (
39+
CONF_FRAMERATE,
40+
CONF_SECURITY_LEVEL,
41+
CONF_STREAM_PATH,
42+
DOMAIN,
43+
INTEGRATION_TITLE,
44+
)
45+
46+
_LOGGER = logging.getLogger(__name__)
3247

3348
DEFAULT_CAMERA_BRAND = "VIVOTEK"
3449
DEFAULT_NAME = "VIVOTEK Camera"
3550
DEFAULT_EVENT_0_KEY = "event_i0_enable"
51+
DEFAULT_FRAMERATE = 2
3652
DEFAULT_SECURITY_LEVEL = "admin"
3753
DEFAULT_STREAM_SOURCE = "live.sdp"
3854

@@ -47,34 +63,86 @@
4763
),
4864
vol.Optional(CONF_SSL, default=False): cv.boolean,
4965
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
50-
vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int,
66+
vol.Optional(CONF_FRAMERATE, default=DEFAULT_FRAMERATE): cv.positive_int,
5167
vol.Optional(CONF_SECURITY_LEVEL, default=DEFAULT_SECURITY_LEVEL): cv.string,
5268
vol.Optional(CONF_STREAM_PATH, default=DEFAULT_STREAM_SOURCE): cv.string,
5369
}
5470
)
5571

5672

57-
def setup_platform(
73+
async def async_setup_platform(
5874
hass: HomeAssistant,
5975
config: ConfigType,
60-
add_entities: AddEntitiesCallback,
76+
async_add_entities: AddEntitiesCallback,
6177
discovery_info: DiscoveryInfoType | None = None,
6278
) -> None:
63-
"""Set up a Vivotek IP Camera."""
64-
creds = f"{config[CONF_USERNAME]}:{config[CONF_PASSWORD]}"
65-
cam = VivotekCamera(
66-
host=config[CONF_IP_ADDRESS],
67-
port=(443 if config[CONF_SSL] else 80),
68-
verify_ssl=config[CONF_VERIFY_SSL],
69-
usr=config[CONF_USERNAME],
70-
pwd=config[CONF_PASSWORD],
71-
digest_auth=config[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION,
72-
sec_lvl=config[CONF_SECURITY_LEVEL],
79+
"""Set up the Vivotek camera platform."""
80+
result = await hass.config_entries.flow.async_init(
81+
DOMAIN,
82+
context={"source": SOURCE_IMPORT},
83+
data=config,
84+
)
85+
if (
86+
result.get("type") is FlowResultType.ABORT
87+
and result.get("reason") != "already_configured"
88+
):
89+
ir.async_create_issue(
90+
hass,
91+
DOMAIN,
92+
f"deprecated_yaml_import_issue_{result.get('reason')}",
93+
breaks_in_ha_version="2026.6.0",
94+
is_fixable=False,
95+
issue_domain=DOMAIN,
96+
severity=ir.IssueSeverity.WARNING,
97+
translation_key=f"deprecated_yaml_import_issue_{result.get('reason')}",
98+
translation_placeholders={
99+
"domain": DOMAIN,
100+
"integration_title": INTEGRATION_TITLE,
101+
},
102+
)
103+
return
104+
105+
ir.async_create_issue(
106+
hass,
107+
HOMEASSISTANT_DOMAIN,
108+
"deprecated_yaml",
109+
breaks_in_ha_version="2026.6.0",
110+
is_fixable=False,
111+
issue_domain=DOMAIN,
112+
severity=ir.IssueSeverity.WARNING,
113+
translation_key="deprecated_yaml",
114+
translation_placeholders={
115+
"domain": DOMAIN,
116+
"integration_title": INTEGRATION_TITLE,
117+
},
73118
)
119+
120+
121+
async def async_setup_entry(
122+
hass: HomeAssistant,
123+
entry: VivotekConfigEntry,
124+
async_add_entities: AddConfigEntryEntitiesCallback,
125+
) -> None:
126+
"""Set up the component from a config entry."""
127+
config = entry.data
128+
creds = f"{config[CONF_USERNAME]}:{config[CONF_PASSWORD]}"
74129
stream_source = (
75130
f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}"
76131
)
77-
add_entities([VivotekCam(config, cam, stream_source)], True)
132+
cam_client = entry.runtime_data
133+
if TYPE_CHECKING:
134+
assert entry.unique_id is not None
135+
async_add_entities(
136+
[
137+
VivotekCam(
138+
cam_client,
139+
stream_source,
140+
entry.unique_id,
141+
entry.options[CONF_FRAMERATE],
142+
entry.title,
143+
)
144+
]
145+
)
78146

79147

80148
class VivotekCam(Camera):
@@ -84,14 +152,19 @@ class VivotekCam(Camera):
84152
_attr_supported_features = CameraEntityFeature.STREAM
85153

86154
def __init__(
87-
self, config: ConfigType, cam: VivotekCamera, stream_source: str
155+
self,
156+
cam_client: VivotekCamera,
157+
stream_source: str,
158+
unique_id: str,
159+
framerate: int,
160+
name: str,
88161
) -> None:
89162
"""Initialize a Vivotek camera."""
90163
super().__init__()
91-
92-
self._cam = cam
93-
self._attr_frame_interval = 1 / config[CONF_FRAMERATE]
94-
self._attr_name = config[CONF_NAME]
164+
self._cam = cam_client
165+
self._attr_frame_interval = 1 / framerate
166+
self._attr_unique_id = unique_id
167+
self._attr_name = name
95168
self._stream_source = stream_source
96169

97170
def camera_image(
@@ -117,3 +190,4 @@ def enable_motion_detection(self) -> None:
117190
def update(self) -> None:
118191
"""Update entity status."""
119192
self._attr_model = self._cam.model_name
193+
self._attr_available = self._attr_model is not None

0 commit comments

Comments
 (0)