Axis discovery updates host address (#22632)
* Discovery can update host on existing entries * Add support in device to update host on entry update * Fix tests and listener * Fix hound comment * Fix failing tests from cleanup
This commit is contained in:
parent
6c14e7afa7
commit
8a0b210f87
7 changed files with 104 additions and 49 deletions
|
@ -4,11 +4,10 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_TRIGGER_TIME,
|
CONF_DEVICE, CONF_NAME, CONF_TRIGGER_TIME, EVENT_HOMEASSISTANT_STOP)
|
||||||
EVENT_HOMEASSISTANT_STOP)
|
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
from .config_flow import configured_devices, DEVICE_SCHEMA
|
from .config_flow import DEVICE_SCHEMA
|
||||||
from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN
|
from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN
|
||||||
from .device import AxisNetworkDevice, get_device
|
from .device import AxisNetworkDevice, get_device
|
||||||
|
|
||||||
|
@ -21,14 +20,13 @@ CONFIG_SCHEMA = vol.Schema({
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up for Axis devices."""
|
"""Set up for Axis devices."""
|
||||||
if DOMAIN in config:
|
if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config:
|
||||||
|
|
||||||
for device_name, device_config in config[DOMAIN].items():
|
for device_name, device_config in config[DOMAIN].items():
|
||||||
|
|
||||||
if CONF_NAME not in device_config:
|
if CONF_NAME not in device_config:
|
||||||
device_config[CONF_NAME] = device_name
|
device_config[CONF_NAME] = device_name
|
||||||
|
|
||||||
if device_config[CONF_HOST] not in configured_devices(hass):
|
|
||||||
hass.async_create_task(hass.config_entries.flow.async_init(
|
hass.async_create_task(hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
|
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
|
||||||
data=device_config
|
data=device_config
|
||||||
|
|
|
@ -52,8 +52,7 @@ class AxisCamera(MjpegCamera):
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Subscribe camera events."""
|
"""Subscribe camera events."""
|
||||||
self.unsub_dispatcher.append(async_dispatcher_connect(
|
self.unsub_dispatcher.append(async_dispatcher_connect(
|
||||||
self.hass, 'axis_{}_new_ip'.format(self.device.name),
|
self.hass, self.device.event_new_address, self._new_address))
|
||||||
self._new_ip))
|
|
||||||
self.unsub_dispatcher.append(async_dispatcher_connect(
|
self.unsub_dispatcher.append(async_dispatcher_connect(
|
||||||
self.hass, self.device.event_reachable, self.update_callback))
|
self.hass, self.device.event_reachable, self.update_callback))
|
||||||
|
|
||||||
|
@ -67,10 +66,10 @@ class AxisCamera(MjpegCamera):
|
||||||
"""Return True if device is available."""
|
"""Return True if device is available."""
|
||||||
return self.device.available
|
return self.device.available
|
||||||
|
|
||||||
def _new_ip(self, host):
|
def _new_address(self):
|
||||||
"""Set new IP for video stream."""
|
"""Set new device address for video stream."""
|
||||||
self._mjpeg_url = AXIS_VIDEO.format(host, self.port)
|
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, self.port)
|
||||||
self._still_image_url = AXIS_IMAGE.format(host, self.port)
|
self._still_image_url = AXIS_IMAGE.format(self.device.host, self.port)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
|
|
|
@ -40,8 +40,8 @@ DEVICE_SCHEMA = vol.Schema({
|
||||||
@callback
|
@callback
|
||||||
def configured_devices(hass):
|
def configured_devices(hass):
|
||||||
"""Return a set of the configured devices."""
|
"""Return a set of the configured devices."""
|
||||||
return set(entry.data[CONF_DEVICE][CONF_HOST] for entry
|
return {entry.data[CONF_MAC]: entry for entry
|
||||||
in hass.config_entries.async_entries(DOMAIN))
|
in hass.config_entries.async_entries(DOMAIN)}
|
||||||
|
|
||||||
|
|
||||||
@config_entries.HANDLERS.register(DOMAIN)
|
@config_entries.HANDLERS.register(DOMAIN)
|
||||||
|
@ -71,9 +71,6 @@ class AxisFlowHandler(config_entries.ConfigFlow):
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
if user_input[CONF_HOST] in configured_devices(self.hass):
|
|
||||||
raise AlreadyConfigured
|
|
||||||
|
|
||||||
self.device_config = {
|
self.device_config = {
|
||||||
CONF_HOST: user_input[CONF_HOST],
|
CONF_HOST: user_input[CONF_HOST],
|
||||||
CONF_PORT: user_input[CONF_PORT],
|
CONF_PORT: user_input[CONF_PORT],
|
||||||
|
@ -84,6 +81,10 @@ class AxisFlowHandler(config_entries.ConfigFlow):
|
||||||
|
|
||||||
self.serial_number = device.vapix.get_param(
|
self.serial_number = device.vapix.get_param(
|
||||||
VAPIX_SERIAL_NUMBER)
|
VAPIX_SERIAL_NUMBER)
|
||||||
|
|
||||||
|
if self.serial_number in configured_devices(self.hass):
|
||||||
|
raise AlreadyConfigured
|
||||||
|
|
||||||
self.model = device.vapix.get_param(VAPIX_MODEL_ID)
|
self.model = device.vapix.get_param(VAPIX_MODEL_ID)
|
||||||
|
|
||||||
return await self._create_entry()
|
return await self._create_entry()
|
||||||
|
@ -142,22 +143,30 @@ class AxisFlowHandler(config_entries.ConfigFlow):
|
||||||
data=data
|
data=data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _update_entry(self, entry, host):
|
||||||
|
"""Update existing entry if it is the same device."""
|
||||||
|
entry.data[CONF_DEVICE][CONF_HOST] = host
|
||||||
|
self.hass.config_entries.async_update_entry(entry)
|
||||||
|
|
||||||
async def async_step_discovery(self, discovery_info):
|
async def async_step_discovery(self, discovery_info):
|
||||||
"""Prepare configuration for a discovered Axis device.
|
"""Prepare configuration for a discovered Axis device.
|
||||||
|
|
||||||
This flow is triggered by the discovery component.
|
This flow is triggered by the discovery component.
|
||||||
"""
|
"""
|
||||||
if discovery_info[CONF_HOST] in configured_devices(self.hass):
|
|
||||||
return self.async_abort(reason='already_configured')
|
|
||||||
|
|
||||||
if discovery_info[CONF_HOST].startswith('169.254'):
|
if discovery_info[CONF_HOST].startswith('169.254'):
|
||||||
return self.async_abort(reason='link_local_address')
|
return self.async_abort(reason='link_local_address')
|
||||||
|
|
||||||
|
serialnumber = discovery_info['properties']['macaddress']
|
||||||
|
device_entries = configured_devices(self.hass)
|
||||||
|
|
||||||
|
if serialnumber in device_entries:
|
||||||
|
entry = device_entries[serialnumber]
|
||||||
|
await self._update_entry(entry, discovery_info[CONF_HOST])
|
||||||
|
return self.async_abort(reason='already_configured')
|
||||||
|
|
||||||
config_file = await self.hass.async_add_executor_job(
|
config_file = await self.hass.async_add_executor_job(
|
||||||
load_json, self.hass.config.path(CONFIG_FILE))
|
load_json, self.hass.config.path(CONFIG_FILE))
|
||||||
|
|
||||||
serialnumber = discovery_info['properties']['macaddress']
|
|
||||||
|
|
||||||
if serialnumber not in config_file:
|
if serialnumber not in config_file:
|
||||||
self.discovery_schema = {
|
self.discovery_schema = {
|
||||||
vol.Required(
|
vol.Required(
|
||||||
|
|
|
@ -101,8 +101,26 @@ class AxisNetworkDevice:
|
||||||
self.api.enable_events(event_callback=self.async_event_callback)
|
self.api.enable_events(event_callback=self.async_event_callback)
|
||||||
self.api.start()
|
self.api.start()
|
||||||
|
|
||||||
|
self.config_entry.add_update_listener(self.async_new_address_callback)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event_new_address(self):
|
||||||
|
"""Device specific event to signal new device address."""
|
||||||
|
return 'axis_new_address_{}'.format(self.serial)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def async_new_address_callback(hass, entry):
|
||||||
|
"""Handle signals of device getting new address.
|
||||||
|
|
||||||
|
This is a static method because a class method (bound method),
|
||||||
|
can not be used with weak references.
|
||||||
|
"""
|
||||||
|
device = hass.data[DOMAIN][entry.data[CONF_MAC]]
|
||||||
|
device.api.config.host = device.host
|
||||||
|
async_dispatcher_send(hass, device.event_new_address)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event_reachable(self):
|
def event_reachable(self):
|
||||||
"""Device specific event to signal a change in connection status."""
|
"""Device specific event to signal a change in connection status."""
|
||||||
|
@ -110,7 +128,7 @@ class AxisNetworkDevice:
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_connection_status_callback(self, status):
|
def async_connection_status_callback(self, status):
|
||||||
"""Handle signals of gateway connection status.
|
"""Handle signals of device connection status.
|
||||||
|
|
||||||
This is called on every RTSP keep-alive message.
|
This is called on every RTSP keep-alive message.
|
||||||
Only signal state change if state change is true.
|
Only signal state change if state change is true.
|
||||||
|
|
|
@ -16,7 +16,7 @@ async def test_configured_devices(hass):
|
||||||
assert not result
|
assert not result
|
||||||
|
|
||||||
entry = MockConfigEntry(domain=axis.DOMAIN,
|
entry = MockConfigEntry(domain=axis.DOMAIN,
|
||||||
data={axis.CONF_DEVICE: {axis.CONF_HOST: ''}})
|
data={axis.config_flow.CONF_MAC: '1234'})
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
result = config_flow.configured_devices(hass)
|
result = config_flow.configured_devices(hass)
|
||||||
|
@ -76,11 +76,15 @@ async def test_flow_fails_already_configured(hass):
|
||||||
flow = config_flow.AxisFlowHandler()
|
flow = config_flow.AxisFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
entry = MockConfigEntry(domain=axis.DOMAIN, data={axis.CONF_DEVICE: {
|
entry = MockConfigEntry(domain=axis.DOMAIN,
|
||||||
axis.CONF_HOST: '1.2.3.4'
|
data={axis.config_flow.CONF_MAC: '1234'})
|
||||||
}})
|
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
mock_device = Mock()
|
||||||
|
mock_device.vapix.get_param.return_value = '1234'
|
||||||
|
|
||||||
|
with patch('homeassistant.components.axis.config_flow.get_device',
|
||||||
|
return_value=mock_coro(mock_device)):
|
||||||
result = await flow.async_step_user(user_input={
|
result = await flow.async_step_user(user_input={
|
||||||
config_flow.CONF_HOST: '1.2.3.4',
|
config_flow.CONF_HOST: '1.2.3.4',
|
||||||
config_flow.CONF_USERNAME: 'user',
|
config_flow.CONF_USERNAME: 'user',
|
||||||
|
@ -220,16 +224,19 @@ async def test_discovery_flow_already_configured(hass):
|
||||||
flow = config_flow.AxisFlowHandler()
|
flow = config_flow.AxisFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
entry = MockConfigEntry(domain=axis.DOMAIN, data={axis.CONF_DEVICE: {
|
entry = MockConfigEntry(
|
||||||
axis.CONF_HOST: '1.2.3.4'
|
domain=axis.DOMAIN,
|
||||||
}})
|
data={axis.CONF_DEVICE: {axis.config_flow.CONF_HOST: '1.2.3.4'},
|
||||||
|
axis.config_flow.CONF_MAC: '1234ABCD'}
|
||||||
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
result = await flow.async_step_discovery(discovery_info={
|
result = await flow.async_step_discovery(discovery_info={
|
||||||
config_flow.CONF_HOST: '1.2.3.4',
|
config_flow.CONF_HOST: '1.2.3.4',
|
||||||
config_flow.CONF_USERNAME: 'user',
|
config_flow.CONF_USERNAME: 'user',
|
||||||
config_flow.CONF_PASSWORD: 'pass',
|
config_flow.CONF_PASSWORD: 'pass',
|
||||||
config_flow.CONF_PORT: 81
|
config_flow.CONF_PORT: 81,
|
||||||
|
'properties': {'macaddress': '1234ABCD'}
|
||||||
})
|
})
|
||||||
print(result)
|
print(result)
|
||||||
assert result['type'] == 'abort'
|
assert result['type'] == 'abort'
|
||||||
|
|
|
@ -3,9 +3,10 @@ from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.common import mock_coro
|
from tests.common import mock_coro, MockConfigEntry
|
||||||
|
|
||||||
from homeassistant.components.axis import device, errors
|
from homeassistant.components.axis import device, errors
|
||||||
|
from homeassistant.components.axis.camera import AxisCamera
|
||||||
|
|
||||||
DEVICE_DATA = {
|
DEVICE_DATA = {
|
||||||
device.CONF_HOST: '1.2.3.4',
|
device.CONF_HOST: '1.2.3.4',
|
||||||
|
@ -16,7 +17,7 @@ DEVICE_DATA = {
|
||||||
|
|
||||||
ENTRY_OPTIONS = {
|
ENTRY_OPTIONS = {
|
||||||
device.CONF_CAMERA: True,
|
device.CONF_CAMERA: True,
|
||||||
device.CONF_EVENTS: ['pir'],
|
device.CONF_EVENTS: True,
|
||||||
}
|
}
|
||||||
|
|
||||||
ENTRY_CONFIG = {
|
ENTRY_CONFIG = {
|
||||||
|
@ -53,6 +54,31 @@ async def test_device_setup():
|
||||||
(entry, 'binary_sensor')
|
(entry, 'binary_sensor')
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_signal_new_address(hass):
|
||||||
|
"""Successful setup."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=device.DOMAIN, data=ENTRY_CONFIG, options=ENTRY_OPTIONS)
|
||||||
|
|
||||||
|
api = Mock()
|
||||||
|
api.vapix.get_param.return_value = '1234'
|
||||||
|
|
||||||
|
axis_device = device.AxisNetworkDevice(hass, entry)
|
||||||
|
hass.data[device.DOMAIN] = {axis_device.serial: axis_device}
|
||||||
|
|
||||||
|
with patch.object(device, 'get_device', return_value=mock_coro(api)), \
|
||||||
|
patch.object(AxisCamera, '_new_address') as new_address_mock:
|
||||||
|
await axis_device.async_setup()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry.data[device.CONF_DEVICE][device.CONF_HOST] = '2.3.4.5'
|
||||||
|
hass.config_entries.async_update_entry(entry, data=entry.data)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert axis_device.host == '2.3.4.5'
|
||||||
|
assert axis_device.api.config.host == '2.3.4.5'
|
||||||
|
assert len(new_address_mock.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_device_not_accessible():
|
async def test_device_not_accessible():
|
||||||
"""Failed setup schedules a retry of setup."""
|
"""Failed setup schedules a retry of setup."""
|
||||||
hass = Mock()
|
hass = Mock()
|
||||||
|
|
|
@ -9,30 +9,28 @@ from tests.common import mock_coro, MockConfigEntry
|
||||||
|
|
||||||
async def test_setup(hass):
|
async def test_setup(hass):
|
||||||
"""Test configured options for a device are loaded via config entry."""
|
"""Test configured options for a device are loaded via config entry."""
|
||||||
with patch.object(hass, 'config_entries') as mock_config_entries, \
|
with patch.object(hass.config_entries, 'flow') as mock_config_flow:
|
||||||
patch.object(axis, 'configured_devices', return_value={}):
|
|
||||||
|
|
||||||
assert await async_setup_component(hass, axis.DOMAIN, {
|
assert await async_setup_component(hass, axis.DOMAIN, {
|
||||||
axis.DOMAIN: {
|
axis.DOMAIN: {
|
||||||
'device_name': {
|
'device_name': {
|
||||||
axis.CONF_HOST: '1.2.3.4',
|
axis.config_flow.CONF_HOST: '1.2.3.4',
|
||||||
axis.config_flow.CONF_PORT: 80,
|
axis.config_flow.CONF_PORT: 80,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
assert len(mock_config_entries.flow.mock_calls) == 1
|
assert len(mock_config_flow.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_device_already_configured(hass):
|
async def test_setup_device_already_configured(hass):
|
||||||
"""Test already configured device does not configure a second."""
|
"""Test already configured device does not configure a second."""
|
||||||
with patch.object(hass, 'config_entries') as mock_config_entries, \
|
with patch.object(hass, 'config_entries') as mock_config_entries:
|
||||||
patch.object(axis, 'configured_devices', return_value={'1.2.3.4'}):
|
|
||||||
|
|
||||||
assert await async_setup_component(hass, axis.DOMAIN, {
|
assert await async_setup_component(hass, axis.DOMAIN, {
|
||||||
axis.DOMAIN: {
|
axis.DOMAIN: {
|
||||||
'device_name': {
|
'device_name': {
|
||||||
axis.CONF_HOST: '1.2.3.4'
|
axis.config_flow.CONF_HOST: '1.2.3.4'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue