Axis component support unloading entries (#22692)

* Add support for unloading entries

* Improve config entry tests

* Improve coverage for device

* Remove callback when relevant
This commit is contained in:
Robert Svensson 2019-04-16 00:06:45 +02:00 committed by Paulus Schoutsen
parent dbcdc32f05
commit 60c787c2e6
10 changed files with 192 additions and 70 deletions

View file

@ -4,7 +4,8 @@ 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_NAME, CONF_TRIGGER_TIME, EVENT_HOMEASSISTANT_STOP) CONF_DEVICE, CONF_MAC, CONF_NAME, CONF_TRIGGER_TIME,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from .config_flow import DEVICE_SCHEMA from .config_flow import DEVICE_SCHEMA
@ -55,6 +56,12 @@ async def async_setup_entry(hass, config_entry):
return True return True
async def async_unload_entry(hass, config_entry):
"""Unload Axis device config entry."""
device = hass.data[DOMAIN].pop(config_entry.data[CONF_MAC])
return await device.async_reset()
async def async_populate_options(hass, config_entry): async def async_populate_options(hass, config_entry):
"""Populate default options for device.""" """Populate default options for device."""
from axis.vapix import VAPIX_IMAGE_FORMAT from axis.vapix import VAPIX_IMAGE_FORMAT

View file

@ -42,6 +42,11 @@ class AxisBinarySensor(BinarySensorDevice):
self.unsub_dispatcher = async_dispatcher_connect( self.unsub_dispatcher = async_dispatcher_connect(
self.hass, self.device.event_reachable, self.update_callback) self.hass, self.device.event_reachable, self.update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self.event.remove_callback(self.update_callback)
self.unsub_dispatcher()
@callback @callback
def update_callback(self, no_delay=False): def update_callback(self, no_delay=False):
"""Update the sensor's state, if needed. """Update the sensor's state, if needed.

View file

@ -56,6 +56,11 @@ class AxisCamera(MjpegCamera):
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))
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
for unsub_dispatcher in self.unsub_dispatcher:
unsub_dispatcher()
@property @property
def supported_features(self): def supported_features(self):
"""Return supported features.""" """Return supported features."""

View file

@ -159,6 +159,24 @@ class AxisNetworkDevice:
"""Stop the event stream.""" """Stop the event stream."""
self.api.stop() self.api.stop()
async def async_reset(self):
"""Reset this device to default state."""
self.api.stop()
if self.config_entry.options[CONF_CAMERA]:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, 'camera')
if self.config_entry.options[CONF_EVENTS]:
await self.hass.config_entries.async_forward_entry_unload(
self.config_entry, 'binary_sensor')
for unsub_dispatcher in self.listeners:
unsub_dispatcher()
self.listeners = []
return True
async def get_device(hass, config): async def get_device(hass, config):
"""Create a Axis device.""" """Create a Axis device."""

View file

