Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 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
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.

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

import logging
from types import MappingProxyType
from typing import Any

from libpyvivotek.vivotek import VivotekCamera, VivotekCameraError

from homeassistant import config_entries
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 = config_entries.ConfigEntry[VivotekCamera]


async def async_build_and_test_cam_client(
hass: HomeAssistant, data: dict[str, Any] | MappingProxyType[str, Any]
) -> VivotekCamera:
"""Build the client and test if the provided configuration is valid."""
cam_client = 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],
)
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, 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)
137 changes: 107 additions & 30 deletions homeassistant/components/vivotek/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@

from __future__ import annotations

from libpyvivotek import VivotekCamera
import logging
from types import MappingProxyType
from typing import Any

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,14 +26,26 @@
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.device_registry import format_mac
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"
Expand All @@ -54,27 +71,78 @@
)


def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: VivotekConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Vivotek IP Camera."""
"""Set up the component from a config entry."""
config = entry.data
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],
)
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
mac_address = await hass.async_add_executor_job(cam_client.get_mac)
unique_id = format_mac(mac_address)

if not entry.unique_id:
hass.config_entries.async_update_entry(
entry,
unique_id=unique_id,
)
merged_config = entry.data | entry.options
async_add_entities(
[VivotekCam(merged_config, cam_client, stream_source, unique_id)]
)


async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""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,
},
)


class VivotekCam(Camera):
Expand All @@ -83,37 +151,46 @@ class VivotekCam(Camera):
_attr_brand = DEFAULT_CAMERA_BRAND
_attr_supported_features = CameraEntityFeature.STREAM

_attr_configuration_url: str | None = None
_attr_serial: str | None = None

def __init__(
self, config: ConfigType, cam: VivotekCamera, stream_source: str
self,
config: ConfigType | MappingProxyType[str, Any],
cam_client: VivotekCamera,
stream_source: str,
unique_id: str,
) -> None:
"""Initialize a Vivotek camera."""
super().__init__()

self._cam = cam
self._cam_client = cam_client
self._attr_configuration_url = f"http://{config[CONF_IP_ADDRESS]}"
self._attr_frame_interval = 1 / config[CONF_FRAMERATE]
self._attr_name = config[CONF_NAME]
self._attr_unique_id = unique_id
self._stream_source = stream_source

def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image."""
return self._cam.snapshot()
return self._cam_client.snapshot()

async def stream_source(self) -> str:
"""Return the source of the stream."""
return self._stream_source

def disable_motion_detection(self) -> None:
"""Disable motion detection in camera."""
response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 0)
response = self._cam_client.set_param(DEFAULT_EVENT_0_KEY, 0)
self._attr_motion_detection_enabled = int(response) == 1

def enable_motion_detection(self) -> None:
"""Enable motion detection in camera."""
response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 1)
response = self._cam_client.set_param(DEFAULT_EVENT_0_KEY, 1)
self._attr_motion_detection_enabled = int(response) == 1

def update(self) -> None:
"""Update entity status."""
self._attr_model = self._cam.model_name
self._attr_model = self._cam_client.model_name
self._attr_available = self._attr_model is not None
self._attr_serial = self._cam_client.get_serial()
Loading
Loading