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:
parent
dbcdc32f05
commit
60c787c2e6
10 changed files with 192 additions and 70 deletions
|
@ -4,7 +4,8 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant import config_entries
|
||||
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 .config_flow import DEVICE_SCHEMA
|
||||
|
@ -55,6 +56,12 @@ async def async_setup_entry(hass, config_entry):
|
|||
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):
|
||||
"""Populate default options for device."""
|
||||
from axis.vapix import VAPIX_IMAGE_FORMAT
|
||||
|
|
|
@ -42,6 +42,11 @@ class AxisBinarySensor(BinarySensorDevice):
|
|||
self.unsub_dispatcher = async_dispatcher_connect(
|
||||
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
|
||||
def update_callback(self, no_delay=False):
|
||||
"""Update the sensor's state, if needed.
|
||||
|
|
|
@ -56,6 +56,11 @@ class AxisCamera(MjpegCamera):
|
|||
self.unsub_dispatcher.append(async_dispatcher_connect(
|
||||
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
|
||||
def supported_features(self):
|
||||
"""Return supported features."""
|
||||
|
|
|
@ -159,6 +159,24 @@ class AxisNetworkDevice:
|
|||
"""Stop the event stream."""
|
||||
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):
|
||||
"""Create a Axis device."""
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Axis",
|
||||
"documentation": "https://www.home-assistant.io/components/axis",
|
||||
"requirements": [
|
||||
"axis==19"
|
||||
"axis==20"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
|
|
@ -192,7 +192,7 @@ av==6.1.2
|
|||
# avion==0.10
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==19
|
||||
axis==20
|
||||
|
||||
# homeassistant.components.baidu
|
||||
baidu-aip==1.6.6
|
||||
|
|
|
@ -61,7 +61,7 @@ apns2==0.3.0
|
|||
av==6.1.2
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==19
|
||||
axis==20
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows-homeassistant==0.7.2
|
||||
|
|
|
@ -26,9 +26,6 @@ async def test_configured_devices(hass):
|
|||
|
||||
async def test_flow_works(hass):
|
||||
"""Test that config flow works."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch('axis.AxisDevice') as mock_device:
|
||||
def mock_constructor(
|
||||
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.get_param.side_effect = mock_get_param
|
||||
|
||||
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
|
||||
})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
context={'source': 'user'}
|
||||
)
|
||||
|
||||
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['title'] == '{} - {}'.format(
|
||||
|
@ -162,15 +170,16 @@ async def test_flow_create_entry_more_entries(hass):
|
|||
|
||||
async def test_discovery_flow(hass):
|
||||
"""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())):
|
||||
result = await flow.async_step_discovery(discovery_info={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_PORT: 80,
|
||||
'properties': {'macaddress': '1234'}
|
||||
})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
data={
|
||||
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['step_id'] == 'user'
|
||||
|
@ -181,9 +190,6 @@ async def test_discovery_flow_known_device(hass):
|
|||
|
||||
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',
|
||||
return_value={'1234ABCD': {
|
||||
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.get_param.side_effect = mock_get_param
|
||||
|
||||
result = await flow.async_step_discovery(discovery_info={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_PORT: 80,
|
||||
'hostname': 'name',
|
||||
'properties': {'macaddress': '1234ABCD'}
|
||||
})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
data={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_PORT: 80,
|
||||
'hostname': 'name',
|
||||
'properties': {'macaddress': '1234ABCD'}
|
||||
},
|
||||
context={'source': 'discovery'}
|
||||
)
|
||||
|
||||
assert result['type'] == 'create_entry'
|
||||
|
||||
|
||||
async def test_discovery_flow_already_configured(hass):
|
||||
"""Test that discovery doesn't setup already configured devices."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=axis.DOMAIN,
|
||||
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)
|
||||
|
||||
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,
|
||||
'properties': {'macaddress': '1234ABCD'}
|
||||
})
|
||||
print(result)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
data={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 80,
|
||||
'hostname': 'name',
|
||||
'properties': {'macaddress': '1234ABCD'}
|
||||
},
|
||||
context={'source': 'discovery'}
|
||||
)
|
||||
|
||||
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."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
result = await flow.async_step_discovery(discovery_info={
|
||||
config_flow.CONF_HOST: '169.254.3.4'
|
||||
})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
data={config_flow.CONF_HOST: '169.254.3.4'},
|
||||
context={'source': 'discovery'}
|
||||
)
|
||||
|
||||
assert result['type'] == 'abort'
|
||||
assert result['reason'] == 'link_local_address'
|
||||
|
||||
|
||||
async def test_discovery_flow_bad_config_file(hass):
|
||||
"""Test that discovery with bad config files abort."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch('homeassistant.components.axis.config_flow.load_json',
|
||||
return_value={'1234ABCD': {
|
||||
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}}), \
|
||||
patch('homeassistant.components.axis.config_flow.DEVICE_SCHEMA',
|
||||
side_effect=config_flow.vol.Invalid('')):
|
||||
result = await flow.async_step_discovery(discovery_info={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
'properties': {'macaddress': '1234ABCD'}
|
||||
})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
data={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
'properties': {'macaddress': '1234ABCD'}
|
||||
},
|
||||
context={'source': 'discovery'}
|
||||
)
|
||||
|
||||
assert result['type'] == 'abort'
|
||||
assert result['reason'] == 'bad_config_file'
|
||||
|
||||
|
||||
async def test_import_flow_works(hass):
|
||||
"""Test that import flow works."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch('axis.AxisDevice') as mock_device:
|
||||
def mock_constructor(
|
||||
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.get_param.side_effect = mock_get_param
|
||||
|
||||
result = await flow.async_step_import(import_config={
|
||||
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_NAME: 'name'
|
||||
})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
data={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
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['title'] == '{} - {}'.format(
|
||||
|
@ -315,7 +331,7 @@ async def test_import_flow_works(hass):
|
|||
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: 80
|
||||
},
|
||||
config_flow.CONF_MAC: axis_lib.vapix.VAPIX_SERIAL_NUMBER,
|
||||
config_flow.CONF_MODEL: axis_lib.vapix.VAPIX_MODEL_ID,
|
||||
|
|
|
@ -70,6 +70,9 @@ async def test_device_signal_new_address(hass):
|
|||
await axis_device.async_setup()
|
||||
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'
|
||||
hass.config_entries.async_update_entry(entry, data=entry.data)
|
||||
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
|
||||
|
||||
|
||||
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():
|
||||
"""Failed setup schedules a retry of setup."""
|
||||
hass = Mock()
|
||||
|
|
|
@ -49,10 +49,11 @@ async def test_setup_entry(hass):
|
|||
entry = MockConfigEntry(
|
||||
domain=axis.DOMAIN, data={axis.device.CONF_MAC: '0123'})
|
||||
|
||||
mock_device = Mock()
|
||||
mock_device.async_setup.return_value = mock_coro(True)
|
||||
mock_device.async_update_device_registry.return_value = mock_coro(True)
|
||||
mock_device.serial.return_value = '1'
|
||||
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(
|
||||
|
@ -62,6 +63,7 @@ async def test_setup_entry(hass):
|
|||
assert await axis.async_setup_entry(hass, entry)
|
||||
|
||||
assert len(hass.data[axis.DOMAIN]) == 1
|
||||
assert '0123' in hass.data[axis.DOMAIN]
|
||||
|
||||
|
||||
async def test_setup_entry_fails(hass):
|
||||
|
@ -80,6 +82,28 @@ async def test_setup_entry_fails(hass):
|
|||
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):
|
||||
"""Test successful populate options."""
|
||||
entry = MockConfigEntry(domain=axis.DOMAIN, data={'device': {}})
|
||||
|
|
Loading…
Add table
Reference in a new issue