@ -3,7 +3,7 @@
"name": "Axis", "name": "Axis",
"documentation": "https://www.home-assistant.io/components/axis", "documentation": "https://www.home-assistant.io/components/axis",
"requirements": [ "requirements": [
"axis==19" "axis==20"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View file

@ -192,7 +192,7 @@ av==6.1.2
# avion==0.10 # avion==0.10
# homeassistant.components.axis # homeassistant.components.axis
axis==19 axis==20
# homeassistant.components.baidu # homeassistant.components.baidu
baidu-aip==1.6.6 baidu-aip==1.6.6

View file

@ -61,7 +61,7 @@ apns2==0.3.0
av==6.1.2 av==6.1.2
# homeassistant.components.axis # homeassistant.components.axis
axis==19 axis==20
# homeassistant.components.zha # homeassistant.components.zha
bellows-homeassistant==0.7.2 bellows-homeassistant==0.7.2

View file

@ -26,9 +26,6 @@ async def test_configured_devices(hass):
async def test_flow_works(hass): async def test_flow_works(hass):
"""Test that config flow works.""" """Test that config flow works."""
flow = config_flow.AxisFlowHandler()
flow.hass = hass
with patch('axis.AxisDevice') as mock_device: with patch('axis.AxisDevice') as mock_device:
def mock_constructor( def mock_constructor(
loop, host, username, password, port, web_proto): loop, host, username, password, port, web_proto):
@ -48,12 +45,23 @@ async def test_flow_works(hass):
mock_device.vapix.load_params.return_value = Mock() mock_device.vapix.load_params.return_value = Mock()
mock_device.vapix.get_param.side_effect = mock_get_param mock_device.vapix.get_param.side_effect = mock_get_param
result = await flow.async_step_user(user_input={ result = await hass.config_entries.flow.async_init(
config_flow.CONF_HOST: '1.2.3.4', config_flow.DOMAIN,
config_flow.CONF_USERNAME: 'user', context={'source': 'user'}
config_flow.CONF_PASSWORD: 'pass', )
config_flow.CONF_PORT: 81
}) assert result['type'] == 'form'
assert result['step_id'] == 'user'
result = await hass.config_entries.flow.async_configure(
result['flow_id'],
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['type'] == 'create_entry' assert result['type'] == 'create_entry'
assert result['title'] == '{} - {}'.format( assert result['title'] == '{} - {}'.format(
@ -162,15 +170,16 @@ async def test_flow_create_entry_more_entries(hass):
async def test_discovery_flow(hass): async def test_discovery_flow(hass):
"""Test that discovery for new devices work.""" """Test that discovery for new devices work."""
flow = config_flow.AxisFlowHandler()
flow.hass = hass
with patch.object(axis, 'get_device', return_value=mock_coro(Mock())): with patch.object(axis, 'get_device', return_value=mock_coro(Mock())):
result = await flow.async_step_discovery(discovery_info={ result = await hass.config_entries.flow.async_init(
config_flow.CONF_HOST: '1.2.3.4', config_flow.DOMAIN,
config_flow.CONF_PORT: 80, data={
'properties': {'macaddress': '1234'} config_flow.CONF_HOST: '1.2.3.4',
}) config_flow.CONF_PORT: 80,
'properties': {'macaddress': '1234'}
},
context={'source': 'discovery'}
)
assert result['type'] == 'form' assert result['type'] == 'form'
assert result['step_id'] == 'user' assert result['step_id'] == 'user'
@ -181,9 +190,6 @@ async def test_discovery_flow_known_device(hass):
This is legacy support from devices registered with configurator. This is legacy support from devices registered with configurator.
""" """
flow = config_flow.AxisFlowHandler()
flow.hass = hass
with patch('homeassistant.components.axis.config_flow.load_json', with patch('homeassistant.components.axis.config_flow.load_json',
return_value={'1234ABCD': { return_value={'1234ABCD': {
config_flow.CONF_HOST: '2.3.4.5', config_flow.CONF_HOST: '2.3.4.5',
@ -209,21 +215,22 @@ async def test_discovery_flow_known_device(hass):
mock_device.vapix.load_params.return_value = Mock() mock_device.vapix.load_params.return_value = Mock()
mock_device.vapix.get_param.side_effect = mock_get_param mock_device.vapix.get_param.side_effect = mock_get_param
result = await flow.async_step_discovery(discovery_info={ result = await hass.config_entries.flow.async_init(
config_flow.CONF_HOST: '1.2.3.4', config_flow.DOMAIN,
config_flow.CONF_PORT: 80, data={
'hostname': 'name', config_flow.CONF_HOST: '1.2.3.4',
'properties': {'macaddress': '1234ABCD'} config_flow.CONF_PORT: 80,
}) 'hostname': 'name',
'properties': {'macaddress': '1234ABCD'}
},
context={'source': 'discovery'}
)
assert result['type'] == 'create_entry' assert result['type'] == 'create_entry'
async def test_discovery_flow_already_configured(hass): async def test_discovery_flow_already_configured(hass):
"""Test that discovery doesn't setup already configured devices.""" """Test that discovery doesn't setup already configured devices."""
flow = config_flow.AxisFlowHandler()
flow.hass = hass
entry = MockConfigEntry( entry = MockConfigEntry(
domain=axis.DOMAIN, domain=axis.DOMAIN,
data={axis.CONF_DEVICE: {axis.config_flow.CONF_HOST: '1.2.3.4'}, data={axis.CONF_DEVICE: {axis.config_flow.CONF_HOST: '1.2.3.4'},
@ -231,34 +238,37 @@ async def test_discovery_flow_already_configured(hass):
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
result = await flow.async_step_discovery(discovery_info={ result = await hass.config_entries.flow.async_init(
config_flow.CONF_HOST: '1.2.3.4', config_flow.DOMAIN,
config_flow.CONF_USERNAME: 'user', data={
config_flow.CONF_PASSWORD: 'pass', config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_PORT: 81, config_flow.CONF_USERNAME: 'user',
'properties': {'macaddress': '1234ABCD'} config_flow.CONF_PASSWORD: 'pass',
}) config_flow.CONF_PORT: 80,
print(result) 'hostname': 'name',
'properties': {'macaddress': '1234ABCD'}
},
context={'source': 'discovery'}
)
assert result['type'] == 'abort' assert result['type'] == 'abort'
assert result['reason'] == 'already_configured'
async def test_discovery_flow_link_local_address(hass): async def test_discovery_flow_ignore_link_local_address(hass):
"""Test that discovery doesn't setup devices with link local addresses.""" """Test that discovery doesn't setup devices with link local addresses."""
flow = config_flow.AxisFlowHandler() result = await hass.config_entries.flow.async_init(
flow.hass = hass config_flow.DOMAIN,
data={config_flow.CONF_HOST: '169.254.3.4'},
result = await flow.async_step_discovery(discovery_info={ context={'source': 'discovery'}
config_flow.CONF_HOST: '169.254.3.4' )
})
assert result['type'] == 'abort' assert result['type'] == 'abort'
assert result['reason'] == 'link_local_address'
async def test_discovery_flow_bad_config_file(hass): async def test_discovery_flow_bad_config_file(hass):
"""Test that discovery with bad config files abort.""" """Test that discovery with bad config files abort."""
flow = config_flow.AxisFlowHandler()
flow.hass = hass
with patch('homeassistant.components.axis.config_flow.load_json', with patch('homeassistant.components.axis.config_flow.load_json',
return_value={'1234ABCD': { return_value={'1234ABCD': {
config_flow.CONF_HOST: '2.3.4.5', config_flow.CONF_HOST: '2.3.4.5',
@ -267,19 +277,21 @@ async def test_discovery_flow_bad_config_file(hass):
config_flow.CONF_PORT: 80}}), \ config_flow.CONF_PORT: 80}}), \
patch('homeassistant.components.axis.config_flow.DEVICE_SCHEMA', patch('homeassistant.components.axis.config_flow.DEVICE_SCHEMA',
side_effect=config_flow.vol.Invalid('')): side_effect=config_flow.vol.Invalid('')):
result = await flow.async_step_discovery(discovery_info={ result = await hass.config_entries.flow.async_init(
config_flow.CONF_HOST: '1.2.3.4', config_flow.DOMAIN,
'properties': {'macaddress': '1234ABCD'} data={
}) config_flow.CONF_HOST: '1.2.3.4',
'properties': {'macaddress': '1234ABCD'}
},
context={'source': 'discovery'}
)
assert result['type'] == 'abort' assert result['type'] == 'abort'
assert result['reason'] == 'bad_config_file'
async def test_import_flow_works(hass): async def test_import_flow_works(hass):
"""Test that import flow works.""" """Test that import flow works."""
flow = config_flow.AxisFlowHandler()
flow.hass = hass
with patch('axis.AxisDevice') as mock_device: with patch('axis.AxisDevice') as mock_device:
def mock_constructor( def mock_constructor(
loop, host, username, password, port, web_proto): loop, host, username, password, port, web_proto):
@ -299,13 +311,17 @@ async def test_import_flow_works(hass):
mock_device.vapix.load_params.return_value = Mock() mock_device.vapix.load_params.return_value = Mock()
mock_device.vapix.get_param.side_effect = mock_get_param mock_device.vapix.get_param.side_effect = mock_get_param
result = await flow.async_step_import(import_config={ result = await hass.config_entries.flow.async_init(
config_flow.CONF_HOST: '1.2.3.4', config_flow.DOMAIN,
config_flow.CONF_USERNAME: 'user', data={
config_flow.CONF_PASSWORD: 'pass', config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_PORT: 81, config_flow.CONF_USERNAME: 'user',
config_flow.CONF_NAME: 'name' config_flow.CONF_PASSWORD: 'pass',
}) config_flow.CONF_PORT: 80,
config_flow.CONF_NAME: 'name'
},
context={'source': 'import'}
)
assert result['type'] == 'create_entry' assert result['type'] == 'create_entry'
assert result['title'] == '{} - {}'.format( assert result['title'] == '{} - {}'.format(
@ -315,7 +331,7 @@ async def test_import_flow_works(hass):
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: 80
}, },
config_flow.CONF_MAC: axis_lib.vapix.VAPIX_SERIAL_NUMBER, config_flow.CONF_MAC: axis_lib.vapix.VAPIX_SERIAL_NUMBER,
config_flow.CONF_MODEL: axis_lib.vapix.VAPIX_MODEL_ID, config_flow.CONF_MODEL: axis_lib.vapix.VAPIX_MODEL_ID,

View file

@ -70,6 +70,9 @@ async def test_device_signal_new_address(hass):
await axis_device.async_setup() await axis_device.async_setup()
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert len(axis_device.listeners) == 1
entry.data[device.CONF_DEVICE][device.CONF_HOST] = '2.3.4.5' entry.data[device.CONF_DEVICE][device.CONF_HOST] = '2.3.4.5'
hass.config_entries.async_update_entry(entry, data=entry.data) hass.config_entries.async_update_entry(entry, data=entry.data)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -79,6 +82,50 @@ async def test_device_signal_new_address(hass):
assert len(new_address_mock.mock_calls) == 1 assert len(new_address_mock.mock_calls) == 1
async def test_device_unavailable(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(device, 'async_dispatcher_send') as mock_dispatcher:
await axis_device.async_setup()
await hass.async_block_till_done()
axis_device.async_connection_status_callback(status=False)
assert not axis_device.available
assert len(mock_dispatcher.mock_calls) == 1
async def test_device_reset(hass):
"""Successfully reset device."""
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)):
await axis_device.async_setup()
await hass.async_block_till_done()
await axis_device.async_reset()
assert len(api.stop.mock_calls) == 1
assert len(hass.states.async_all()) == 0
assert len(axis_device.listeners) == 0
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()

View file

@ -49,10 +49,11 @@ async def test_setup_entry(hass):
entry = MockConfigEntry( entry = MockConfigEntry(
domain=axis.DOMAIN, data={axis.device.CONF_MAC: '0123'}) domain=axis.DOMAIN, data={axis.device.CONF_MAC: '0123'})
mock_device = Mock() mock_device = axis.AxisNetworkDevice(hass, entry)
mock_device.async_setup.return_value = mock_coro(True) mock_device.async_setup = Mock(return_value=mock_coro(True))
mock_device.async_update_device_registry.return_value = mock_coro(True) mock_device.async_update_device_registry = \
mock_device.serial.return_value = '1' Mock(return_value=mock_coro(True))
mock_device.async_reset = Mock(return_value=mock_coro(True))
with patch.object(axis, 'AxisNetworkDevice') as mock_device_class, \ with patch.object(axis, 'AxisNetworkDevice') as mock_device_class, \
patch.object( patch.object(
@ -62,6 +63,7 @@ async def test_setup_entry(hass):
assert await axis.async_setup_entry(hass, entry) assert await axis.async_setup_entry(hass, entry)
assert len(hass.data[axis.DOMAIN]) == 1 assert len(hass.data[axis.DOMAIN]) == 1
assert '0123' in hass.data[axis.DOMAIN]
async def test_setup_entry_fails(hass): async def test_setup_entry_fails(hass):
@ -80,6 +82,28 @@ async def test_setup_entry_fails(hass):
assert not hass.data[axis.DOMAIN] assert not hass.data[axis.DOMAIN]
async def test_unload_entry(hass):
"""Test successful unload of entry."""
entry = MockConfigEntry(
domain=axis.DOMAIN, data={axis.device.CONF_MAC: '0123'})
mock_device = axis.AxisNetworkDevice(hass, entry)
mock_device.async_setup = Mock(return_value=mock_coro(True))
mock_device.async_update_device_registry = \
Mock(return_value=mock_coro(True))
mock_device.async_reset = Mock(return_value=mock_coro(True))
with patch.object(axis, 'AxisNetworkDevice') as mock_device_class, \
patch.object(
axis, 'async_populate_options', return_value=mock_coro(True)):
mock_device_class.return_value = mock_device
assert await axis.async_setup_entry(hass, entry)
assert await axis.async_unload_entry(hass, entry)
assert not hass.data[axis.DOMAIN]
async def test_populate_options(hass): async def test_populate_options(hass):
"""Test successful populate options.""" """Test successful populate options."""
entry = MockConfigEntry(domain=axis.DOMAIN, data={'device': {}}) entry = MockConfigEntry(domain=axis.DOMAIN, data={'device': {}})