Add dhcp ip update support to onvif (#91474)

* Add dhcp ip update support to onvif

If we know the mac address of the camera we can
update the config entry when the ip changes

* fix lookup

* coverage

* remove unreachable

* remove unreachable

* remove unreachable
This commit is contained in:
J. Nick Koston 2023-04-16 09:55:33 -10:00 committed by GitHub
parent d16e1b4ed0
commit 7f7909e0d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 147 additions and 5 deletions

View File

@ -13,6 +13,7 @@ from wsdiscovery.service import Service
from zeep.exceptions import Fault
from homeassistant import config_entries
from homeassistant.components import dhcp
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS
from homeassistant.components.stream import (
CONF_RTSP_TRANSPORT,
@ -27,6 +28,8 @@ from homeassistant.const import (
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import device_registry as dr
from .const import CONF_DEVICE_ID, DEFAULT_ARGUMENTS, DEFAULT_PORT, DOMAIN, LOGGER
from .device import get_device
@ -101,6 +104,30 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema({vol.Required("auto", default=True): bool}),
)
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
"""Handle dhcp discovery."""
hass = self.hass
mac = discovery_info.macaddress
registry = dr.async_get(self.hass)
if not (
device := registry.async_get_device(
identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, mac)}
)
):
return self.async_abort(reason="no_devices_found")
for entry_id in device.config_entries:
if (
not (entry := hass.config_entries.async_get_entry(entry_id))
or entry.domain != DOMAIN
or entry.state is config_entries.ConfigEntryState.LOADED
):
continue
if hass.config_entries.async_update_entry(
entry, data=entry.data | {CONF_HOST: discovery_info.ip}
):
hass.async_create_task(self.hass.config_entries.async_reload(entry_id))
return self.async_abort(reason="already_configured")
async def async_step_device(self, user_input=None):
"""Handle WS-Discovery.

View File

@ -4,6 +4,7 @@
"codeowners": ["@hunterjm"],
"config_flow": true,
"dependencies": ["ffmpeg"],
"dhcp": [{ "registered_devices": true }],
"documentation": "https://www.home-assistant.io/integrations/onvif",
"iot_class": "local_push",
"loggers": ["onvif", "wsdiscovery", "zeep"],

View File

@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"onvif_error": "Error setting up ONVIF device. Check logs for more information.",
"no_h264": "There were no H264 streams available. Check the profile configuration on your device.",

View File

@ -339,6 +339,10 @@ DHCP: list[dict[str, str | bool]] = [
"hostname": "kohlergen*",
"macaddress": "00146F*",
},
{
"domain": "onvif",
"registered_devices": True,
},
{
"domain": "overkiz",
"hostname": "gateway*",

View File

@ -6,7 +6,13 @@ from zeep.exceptions import Fault
from homeassistant import config_entries
from homeassistant.components.onvif import config_flow
from homeassistant.components.onvif.const import CONF_SNAPSHOT_AUTH
from homeassistant.components.onvif.models import Capabilities, DeviceInfo, Profile
from homeassistant.components.onvif.models import (
Capabilities,
DeviceInfo,
Profile,
Resolution,
Video,
)
from homeassistant.const import HTTP_DIGEST_AUTHENTICATION
from tests.common import MockConfigEntry
@ -100,7 +106,7 @@ def setup_mock_device(mock_device):
index=0,
token="dummy",
name="profile1",
video=None,
video=Video("any", Resolution(640, 480)),
ptz=None,
video_source_token=None,
)
@ -120,7 +126,7 @@ async def setup_onvif_integration(
unique_id=MAC,
entry_id="1",
source=config_entries.SOURCE_USER,
):
) -> tuple[MockConfigEntry, MagicMock, MagicMock]:
"""Create an ONVIF config entry."""
if not config:
config = {

View File

@ -2,8 +2,13 @@
from unittest.mock import MagicMock, patch
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.onvif import config_flow
from homeassistant.components import dhcp
from homeassistant.components.onvif import DOMAIN, config_flow
from homeassistant.config_entries import SOURCE_DHCP
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr
from . import (
HOST,
@ -34,6 +39,16 @@ DISCOVERY = [
"MAC": "ee:dd:cc:bb:aa",
},
]
DHCP_DISCOVERY = dhcp.DhcpServiceInfo(
hostname="any",
ip="5.6.7.8",
macaddress=MAC,
)
DHCP_DISCOVERY_SAME_IP = dhcp.DhcpServiceInfo(
hostname="any",
ip="1.2.3.4",
macaddress=MAC,
)
def setup_mock_discovery(
@ -339,3 +354,88 @@ async def test_option_flow(hass: HomeAssistant) -> None:
config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1],
config_flow.CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True,
}
async def test_discovered_by_dhcp_updates_host(hass: HomeAssistant) -> None:
"""Test dhcp updates existing host."""
config_entry, _camera, device = await setup_onvif_integration(hass)
device.profiles = device.async_get_profiles()
registry = dr.async_get(hass)
devices = dr.async_entries_for_config_entry(registry, config_entry.entry_id)
assert len(devices) == 1
device = devices[0]
assert device.model == "TestModel"
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)}
assert config_entry.data[CONF_HOST] == "1.2.3.4"
await hass.config_entries.async_unload(config_entry.entry_id)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_DISCOVERY
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert config_entry.data[CONF_HOST] == DHCP_DISCOVERY.ip
async def test_discovered_by_dhcp_does_nothing_if_host_is_the_same(
hass: HomeAssistant,
) -> None:
"""Test dhcp update does nothing if host is the same."""
config_entry, _camera, device = await setup_onvif_integration(hass)
device.profiles = device.async_get_profiles()
registry = dr.async_get(hass)
devices = dr.async_entries_for_config_entry(registry, config_entry.entry_id)
assert len(devices) == 1
device = devices[0]
assert device.model == "TestModel"
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)}
assert config_entry.data[CONF_HOST] == DHCP_DISCOVERY_SAME_IP.ip
await hass.config_entries.async_unload(config_entry.entry_id)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_DISCOVERY_SAME_IP
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert config_entry.data[CONF_HOST] == DHCP_DISCOVERY_SAME_IP.ip
async def test_discovered_by_dhcp_does_not_update_if_already_loaded(
hass: HomeAssistant,
) -> None:
"""Test dhcp does not update existing host if its already loaded."""
config_entry, _camera, device = await setup_onvif_integration(hass)
device.profiles = device.async_get_profiles()
registry = dr.async_get(hass)
devices = dr.async_entries_for_config_entry(registry, config_entry.entry_id)
assert len(devices) == 1
device = devices[0]
assert device.model == "TestModel"
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)}
assert config_entry.data[CONF_HOST] == "1.2.3.4"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_DISCOVERY
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert config_entry.data[CONF_HOST] != DHCP_DISCOVERY.ip
async def test_discovered_by_dhcp_does_not_update_if_no_matching_entry(
hass: HomeAssistant,
) -> None:
"""Test dhcp does not update existing host if there are no matching entries."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_DISCOVERY
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "no_devices_found"

View File

@ -63,7 +63,10 @@ async def test_diagnostics(
"index": 0,
"token": "dummy",
"name": "profile1",
"video": None,
"video": {
"encoding": "any",
"resolution": {"width": 640, "height": 480},
},
"ptz": None,
"video_source_token": None,
}