diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json index c7d84aa8cc3..abc1e2f17ec 100644 --- a/homeassistant/components/axis/.translations/en.json +++ b/homeassistant/components/axis/.translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured", "bad_config_file": "Bad data from config file", "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Discovered device not an Axis device" + "not_axis_device": "Discovered device not an Axis device", + "updated_configuration": "Updated device configuration with new host address" }, "error": { "already_configured": "Device is already configured", diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 20712979e0f..5c928aa9f31 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -29,6 +29,12 @@ async def async_setup_entry(hass, config_entry): if not await device.async_setup(): return False + # 0.104 introduced config entry unique id, this makes upgrading possible + if config_entry.unique_id is None: + hass.config_entries.async_update_entry( + config_entry, unique_id=device.api.vapix.params.system_serialnumber + ) + hass.data[DOMAIN][device.serial] = device await device.async_update_device_registry() diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 1d12e0b8d61..b3593179ffc 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -5,7 +5,7 @@ from datetime import timedelta from axis.event_stream import CLASS_INPUT, CLASS_OUTPUT from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import CONF_MAC, CONF_TRIGGER_TIME +from homeassistant.const import CONF_TRIGGER_TIME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time @@ -17,8 +17,7 @@ from .const import DOMAIN as AXIS_DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Axis binary sensor.""" - serial_number = config_entry.data[CONF_MAC] - device = hass.data[AXIS_DOMAIN][serial_number] + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] @callback def async_add_sensor(event_id): diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index a55e45dd374..6b82c938a99 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -11,7 +11,6 @@ from homeassistant.const import ( CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, - CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -32,8 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Axis camera video stream.""" filter_urllib3_logging() - serial_number = config_entry.data[CONF_MAC] - device = hass.data[AXIS_DOMAIN][serial_number] + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] config = { CONF_NAME: config_entry.data[CONF_NAME], diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 7d472b58687..6951da230d6 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -12,12 +12,10 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv from .const import CONF_MODEL, DOMAIN from .device import get_device -from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect +from .errors import AuthenticationRequired, CannotConnect AXIS_OUI = {"00408C", "ACCC8E", "B8A44F"} @@ -29,31 +27,8 @@ PLATFORMS = ["camera"] AXIS_INCLUDE = EVENT_TYPES + PLATFORMS -AXIS_DEFAULT_HOST = "192.168.0.90" -AXIS_DEFAULT_USERNAME = "root" -AXIS_DEFAULT_PASSWORD = "pass" DEFAULT_PORT = 80 -DEVICE_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string, - vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }, - extra=vol.ALLOW_EXTRA, -) - - -@callback -def configured_devices(hass): - """Return a set of the configured devices.""" - return { - entry.data[CONF_MAC]: entry - for entry in hass.config_entries.async_entries(DOMAIN) - } - class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Axis config flow.""" @@ -90,16 +65,13 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.serial_number = device.vapix.params.system_serialnumber - if self.serial_number in configured_devices(self.hass): - raise AlreadyConfigured + await self.async_set_unique_id(self.serial_number) + self._abort_if_unique_id_configured() self.model = device.vapix.params.prodnbr return await self._create_entry() - except AlreadyConfigured: - errors["base"] = "already_configured" - except AuthenticationRequired: errors["base"] = "faulty_credentials" @@ -147,40 +119,39 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title = f"{self.model} - {self.serial_number}" return self.async_create_entry(title=title, data=data) - async def _update_entry(self, entry, host): - """Update existing entry if it is the same device.""" + def _update_entry(self, entry, host, port): + """Update existing entry.""" + if ( + entry.data[CONF_DEVICE][CONF_HOST] == host + and entry.data[CONF_DEVICE][CONF_PORT] == port + ): + return self.async_abort(reason="already_configured") + entry.data[CONF_DEVICE][CONF_HOST] = host + entry.data[CONF_DEVICE][CONF_PORT] = port + self.hass.config_entries.async_update_entry(entry) + return self.async_abort(reason="updated_configuration") async def async_step_zeroconf(self, discovery_info): - """Prepare configuration for a discovered Axis device. + """Prepare configuration for a discovered Axis device.""" + serial_number = discovery_info["properties"]["macaddress"] - This flow is triggered by the discovery component. - """ - serialnumber = discovery_info["properties"]["macaddress"] - - if serialnumber[:6] not in AXIS_OUI: + if serial_number[:6] not in AXIS_OUI: return self.async_abort(reason="not_axis_device") if discovery_info[CONF_HOST].startswith("169.254"): return self.async_abort(reason="link_local_address") - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context["macaddress"] = serialnumber - - if any( - serialnumber == flow["context"]["macaddress"] - for flow in self._async_in_progress() - ): - return self.async_abort(reason="already_in_progress") - - 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") + for entry in self.hass.config_entries.async_entries(DOMAIN): + if serial_number == entry.unique_id: + return self._update_entry( + entry, + host=discovery_info[CONF_HOST], + port=discovery_info[CONF_PORT], + ) + await self.async_set_unique_id(serial_number) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { "name": discovery_info["hostname"][:-7], diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index b05c5b2fed0..85ad59268df 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -56,8 +56,8 @@ class AxisNetworkDevice: @property def serial(self): - """Return the mac of this device.""" - return self.config_entry.data[CONF_MAC] + """Return the serial number of this device.""" + return self.config_entry.unique_id async def async_update_device_registry(self): """Update device registry.""" diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 2dc23f3e466..7facd7060ad 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -23,7 +23,8 @@ "already_configured": "Device is already configured", "bad_config_file": "Bad data from config file", "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Discovered device not an Axis device" + "not_axis_device": "Discovered device not an Axis device", + "updated_configuration": "Updated device configuration with new host address" } } } diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index a64ffc3fa85..a83460bc529 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -3,7 +3,6 @@ from axis.event_stream import CLASS_OUTPUT from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_MAC from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -13,8 +12,7 @@ from .const import DOMAIN as AXIS_DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Axis switch.""" - serial_number = config_entry.data[CONF_MAC] - device = hass.data[AXIS_DOMAIN][serial_number] + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] @callback def async_add_switch(event_id): diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 550d87c7375..1792f901da9 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -1,28 +1,17 @@ """Test Axis config flow.""" from unittest.mock import Mock, patch +import pytest + +import homeassistant from homeassistant.components import axis from homeassistant.components.axis import config_flow +from .test_device import MAC, setup_axis_integration + from tests.common import MockConfigEntry, mock_coro -async def test_configured_devices(hass): - """Test that configured devices works as expected.""" - result = config_flow.configured_devices(hass) - - assert not result - - entry = MockConfigEntry( - domain=axis.DOMAIN, data={axis.config_flow.CONF_MAC: "1234"} - ) - entry.add_to_hass(hass) - - result = config_flow.configured_devices(hass) - - assert len(result) == 1 - - async def test_flow_works(hass): """Test that config flow works.""" with patch("axis.AxisDevice") as mock_device: @@ -76,22 +65,20 @@ async def test_flow_works(hass): async def test_flow_fails_already_configured(hass): """Test that config flow fails on already configured device.""" + await setup_axis_integration(hass) + flow = config_flow.AxisFlowHandler() flow.hass = hass - - entry = MockConfigEntry( - domain=axis.DOMAIN, data={axis.config_flow.CONF_MAC: "1234"} - ) - entry.add_to_hass(hass) + flow.context = {} mock_device = Mock() - mock_device.vapix.params.system_serialnumber = "1234" + mock_device.vapix.params.system_serialnumber = MAC with patch( "homeassistant.components.axis.config_flow.get_device", return_value=mock_coro(mock_device), - ): - result = await flow.async_step_user( + ), pytest.raises(homeassistant.data_entry_flow.AbortFlow): + await flow.async_step_user( user_input={ config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_USERNAME: "user", @@ -100,8 +87,6 @@ async def test_flow_fails_already_configured(hass): } ) - assert result["errors"] == {"base": "already_configured"} - async def test_flow_fails_faulty_credentials(hass): """Test that config flow fails on faulty credentials.""" @@ -198,21 +183,13 @@ async def test_zeroconf_flow(hass): async def test_zeroconf_flow_already_configured(hass): """Test that zeroconf doesn't setup already configured devices.""" - entry = MockConfigEntry( - domain=axis.DOMAIN, - data={ - axis.CONF_DEVICE: {axis.config_flow.CONF_HOST: "1.2.3.4"}, - axis.config_flow.CONF_MAC: "00408C12345", - }, - ) - entry.add_to_hass(hass) + device = await setup_axis_integration(hass) + assert device.host == "1.2.3.4" 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": "00408C12345"}, @@ -222,6 +199,31 @@ async def test_zeroconf_flow_already_configured(hass): assert result["type"] == "abort" assert result["reason"] == "already_configured" + assert device.host == "1.2.3.4" + + +async def test_zeroconf_flow_updated_configuration(hass): + """Test that zeroconf update configuration with new parameters.""" + device = await setup_axis_integration(hass) + assert device.host == "1.2.3.4" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + data={ + config_flow.CONF_HOST: "2.3.4.5", + config_flow.CONF_PORT: 8080, + "hostname": "name", + "properties": {"macaddress": MAC}, + }, + context={"source": "zeroconf"}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "updated_configuration" + assert device.host == "2.3.4.5" + assert ( + device.config_entry.data[config_flow.CONF_DEVICE][config_flow.CONF_PORT] == 8080 + ) async def test_zeroconf_flow_ignore_non_axis_device(hass): diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 7bcb1350fe8..b175d22cfb4 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -18,7 +18,7 @@ DEVICE_DATA = { axis.device.CONF_HOST: "1.2.3.4", axis.device.CONF_USERNAME: "username", axis.device.CONF_PASSWORD: "password", - axis.device.CONF_PORT: 1234, + axis.device.CONF_PORT: 80, } ENTRY_OPTIONS = {axis.device.CONF_CAMERA: True, axis.device.CONF_EVENTS: True} diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 6b40f7f2882..748bb539369 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -4,6 +4,8 @@ from unittest.mock import Mock, patch from homeassistant.components import axis from homeassistant.setup import async_setup_component +from .test_device import MAC, setup_axis_integration + from tests.common import MockConfigEntry, mock_coro @@ -28,22 +30,9 @@ async def test_setup_no_config(hass): async def test_setup_entry(hass): """Test successful setup 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) - + await setup_axis_integration(hass) assert len(hass.data[axis.DOMAIN]) == 1 - assert "0123" in hass.data[axis.DOMAIN] + assert MAC in hass.data[axis.DOMAIN] async def test_setup_entry_fails(hass): @@ -65,21 +54,10 @@ async def test_setup_entry_fails(hass): async def test_unload_entry(hass): """Test successful unload of entry.""" - entry = MockConfigEntry(domain=axis.DOMAIN, data={axis.device.CONF_MAC: "0123"}) + device = await setup_axis_integration(hass) + assert hass.data[axis.DOMAIN] - 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 await axis.async_unload_entry(hass, device.config_entry) assert not hass.data[axis.DOMAIN]