Make Axis utilise forward_entry_setups (#75178)

This commit is contained in:
Robert Svensson 2022-07-28 11:41:03 +02:00 committed by GitHub
parent 4f25b8d58e
commit c16db4c3e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 62 additions and 82 deletions

View file

@ -4,28 +4,36 @@ import logging
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_registry import async_migrate_entries from homeassistant.helpers.entity_registry import async_migrate_entries
from .const import DOMAIN as AXIS_DOMAIN from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS
from .device import AxisNetworkDevice from .device import AxisNetworkDevice, get_axis_device
from .errors import AuthenticationRequired, CannotConnect
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the Axis component.""" """Set up the Axis integration."""
hass.data.setdefault(AXIS_DOMAIN, {}) hass.data.setdefault(AXIS_DOMAIN, {})
device = AxisNetworkDevice(hass, config_entry) try:
api = await get_axis_device(hass, config_entry.data)
if not await device.async_setup(): except CannotConnect as err:
return False raise ConfigEntryNotReady from err
except AuthenticationRequired as err:
hass.data[AXIS_DOMAIN][config_entry.unique_id] = device raise ConfigEntryAuthFailed from err
device = hass.data[AXIS_DOMAIN][config_entry.unique_id] = AxisNetworkDevice(
hass, config_entry, api
)
await device.async_update_device_registry() await device.async_update_device_registry()
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
device.async_setup_events()
config_entry.add_update_listener(device.async_new_address_callback)
config_entry.async_on_unload( config_entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown)
) )

View file

@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from ipaddress import ip_address from ipaddress import ip_address
from types import MappingProxyType
from typing import Any from typing import Any
from urllib.parse import urlsplit from urllib.parse import urlsplit
@ -32,7 +33,7 @@ from .const import (
DEFAULT_VIDEO_SOURCE, DEFAULT_VIDEO_SOURCE,
DOMAIN as AXIS_DOMAIN, DOMAIN as AXIS_DOMAIN,
) )
from .device import AxisNetworkDevice, get_device from .device import AxisNetworkDevice, get_axis_device
from .errors import AuthenticationRequired, CannotConnect from .errors import AuthenticationRequired, CannotConnect
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"} AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"}
@ -66,13 +67,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
device = await get_device( device = await get_axis_device(self.hass, MappingProxyType(user_input))
self.hass,
host=user_input[CONF_HOST],
port=user_input[CONF_PORT],
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
)
serial = device.vapix.serial_number serial = device.vapix.serial_number
await self.async_set_unique_id(format_mac(serial)) await self.async_set_unique_id(format_mac(serial))

View file

@ -1,6 +1,8 @@
"""Axis network device abstraction.""" """Axis network device abstraction."""
import asyncio import asyncio
from types import MappingProxyType
from typing import Any
import async_timeout import async_timeout
import axis import axis
@ -24,7 +26,6 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
@ -50,15 +51,15 @@ from .errors import AuthenticationRequired, CannotConnect
class AxisNetworkDevice: class AxisNetworkDevice:
"""Manages a Axis device.""" """Manages a Axis device."""
def __init__(self, hass, config_entry): def __init__(self, hass, config_entry, api):
"""Initialize the device.""" """Initialize the device."""
self.hass = hass self.hass = hass
self.config_entry = config_entry self.config_entry = config_entry
self.available = True self.api = api
self.api = None self.available = True
self.fw_version = None self.fw_version = api.vapix.firmware_version
self.product_type = None self.product_type = api.vapix.product_type
@property @property
def host(self): def host(self):
@ -184,7 +185,7 @@ class AxisNetworkDevice:
sw_version=self.fw_version, sw_version=self.fw_version,
) )
async def use_mqtt(self, hass: HomeAssistant, component: str) -> None: async def async_use_mqtt(self, hass: HomeAssistant, component: str) -> None:
"""Set up to use MQTT.""" """Set up to use MQTT."""
try: try:
status = await self.api.vapix.mqtt.get_client_status() status = await self.api.vapix.mqtt.get_client_status()
@ -209,50 +210,18 @@ class AxisNetworkDevice:
# Setup and teardown methods # Setup and teardown methods
async def async_setup(self): def async_setup_events(self):
"""Set up the device.""" """Set up the device events."""
try:
self.api = await get_device( if self.option_events:
self.hass, self.api.stream.connection_status_callback.append(
host=self.host, self.async_connection_status_callback
port=self.port,
username=self.username,
password=self.password,
) )
self.api.enable_events(event_callback=self.async_event_callback)
self.api.stream.start()
except CannotConnect as err: if self.api.vapix.mqtt:
raise ConfigEntryNotReady from err async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt)
except AuthenticationRequired as err:
raise ConfigEntryAuthFailed from err
self.fw_version = self.api.vapix.firmware_version
self.product_type = self.api.vapix.product_type
async def start_platforms():
await asyncio.gather(
*(
self.hass.config_entries.async_forward_entry_setup(
self.config_entry, platform
)
for platform in PLATFORMS
)
)
if self.option_events:
self.api.stream.connection_status_callback.append(
self.async_connection_status_callback
)
self.api.enable_events(event_callback=self.async_event_callback)
self.api.stream.start()
if self.api.vapix.mqtt:
async_when_setup(self.hass, MQTT_DOMAIN, self.use_mqtt)
self.hass.async_create_task(start_platforms())
self.config_entry.add_update_listener(self.async_new_address_callback)
return True
@callback @callback
def disconnect_from_stream(self): def disconnect_from_stream(self):
@ -274,14 +243,21 @@ class AxisNetworkDevice:
) )
async def get_device( async def get_axis_device(
hass: HomeAssistant, host: str, port: int, username: str, password: str hass: HomeAssistant,
config: MappingProxyType[str, Any],
) -> axis.AxisDevice: ) -> axis.AxisDevice:
"""Create a Axis device.""" """Create a Axis device."""
session = get_async_client(hass, verify_ssl=False) session = get_async_client(hass, verify_ssl=False)
device = axis.AxisDevice( device = axis.AxisDevice(
Configuration(session, host, port=port, username=username, password=password) Configuration(
session,
config[CONF_HOST],
port=config[CONF_PORT],
username=config[CONF_USERNAME],
password=config[CONF_PASSWORD],
)
) )
try: try:
@ -291,11 +267,13 @@ async def get_device(
return device return device
except axis.Unauthorized as err: except axis.Unauthorized as err:
LOGGER.warning("Connected to device at %s but not registered", host) LOGGER.warning(
"Connected to device at %s but not registered", config[CONF_HOST]
)
raise AuthenticationRequired from err raise AuthenticationRequired from err
except (asyncio.TimeoutError, axis.RequestError) as err: except (asyncio.TimeoutError, axis.RequestError) as err:
LOGGER.error("Error connecting to the Axis device at %s", host) LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST])
raise CannotConnect from err raise CannotConnect from err
except axis.AxisException as err: except axis.AxisException as err:

View file

@ -123,7 +123,7 @@ async def test_flow_fails_faulty_credentials(hass):
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
with patch( with patch(
"homeassistant.components.axis.config_flow.get_device", "homeassistant.components.axis.config_flow.get_axis_device",
side_effect=config_flow.AuthenticationRequired, side_effect=config_flow.AuthenticationRequired,
): ):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@ -149,7 +149,7 @@ async def test_flow_fails_cannot_connect(hass):
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
with patch( with patch(
"homeassistant.components.axis.config_flow.get_device", "homeassistant.components.axis.config_flow.get_axis_device",
side_effect=config_flow.CannotConnect, side_effect=config_flow.CannotConnect,
): ):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(

View file

@ -441,7 +441,7 @@ async def test_device_reset(hass):
async def test_device_not_accessible(hass): async def test_device_not_accessible(hass):
"""Failed setup schedules a retry of setup.""" """Failed setup schedules a retry of setup."""
with patch.object(axis.device, "get_device", side_effect=axis.errors.CannotConnect): with patch.object(axis, "get_axis_device", side_effect=axis.errors.CannotConnect):
await setup_axis_integration(hass) await setup_axis_integration(hass)
assert hass.data[AXIS_DOMAIN] == {} assert hass.data[AXIS_DOMAIN] == {}
@ -449,7 +449,7 @@ async def test_device_not_accessible(hass):
async def test_device_trigger_reauth_flow(hass): async def test_device_trigger_reauth_flow(hass):
"""Failed authentication trigger a reauthentication flow.""" """Failed authentication trigger a reauthentication flow."""
with patch.object( with patch.object(
axis.device, "get_device", side_effect=axis.errors.AuthenticationRequired axis, "get_axis_device", side_effect=axis.errors.AuthenticationRequired
), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init:
await setup_axis_integration(hass) await setup_axis_integration(hass)
mock_flow_init.assert_called_once() mock_flow_init.assert_called_once()
@ -458,7 +458,7 @@ async def test_device_trigger_reauth_flow(hass):
async def test_device_unknown_error(hass): async def test_device_unknown_error(hass):
"""Unknown errors are handled.""" """Unknown errors are handled."""
with patch.object(axis.device, "get_device", side_effect=Exception): with patch.object(axis, "get_axis_device", side_effect=Exception):
await setup_axis_integration(hass) await setup_axis_integration(hass)
assert hass.data[AXIS_DOMAIN] == {} assert hass.data[AXIS_DOMAIN] == {}
@ -468,7 +468,7 @@ async def test_new_event_sends_signal(hass):
entry = Mock() entry = Mock()
entry.data = ENTRY_CONFIG entry.data = ENTRY_CONFIG
axis_device = axis.device.AxisNetworkDevice(hass, entry) axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock())
with patch.object(axis.device, "async_dispatcher_send") as mock_dispatch_send: with patch.object(axis.device, "async_dispatcher_send") as mock_dispatch_send:
axis_device.async_event_callback(action=OPERATION_INITIALIZED, event_id="event") axis_device.async_event_callback(action=OPERATION_INITIALIZED, event_id="event")
@ -484,8 +484,7 @@ async def test_shutdown():
entry = Mock() entry = Mock()
entry.data = ENTRY_CONFIG entry.data = ENTRY_CONFIG
axis_device = axis.device.AxisNetworkDevice(hass, entry) axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock())
axis_device.api = Mock()
await axis_device.shutdown(None) await axis_device.shutdown(None)
@ -497,7 +496,7 @@ async def test_get_device_fails(hass):
with patch( with patch(
"axis.vapix.Vapix.request", side_effect=axislib.Unauthorized "axis.vapix.Vapix.request", side_effect=axislib.Unauthorized
), pytest.raises(axis.errors.AuthenticationRequired): ), pytest.raises(axis.errors.AuthenticationRequired):
await axis.device.get_device(hass, host="", port="", username="", password="") await axis.device.get_axis_device(hass, ENTRY_CONFIG)
async def test_get_device_device_unavailable(hass): async def test_get_device_device_unavailable(hass):
@ -505,7 +504,7 @@ async def test_get_device_device_unavailable(hass):
with patch( with patch(
"axis.vapix.Vapix.request", side_effect=axislib.RequestError "axis.vapix.Vapix.request", side_effect=axislib.RequestError
), pytest.raises(axis.errors.CannotConnect): ), pytest.raises(axis.errors.CannotConnect):
await axis.device.get_device(hass, host="", port="", username="", password="") await axis.device.get_axis_device(hass, ENTRY_CONFIG)
async def test_get_device_unknown_error(hass): async def test_get_device_unknown_error(hass):
@ -513,4 +512,4 @@ async def test_get_device_unknown_error(hass):
with patch( with patch(
"axis.vapix.Vapix.request", side_effect=axislib.AxisException "axis.vapix.Vapix.request", side_effect=axislib.AxisException
), pytest.raises(axis.errors.AuthenticationRequired): ), pytest.raises(axis.errors.AuthenticationRequired):
await axis.device.get_device(hass, host="", port="", username="", password="") await axis.device.get_axis_device(hass, ENTRY_CONFIG)