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.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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue