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:
Robert Svensson 2019-04-02 20:13:11 +02:00 committed by Paulus Schoutsen
parent 6c14e7afa7
commit 8a0b210f87
7 changed files with 104 additions and 49 deletions

View file

@ -4,11 +4,10 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_TRIGGER_TIME,
EVENT_HOMEASSISTANT_STOP)
CONF_DEVICE, CONF_NAME, CONF_TRIGGER_TIME, EVENT_HOMEASSISTANT_STOP)
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 .device import AxisNetworkDevice, get_device
@ -21,18 +20,17 @@ CONFIG_SCHEMA = vol.Schema({
async def async_setup(hass, config):
"""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():
if CONF_NAME not in device_config:
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(
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data=device_config
))
hass.async_create_task(hass.config_entries.flow.async_init(
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data=device_config
))
return True

View file

@ -52,8 +52,7 @@ class AxisCamera(MjpegCamera):
async def async_added_to_hass(self):
"""Subscribe camera events."""
self.unsub_dispatcher.append(async_dispatcher_connect(
self.hass, 'axis_{}_new_ip'.format(self.device.name),
self._new_ip))
self.hass, self.device.event_new_address, self._new_address))
self.unsub_dispatcher.append(async_dispatcher_connect(
self.hass, self.device.event_reachable, self.update_callback))
@ -67,10 +66,10 @@ class AxisCamera(MjpegCamera):
"""Return True if device is available."""
return self.device.available
def _new_ip(self, host):
"""Set new IP for video stream."""
self._mjpeg_url = AXIS_VIDEO.format(host, self.port)
self._still_image_url = AXIS_IMAGE.format(host, self.port)
def _new_address(self):
"""Set new device address for video stream."""
self._mjpeg_url = AXIS_VIDEO.format(self.device.host, self.port)
self._still_image_url = AXIS_IMAGE.format(self.device.host, self.port)
@property
def unique_id(self):

View file

@ -40,8 +40,8 @@ DEVICE_SCHEMA = vol.Schema({
@callback
def configured_devices(hass):
"""Return a set of the configured devices."""
return set(entry.data[CONF_DEVICE][CONF_HOST] for entry
in hass.config_entries.async_entries(DOMAIN))
return {entry.data[CONF_MAC]: entry for entry
in hass.config_entries.async_entries(DOMAIN)}
@config_entries.HANDLERS.register(DOMAIN)
@ -71,9 +71,6 @@ class AxisFlowHandler(config_entries.ConfigFlow):
if user_input is not None:
try:
if user_input[CONF_HOST] in configured_devices(self.hass):
raise AlreadyConfigured
self.device_config = {
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
@ -84,6 +81,10 @@ class AxisFlowHandler(config_entries.ConfigFlow):
self.serial_number = device.vapix.get_param(
VAPIX_SERIAL_NUMBER)
if self.serial_number in configured_devices(self.hass):
raise AlreadyConfigured
self.model = device.vapix.get_param(VAPIX_MODEL_ID)
return await self._create_entry()
@ -142,22 +143,30 @@ class AxisFlowHandler(config_entries.ConfigFlow):
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):
"""Prepare configuration for a discovered Axis device.
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'):
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(
load_json, self.hass.config.path(CONFIG_FILE))
serialnumber = discovery_info['properties']['macaddress']
if serialnumber not in config_file:
self.discovery_schema = {
vol.Required(

View file

@ -101,8 +101,26 @@ class AxisNetworkDevice:
self.api.enable_events(event_callback=self.async_event_callback)
self.api.start()
self.config_entry.add_update_listener(self.async_new_address_callback)
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
def event_reachable(self):
"""Device specific event to signal a change in connection status."""
@ -110,7 +128,7 @@ class AxisNetworkDevice:
@callback
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.
Only signal state change if state change is true.

View file

@ -16,7 +16,7 @@ async def test_configured_devices(hass):
assert not result
entry = MockConfigEntry(domain=axis.DOMAIN,
data={axis.CONF_DEVICE: {axis.CONF_HOST: ''}})
data={axis.config_flow.CONF_MAC: '1234'})
entry.add_to_hass(hass)
result = config_flow.configured_devices(hass)
@ -76,17 +76,21 @@ async def test_flow_fails_already_configured(hass):
flow = config_flow.AxisFlowHandler()
flow.hass = hass
entry = MockConfigEntry(domain=axis.DOMAIN, data={axis.CONF_DEVICE: {
axis.CONF_HOST: '1.2.3.4'
}})
entry = MockConfigEntry(domain=axis.DOMAIN,
data={axis.config_flow.CONF_MAC: '1234'})
entry.add_to_hass(hass)
result = await flow.async_step_user(user_input={
config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_USERNAME: 'user',
config_flow.CONF_PASSWORD: 'pass',
config_flow.CONF_PORT: 81
})
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={
config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_USERNAME: 'user',
config_flow.CONF_PASSWORD: 'pass',
config_flow.CONF_PORT: 81
})
assert result['errors'] == {'base': 'already_configured'}
@ -220,16 +224,19 @@ async def test_discovery_flow_already_configured(hass):
flow = config_flow.AxisFlowHandler()
flow.hass = hass
entry = MockConfigEntry(domain=axis.DOMAIN, data={axis.CONF_DEVICE: {
axis.CONF_HOST: '1.2.3.4'
}})
entry = MockConfigEntry(
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)
result = await flow.async_step_discovery(discovery_info={
config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_USERNAME: 'user',
config_flow.CONF_PASSWORD: 'pass',
config_flow.CONF_PORT: 81
config_flow.CONF_PORT: 81,
'properties': {'macaddress': '1234ABCD'}
})
print(result)
assert result['type'] == 'abort'

View file

@ -3,9 +3,10 @@ from unittest.mock import Mock, patch
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.camera import AxisCamera
DEVICE_DATA = {
device.CONF_HOST: '1.2.3.4',
@ -16,7 +17,7 @@ DEVICE_DATA = {
ENTRY_OPTIONS = {
device.CONF_CAMERA: True,
device.CONF_EVENTS: ['pir'],
device.CONF_EVENTS: True,
}
ENTRY_CONFIG = {
@ -53,6 +54,31 @@ async def test_device_setup():
(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():
"""Failed setup schedules a retry of setup."""
hass = Mock()

View file

@ -9,30 +9,28 @@ from tests.common import mock_coro, MockConfigEntry
async def test_setup(hass):
"""Test configured options for a device are loaded via config entry."""
with patch.object(hass, 'config_entries') as mock_config_entries, \
patch.object(axis, 'configured_devices', return_value={}):
with patch.object(hass.config_entries, 'flow') as mock_config_flow:
assert await async_setup_component(hass, axis.DOMAIN, {
axis.DOMAIN: {
'device_name': {
axis.CONF_HOST: '1.2.3.4',
axis.config_flow.CONF_HOST: '1.2.3.4',
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):
"""Test already configured device does not configure a second."""
with patch.object(hass, 'config_entries') as mock_config_entries, \
patch.object(axis, 'configured_devices', return_value={'1.2.3.4'}):
with patch.object(hass, 'config_entries') as mock_config_entries:
assert await async_setup_component(hass, axis.DOMAIN, {
axis.DOMAIN: {
'device_name': {
axis.CONF_HOST: '1.2.3.4'
axis.config_flow.CONF_HOST: '1.2.3.4'
}
}
})