Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
229a399
Add config flow for Vivotek integration
HarlemSquirrel Oct 19, 2025
3133616
Update homeassistant/components/vivotek/__init__.py
HarlemSquirrel Oct 19, 2025
f26ba03
Update homeassistant/components/vivotek/config_flow.py
HarlemSquirrel Oct 19, 2025
85aed59
Add config flow tests for vivotek
HarlemSquirrel Oct 19, 2025
dd741d4
Use port from config
HarlemSquirrel Oct 19, 2025
8d51338
Update requirements
HarlemSquirrel Oct 20, 2025
7bb1fd5
Add device info
HarlemSquirrel Oct 27, 2025
92e6737
Code review fixes
HarlemSquirrel Oct 27, 2025
fa31269
Merge branch 'dev' into vivotek-add-config-flow
HarlemSquirrel Oct 30, 2025
9dd00f6
Use add_suggested_values_to_schema and fix tests
HarlemSquirrel Oct 30, 2025
8f552f2
Remove unused instance var
HarlemSquirrel Oct 30, 2025
0c1c473
Format with prettier
HarlemSquirrel Oct 30, 2025
a7f81b0
fixup! Format with prettier
HarlemSquirrel Oct 30, 2025
b19cb57
Fix security level default config
HarlemSquirrel Oct 30, 2025
bb89459
Code review updates and remove some uneccessary code
HarlemSquirrel Oct 30, 2025
ee1b19a
Updates from code review
HarlemSquirrel Nov 19, 2025
df8f876
Update tests
HarlemSquirrel Nov 19, 2025
fb6f9b5
Update yaml import
HarlemSquirrel Nov 19, 2025
bd4ae4b
Put back plat schema
HarlemSquirrel Nov 19, 2025
db140f3
Move framerate to options
HarlemSquirrel Nov 19, 2025
8aaaba2
Update homeassistant/components/vivotek/config_flow.py
HarlemSquirrel Nov 19, 2025
1d1c8fa
Create deprecation issue for config flow change
HarlemSquirrel Nov 20, 2025
99ac290
Merge branch 'dev' into vivotek-add-config-flow
joostlek Dec 2, 2025
7bc7c41
Fix
joostlek Dec 2, 2025
6caf3f7
Fix
joostlek Dec 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions homeassistant/components/vivotek/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,70 @@
"""The Vivotek camera component."""

import logging
from typing import Any

from libpyvivotek.vivotek import VivotekCamera, VivotekCameraError

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_AUTHENTICATION,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
CONF_VERIFY_SSL,
HTTP_DIGEST_AUTHENTICATION,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError

from .const import CONF_SECURITY_LEVEL

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [Platform.CAMERA]

type VivotekConfigEntry = ConfigEntry[VivotekCamera]


def build_cam_client(data: dict[str, Any]) -> VivotekCamera:
"""Build the Vivotek camera client from the provided configuration data."""
return VivotekCamera(
host=data[CONF_IP_ADDRESS],
port=data[CONF_PORT],
verify_ssl=data[CONF_VERIFY_SSL],
usr=data[CONF_USERNAME],
pwd=data[CONF_PASSWORD],
digest_auth=(data[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION),
sec_lvl=data[CONF_SECURITY_LEVEL],
)


async def async_build_and_test_cam_client(
hass: HomeAssistant, data: dict[str, Any]
) -> VivotekCamera:
"""Build the client and test if the provided configuration is valid."""
cam_client = build_cam_client(data)
await hass.async_add_executor_job(cam_client.get_mac)

return cam_client


async def async_setup_entry(hass: HomeAssistant, entry: VivotekConfigEntry) -> bool:
"""Set up the Vivotek component from a config entry."""

try:
cam_client = await async_build_and_test_cam_client(hass, dict(entry.data))
except VivotekCameraError as err:
raise ConfigEntryError("Failed to connect to camera") from err

entry.runtime_data = cam_client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: VivotekConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
126 changes: 100 additions & 26 deletions homeassistant/components/vivotek/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

from __future__ import annotations

from libpyvivotek import VivotekCamera
import logging
from typing import TYPE_CHECKING

from libpyvivotek.vivotek import VivotekCamera
import voluptuous as vol

from homeassistant.components.camera import (
PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA,
Camera,
CameraEntityFeature,
)
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_AUTHENTICATION,
CONF_IP_ADDRESS,
Expand All @@ -21,18 +25,30 @@
HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

CONF_FRAMERATE = "framerate"
CONF_SECURITY_LEVEL = "security_level"
CONF_STREAM_PATH = "stream_path"
from . import VivotekConfigEntry
from .const import (
CONF_FRAMERATE,
CONF_SECURITY_LEVEL,
CONF_STREAM_PATH,
DOMAIN,
INTEGRATION_TITLE,
)

_LOGGER = logging.getLogger(__name__)

DEFAULT_CAMERA_BRAND = "VIVOTEK"
DEFAULT_NAME = "VIVOTEK Camera"
DEFAULT_EVENT_0_KEY = "event_i0_enable"
DEFAULT_FRAMERATE = 2
DEFAULT_SECURITY_LEVEL = "admin"
DEFAULT_STREAM_SOURCE = "live.sdp"

Expand All @@ -47,34 +63,86 @@
),
vol.Optional(CONF_SSL, default=False): cv.boolean,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int,
vol.Optional(CONF_FRAMERATE, default=DEFAULT_FRAMERATE): cv.positive_int,
vol.Optional(CONF_SECURITY_LEVEL, default=DEFAULT_SECURITY_LEVEL): cv.string,
vol.Optional(CONF_STREAM_PATH, default=DEFAULT_STREAM_SOURCE): cv.string,
}
)


def setup_platform(
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up a Vivotek IP Camera."""
creds = f"{config[CONF_USERNAME]}:{config[CONF_PASSWORD]}"
cam = VivotekCamera(
host=config[CONF_IP_ADDRESS],
port=(443 if config[CONF_SSL] else 80),
verify_ssl=config[CONF_VERIFY_SSL],
usr=config[CONF_USERNAME],
pwd=config[CONF_PASSWORD],
digest_auth=config[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION,
sec_lvl=config[CONF_SECURITY_LEVEL],
"""Set up the Vivotek camera platform."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
if (
result.get("type") is FlowResultType.ABORT
and result.get("reason") != "already_configured"
):
ir.async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_import_issue_{result.get('reason')}",
breaks_in_ha_version="2026.6.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key=f"deprecated_yaml_import_issue_{result.get('reason')}",
translation_placeholders={
"domain": DOMAIN,
"integration_title": INTEGRATION_TITLE,
},
)
return

ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2026.6.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": INTEGRATION_TITLE,
},
)


async def async_setup_entry(
hass: HomeAssistant,
entry: VivotekConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the component from a config entry."""
config = entry.data
creds = f"{config[CONF_USERNAME]}:{config[CONF_PASSWORD]}"
stream_source = (
f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}"
)
add_entities([VivotekCam(config, cam, stream_source)], True)
cam_client = entry.runtime_data
if TYPE_CHECKING:
assert entry.unique_id is not None
async_add_entities(
[
VivotekCam(
cam_client,
stream_source,
entry.unique_id,
entry.options[CONF_FRAMERATE],
entry.title,
)
]
)


class VivotekCam(Camera):
Expand All @@ -84,14 +152,19 @@ class VivotekCam(Camera):
_attr_supported_features = CameraEntityFeature.STREAM

def __init__(
self, config: ConfigType, cam: VivotekCamera, stream_source: str
self,
cam_client: VivotekCamera,
stream_source: str,
unique_id: str,
framerate: int,
name: str,
) -> None:
"""Initialize a Vivotek camera."""
super().__init__()

self._cam = cam
self._attr_frame_interval = 1 / config[CONF_FRAMERATE]
self._attr_name = config[CONF_NAME]
self._cam = cam_client
self._attr_frame_interval = 1 / framerate
self._attr_unique_id = unique_id
self._attr_name = name
self._stream_source = stream_source

def camera_image(
Expand All @@ -117,3 +190,4 @@ def enable_motion_detection(self) -> None:
def update(self) -> None:
"""Update entity status."""
self._attr_model = self._cam.model_name
self._attr_available = self._attr_model is not None
Loading
Loading