Bump nam backend library (#72771)

* Update config flow

* Fix discovery with auth

* Call check_credentials() on init

* Update tests

* Bump library version

* Cleaning

* Return dataclass instead of tuple

* Fix pylint error
This commit is contained in:
Maciej Bieniek 2022-06-07 18:56:11 +02:00 committed by GitHub
parent a5dc7c5f28
commit 0b5c0f8249
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 99 additions and 75 deletions

View file

@ -52,11 +52,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
options = ConnectionOptions(host=host, username=username, password=password) options = ConnectionOptions(host=host, username=username, password=password)
try: try:
nam = await NettigoAirMonitor.create(websession, options) nam = await NettigoAirMonitor.create(websession, options)
except AuthFailed as err:
raise ConfigEntryAuthFailed from err
except (ApiError, ClientError, ClientConnectorError, asyncio.TimeoutError) as err: except (ApiError, ClientError, ClientConnectorError, asyncio.TimeoutError) as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
try:
await nam.async_check_credentials()
except AuthFailed as err:
raise ConfigEntryAuthFailed from err
coordinator = NAMDataUpdateCoordinator(hass, nam, entry.unique_id) coordinator = NAMDataUpdateCoordinator(hass, nam, entry.unique_id)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass
import logging import logging
from typing import Any from typing import Any
@ -27,6 +28,15 @@ from homeassistant.helpers.device_registry import format_mac
from .const import DOMAIN from .const import DOMAIN
@dataclass
class NamConfig:
"""NAM device configuration class."""
mac_address: str
auth_enabled: bool
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AUTH_SCHEMA = vol.Schema( AUTH_SCHEMA = vol.Schema(
@ -34,15 +44,31 @@ AUTH_SCHEMA = vol.Schema(
) )
async def async_get_mac(hass: HomeAssistant, host: str, data: dict[str, Any]) -> str: async def async_get_config(hass: HomeAssistant, host: str) -> NamConfig:
"""Get device MAC address.""" """Get device MAC address and auth_enabled property."""
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)) options = ConnectionOptions(host)
nam = await NettigoAirMonitor.create(websession, options) nam = await NettigoAirMonitor.create(websession, options)
async with async_timeout.timeout(10): async with async_timeout.timeout(10):
return await nam.async_get_mac_address() mac = await nam.async_get_mac_address()
return NamConfig(mac, nam.auth_enabled)
async def async_check_credentials(
hass: HomeAssistant, host: str, data: dict[str, Any]
) -> None:
"""Check if credentials are valid."""
websession = async_get_clientsession(hass)
options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD))
nam = await NettigoAirMonitor.create(websession, options)
async with async_timeout.timeout(10):
await nam.async_check_credentials()
class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@ -54,6 +80,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Initialize flow.""" """Initialize flow."""
self.host: str self.host: str
self.entry: config_entries.ConfigEntry self.entry: config_entries.ConfigEntry
self._config: NamConfig
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -65,9 +92,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self.host = user_input[CONF_HOST] self.host = user_input[CONF_HOST]
try: try:
mac = await async_get_mac(self.hass, self.host, {}) config = await async_get_config(self.hass, self.host)
except AuthFailed:
return await self.async_step_credentials()
except (ApiError, ClientConnectorError, asyncio.TimeoutError): except (ApiError, ClientConnectorError, asyncio.TimeoutError):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except CannotGetMac: except CannotGetMac:
@ -76,9 +101,12 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
await self.async_set_unique_id(format_mac(mac)) await self.async_set_unique_id(format_mac(config.mac_address))
self._abort_if_unique_id_configured({CONF_HOST: self.host}) self._abort_if_unique_id_configured({CONF_HOST: self.host})
if config.auth_enabled is True:
return await self.async_step_credentials()
return self.async_create_entry( return self.async_create_entry(
title=self.host, title=self.host,
data=user_input, data=user_input,
@ -98,19 +126,15 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
mac = await async_get_mac(self.hass, self.host, user_input) await async_check_credentials(self.hass, self.host, user_input)
except AuthFailed: except AuthFailed:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except (ApiError, ClientConnectorError, asyncio.TimeoutError): except (ApiError, ClientConnectorError, asyncio.TimeoutError):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except CannotGetMac:
return self.async_abort(reason="device_unsupported")
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_configured({CONF_HOST: self.host})
return self.async_create_entry( return self.async_create_entry(
title=self.host, title=self.host,
@ -132,15 +156,13 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_HOST: self.host}) self._async_abort_entries_match({CONF_HOST: self.host})
try: try:
mac = await async_get_mac(self.hass, self.host, {}) self._config = await async_get_config(self.hass, self.host)
except AuthFailed:
return await self.async_step_credentials()
except (ApiError, ClientConnectorError, asyncio.TimeoutError): except (ApiError, ClientConnectorError, asyncio.TimeoutError):
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
except CannotGetMac: except CannotGetMac:
return self.async_abort(reason="device_unsupported") return self.async_abort(reason="device_unsupported")
await self.async_set_unique_id(format_mac(mac)) await self.async_set_unique_id(format_mac(self._config.mac_address))
self._abort_if_unique_id_configured({CONF_HOST: self.host}) self._abort_if_unique_id_configured({CONF_HOST: self.host})
return await self.async_step_confirm_discovery() return await self.async_step_confirm_discovery()
@ -157,6 +179,9 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data={CONF_HOST: self.host}, data={CONF_HOST: self.host},
) )
if self._config.auth_enabled is True:
return await self.async_step_credentials()
self._set_confirm_only() self._set_confirm_only()
return self.async_show_form( return self.async_show_form(
@ -181,7 +206,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
await async_get_mac(self.hass, self.host, user_input) await async_check_credentials(self.hass, self.host, user_input)
except (ApiError, AuthFailed, ClientConnectorError, asyncio.TimeoutError): except (ApiError, AuthFailed, ClientConnectorError, asyncio.TimeoutError):
return self.async_abort(reason="reauth_unsuccessful") return self.async_abort(reason="reauth_unsuccessful")
else: else:

View file

@ -3,7 +3,7 @@
"name": "Nettigo Air Monitor", "name": "Nettigo Air Monitor",
"documentation": "https://www.home-assistant.io/integrations/nam", "documentation": "https://www.home-assistant.io/integrations/nam",
"codeowners": ["@bieniu"], "codeowners": ["@bieniu"],
"requirements": ["nettigo-air-monitor==1.2.4"], "requirements": ["nettigo-air-monitor==1.3.0"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "type": "_http._tcp.local.",

View file

@ -1077,7 +1077,7 @@ netdisco==3.0.0
netmap==0.7.0.2 netmap==0.7.0.2
# homeassistant.components.nam # homeassistant.components.nam
nettigo-air-monitor==1.2.4 nettigo-air-monitor==1.3.0
# homeassistant.components.neurio_energy # homeassistant.components.neurio_energy
neurio==0.3.1 neurio==0.3.1

View file

@ -745,7 +745,7 @@ netdisco==3.0.0
netmap==0.7.0.2 netmap==0.7.0.2
# homeassistant.components.nam # homeassistant.components.nam
nettigo-air-monitor==1.2.4 nettigo-air-monitor==1.3.0
# homeassistant.components.nexia # homeassistant.components.nexia
nexia==1.0.1 nexia==1.0.1

View file

@ -23,6 +23,8 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo(
) )
VALID_CONFIG = {"host": "10.10.2.3"} VALID_CONFIG = {"host": "10.10.2.3"}
VALID_AUTH = {"username": "fake_username", "password": "fake_password"} VALID_AUTH = {"username": "fake_username", "password": "fake_password"}
DEVICE_CONFIG = {"www_basicauth_enabled": False}
DEVICE_CONFIG_AUTH = {"www_basicauth_enabled": True}
async def test_form_create_entry_without_auth(hass): async def test_form_create_entry_without_auth(hass):
@ -34,7 +36,10 @@ async def test_form_create_entry_without_auth(hass):
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
assert result["errors"] == {} assert result["errors"] == {}
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
return_value=DEVICE_CONFIG,
), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
return_value="aa:bb:cc:dd:ee:ff", return_value="aa:bb:cc:dd:ee:ff",
), patch( ), patch(
@ -62,24 +67,22 @@ async def test_form_create_entry_with_auth(hass):
assert result["errors"] == {} assert result["errors"] == {}
with patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize", "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
side_effect=AuthFailed("Auth Error"), return_value=DEVICE_CONFIG_AUTH,
): ), patch(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
VALID_CONFIG,
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "credentials"
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
return_value="aa:bb:cc:dd:ee:ff", return_value="aa:bb:cc:dd:ee:ff",
), patch( ), patch(
"homeassistant.components.nam.async_setup_entry", return_value=True "homeassistant.components.nam.async_setup_entry", return_value=True
) as mock_setup_entry: ) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
VALID_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "credentials"
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
VALID_AUTH, VALID_AUTH,
@ -104,7 +107,10 @@ async def test_reauth_successful(hass):
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
return_value=DEVICE_CONFIG_AUTH,
), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
return_value="aa:bb:cc:dd:ee:ff", return_value="aa:bb:cc:dd:ee:ff",
): ):
@ -137,7 +143,7 @@ async def test_reauth_unsuccessful(hass):
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize", "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
side_effect=ApiError("API Error"), side_effect=ApiError("API Error"),
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -171,8 +177,11 @@ async def test_form_with_auth_errors(hass, error):
"""Test we handle errors when auth is required.""" """Test we handle errors when auth is required."""
exc, base_error = error exc, base_error = error
with patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize", "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
side_effect=AuthFailed("Auth Error"), side_effect=AuthFailed("Auth Error"),
), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
return_value="aa:bb:cc:dd:ee:ff",
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -221,26 +230,13 @@ async def test_form_errors(hass, error):
async def test_form_abort(hass): async def test_form_abort(hass):
"""Test we handle abort after error."""
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
side_effect=CannotGetMac("Cannot get MAC address from device"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data=VALID_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "device_unsupported"
async def test_form_with_auth_abort(hass):
"""Test we handle abort after error.""" """Test we handle abort after error."""
with patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize", "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
side_effect=AuthFailed("Auth Error"), return_value=DEVICE_CONFIG,
), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
side_effect=CannotGetMac("Cannot get MAC address from device"),
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -248,18 +244,6 @@ async def test_form_with_auth_abort(hass):
data=VALID_CONFIG, data=VALID_CONFIG,
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "credentials"
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
side_effect=CannotGetMac("Cannot get MAC address from device"),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
VALID_AUTH,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "device_unsupported" assert result["reason"] == "device_unsupported"
@ -275,7 +259,10 @@ async def test_form_already_configured(hass):
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
return_value=DEVICE_CONFIG,
), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
return_value="aa:bb:cc:dd:ee:ff", return_value="aa:bb:cc:dd:ee:ff",
): ):
@ -293,7 +280,10 @@ async def test_form_already_configured(hass):
async def test_zeroconf(hass): async def test_zeroconf(hass):
"""Test we get the form.""" """Test we get the form."""
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
return_value=DEVICE_CONFIG,
), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
return_value="aa:bb:cc:dd:ee:ff", return_value="aa:bb:cc:dd:ee:ff",
): ):
@ -332,8 +322,11 @@ async def test_zeroconf(hass):
async def test_zeroconf_with_auth(hass): async def test_zeroconf_with_auth(hass):
"""Test that the zeroconf step with auth works.""" """Test that the zeroconf step with auth works."""
with patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize", "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
side_effect=AuthFailed("Auth Error"), side_effect=AuthFailed("Auth Error"),
), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
return_value="aa:bb:cc:dd:ee:ff",
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -351,7 +344,10 @@ async def test_zeroconf_with_auth(hass):
assert result["errors"] == {} assert result["errors"] == {}
assert context["title_placeholders"]["host"] == "10.10.2.3" assert context["title_placeholders"]["host"] == "10.10.2.3"
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
return_value=DEVICE_CONFIG_AUTH,
), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
return_value="aa:bb:cc:dd:ee:ff", return_value="aa:bb:cc:dd:ee:ff",
), patch( ), patch(

View file

@ -51,7 +51,7 @@ async def test_config_auth_failed(hass):
) )
with patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize", "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials",
side_effect=AuthFailed("Authorization has failed"), side_effect=AuthFailed("Authorization has failed"),
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)