Bump Nettigo Air Monitor backend library (#59675)

This commit is contained in:
Maciej Bieniek 2021-11-18 02:00:19 +01:00 committed by GitHub
parent 9a85c8d894
commit 1c11e7061d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 425 additions and 81 deletions

View File

@ -1,14 +1,16 @@
"""The Nettigo Air Monitor component.""" """The Nettigo Air Monitor component."""
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from typing import cast from typing import cast
from aiohttp import ClientSession from aiohttp.client_exceptions import ClientConnectorError, ClientError
from aiohttp.client_exceptions import ClientConnectorError
import async_timeout import async_timeout
from nettigo_air_monitor import ( from nettigo_air_monitor import (
ApiError, ApiError,
AuthFailed,
ConnectionOptions,
InvalidSensorData, InvalidSensorData,
NAMSensors, NAMSensors,
NettigoAirMonitor, NettigoAirMonitor,
@ -16,13 +18,18 @@ from nettigo_air_monitor import (
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry from homeassistant.helpers import entity_registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import ( from .const import (
ATTR_SDS011, ATTR_SDS011,
@ -41,10 +48,20 @@ PLATFORMS = ["sensor"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Nettigo as config entry.""" """Set up Nettigo as config entry."""
host: str = entry.data[CONF_HOST] host: str = entry.data[CONF_HOST]
username: str | None = entry.data.get(CONF_USERNAME)
password: str | None = entry.data.get(CONF_PASSWORD)
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
coordinator = NAMDataUpdateCoordinator(hass, websession, host, entry.unique_id) options = ConnectionOptions(host=host, username=username, password=password)
try:
nam = await NettigoAirMonitor.create(websession, options)
except AuthFailed as err:
raise ConfigEntryAuthFailed from err
except (ApiError, ClientError, ClientConnectorError, asyncio.TimeoutError) as err:
raise ConfigEntryNotReady from err
coordinator = NAMDataUpdateCoordinator(hass, nam, entry.unique_id)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
@ -81,14 +98,12 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator):
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
session: ClientSession, nam: NettigoAirMonitor,
host: str,
unique_id: str | None, unique_id: str | None,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
self.host = host
self.nam = NettigoAirMonitor(session, host)
self._unique_id = unique_id self._unique_id = unique_id
self.nam = nam
super().__init__( super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL
@ -102,6 +117,8 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator):
# get the data 4 times, so we use a longer than usual timeout here. # get the data 4 times, so we use a longer than usual timeout here.
async with async_timeout.timeout(30): async with async_timeout.timeout(30):
data = await self.nam.async_update() data = await self.nam.async_update()
# We do not need to catch AuthFailed exception here because sensor data is
# always available without authorization.
except (ApiError, ClientConnectorError, InvalidSensorData) as error: except (ApiError, ClientConnectorError, InvalidSensorData) as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error
@ -120,5 +137,5 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator):
name=DEFAULT_NAME, name=DEFAULT_NAME,
sw_version=self.nam.software_version, sw_version=self.nam.software_version,
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
configuration_url=f"http://{self.host}/", configuration_url=f"http://{self.nam.host}/",
) )

View File

@ -3,16 +3,23 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
from typing import Any, cast from typing import Any
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
import async_timeout import async_timeout
from nettigo_air_monitor import ApiError, CannotGetMac, NettigoAirMonitor from nettigo_air_monitor import (
ApiError,
AuthFailed,
CannotGetMac,
ConnectionOptions,
NettigoAirMonitor,
)
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.const import ATTR_NAME, CONF_HOST from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.device_registry import format_mac
@ -21,6 +28,23 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AUTH_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
)
async def async_get_mac(hass: HomeAssistant, host: str, data: dict[str, Any]) -> str:
"""Get device MAC address."""
websession = async_get_clientsession(hass)
options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD))
nam = await NettigoAirMonitor.create(websession, options)
# Device firmware uses synchronous code and doesn't respond to http queries
# when reading data from sensors. The nettigo-air-monitor library tries to get
# the data 4 times, so we use a longer than usual timeout here.
async with async_timeout.timeout(30):
return await nam.async_get_mac_address()
class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Nettigo Air Monitor.""" """Config flow for Nettigo Air Monitor."""
@ -29,18 +53,22 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize flow.""" """Initialize flow."""
self.host: str | None = None self.host: str
self.entry: config_entries.ConfigEntry
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
) -> FlowResult: ) -> FlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
errors = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
self.host = user_input[CONF_HOST] self.host = user_input[CONF_HOST]
try: try:
mac = await self._async_get_mac(cast(str, self.host)) mac = await async_get_mac(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:
@ -49,36 +77,65 @@ 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(mac))
self._abort_if_unique_id_configured({CONF_HOST: self.host}) self._abort_if_unique_id_configured({CONF_HOST: self.host})
return self.async_create_entry( return self.async_create_entry(
title=cast(str, self.host), title=self.host,
data=user_input, data=user_input,
) )
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=vol.Schema( data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
{
vol.Required(CONF_HOST, default=""): str,
}
),
errors=errors, errors=errors,
) )
async def async_step_credentials(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the credentials step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
mac = await async_get_mac(self.hass, self.host, user_input)
except AuthFailed:
errors["base"] = "invalid_auth"
except (ApiError, ClientConnectorError, asyncio.TimeoutError):
errors["base"] = "cannot_connect"
except CannotGetMac:
return self.async_abort(reason="device_unsupported")
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
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(
title=self.host,
data={**user_input, CONF_HOST: self.host},
)
return self.async_show_form(
step_id="credentials", data_schema=AUTH_SCHEMA, errors=errors
)
async def async_step_zeroconf( async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult: ) -> FlowResult:
"""Handle zeroconf discovery.""" """Handle zeroconf discovery."""
self.host = discovery_info[CONF_HOST] self.host = discovery_info[CONF_HOST]
self.context["title_placeholders"] = {"host": self.host}
# Do not probe the device if the host is already configured # Do not probe the device if the host is already configured
self._async_abort_entries_match({CONF_HOST: self.host}) self._async_abort_entries_match({CONF_HOST: self.host})
try: try:
mac = await self._async_get_mac(self.host) mac = await async_get_mac(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:
@ -87,21 +144,17 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(format_mac(mac)) await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_configured({CONF_HOST: self.host}) self._abort_if_unique_id_configured({CONF_HOST: self.host})
self.context["title_placeholders"] = {
ATTR_NAME: discovery_info[ATTR_NAME].split(".")[0]
}
return await self.async_step_confirm_discovery() return await self.async_step_confirm_discovery()
async def async_step_confirm_discovery( async def async_step_confirm_discovery(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Handle discovery confirm.""" """Handle discovery confirm."""
errors: dict = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
return self.async_create_entry( return self.async_create_entry(
title=cast(str, self.host), title=self.host,
data={CONF_HOST: self.host}, data={CONF_HOST: self.host},
) )
@ -109,16 +162,39 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form( return self.async_show_form(
step_id="confirm_discovery", step_id="confirm_discovery",
description_placeholders={CONF_HOST: self.host}, description_placeholders={"host": self.host},
errors=errors, errors=errors,
) )
async def _async_get_mac(self, host: str) -> str: async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult:
"""Get device MAC address.""" """Handle configuration by re-auth."""
websession = async_get_clientsession(self.hass) if entry := self.hass.config_entries.async_get_entry(self.context["entry_id"]):
nam = NettigoAirMonitor(websession, host) self.entry = entry
# Device firmware uses synchronous code and doesn't respond to http queries self.host = data[CONF_HOST]
# when reading data from sensors. The nettigo-air-monitor library tries to get self.context["title_placeholders"] = {"host": self.host}
# the data 4 times, so we use a longer than usual timeout here. return await self.async_step_reauth_confirm()
async with async_timeout.timeout(30):
return await nam.async_get_mac_address() async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
if user_input is not None:
try:
await async_get_mac(self.hass, self.host, user_input)
except (ApiError, AuthFailed, ClientConnectorError, asyncio.TimeoutError):
return self.async_abort(reason="reauth_unsuccessful")
else:
self.hass.config_entries.async_update_entry(
self.entry, data={**user_input, CONF_HOST: self.host}
)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={"host": self.host},
data_schema=AUTH_SCHEMA,
errors=errors,
)

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.1.1"], "requirements": ["nettigo-air-monitor==1.2.1"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "type": "_http._tcp.local.",

View File

@ -1,6 +1,6 @@
{ {
"config": { "config": {
"flow_title": "{name}", "flow_title": "{host}",
"step": { "step": {
"user": { "user": {
"description": "Set up Nettigo Air Monitor integration.", "description": "Set up Nettigo Air Monitor integration.",
@ -8,17 +8,34 @@
"host": "[%key:common::config_flow::data::host%]" "host": "[%key:common::config_flow::data::host%]"
} }
}, },
"credentials": {
"description": "Please enter the username and password.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"reauth_confirm": {
"description": "Please enter the correct username and password for host: {host}",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"confirm_discovery": { "confirm_discovery": {
"description": "Do you want to set up Nettigo Air Monitor at {host}?" "description": "Do you want to set up Nettigo Air Monitor at {host}?"
} }
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"device_unsupported": "The device is unsupported." "device_unsupported": "The device is unsupported.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again."
} }
} }
} }

View File

@ -1056,7 +1056,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.1.1 nettigo-air-monitor==1.2.1
# homeassistant.components.neurio_energy # homeassistant.components.neurio_energy
neurio==0.3.1 neurio==0.3.1

View File

@ -642,7 +642,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.1.1 nettigo-air-monitor==1.2.1
# homeassistant.components.nexia # homeassistant.components.nexia
nexia==0.9.11 nexia==0.9.11

View File

@ -1,5 +1,5 @@
"""Tests for the Nettigo Air Monitor integration.""" """Tests for the Nettigo Air Monitor integration."""
from unittest.mock import patch from unittest.mock import AsyncMock, Mock, patch
from homeassistant.components.nam.const import DOMAIN from homeassistant.components.nam.const import DOMAIN
@ -52,9 +52,11 @@ async def init_integration(hass, co2_sensor=True) -> MockConfigEntry:
# Remove conc_co2_ppm value # Remove conc_co2_ppm value
nam_data["sensordatavalues"].pop(6) nam_data["sensordatavalues"].pop(6)
with patch( update_response = Mock(json=AsyncMock(return_value=nam_data))
"homeassistant.components.nam.NettigoAirMonitor._async_get_data",
return_value=nam_data, with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
"homeassistant.components.nam.NettigoAirMonitor._async_http_request",
return_value=update_response,
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)

View File

@ -2,21 +2,22 @@
import asyncio import asyncio
from unittest.mock import patch from unittest.mock import patch
from nettigo_air_monitor import ApiError, CannotGetMac from nettigo_air_monitor import ApiError, AuthFailed, CannotGetMac
import pytest import pytest
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.nam.const import DOMAIN from homeassistant.components.nam.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
DISCOVERY_INFO = {"host": "10.10.2.3", "name": "NAM-12345"} DISCOVERY_INFO = {"host": "10.10.2.3"}
VALID_CONFIG = {"host": "10.10.2.3"} VALID_CONFIG = {"host": "10.10.2.3"}
VALID_AUTH = {"username": "fake_username", "password": "fake_password"}
async def test_form_create_entry(hass): async def test_form_create_entry_without_auth(hass):
"""Test that the user step works.""" """Test that the user step without auth works."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
@ -24,13 +25,12 @@ async def test_form_create_entry(hass):
assert result["step_id"] == SOURCE_USER assert result["step_id"] == SOURCE_USER
assert result["errors"] == {} assert result["errors"] == {}
with patch( 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
VALID_CONFIG, VALID_CONFIG,
@ -43,10 +43,153 @@ async def test_form_create_entry(hass):
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_form_create_entry_with_auth(hass):
"""Test that the user step with auth works."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
assert result["errors"] == {}
with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=AuthFailed("Auth Error"),
):
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",
return_value="aa:bb:cc:dd:ee:ff",
), patch(
"homeassistant.components.nam.async_setup_entry", return_value=True
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
VALID_AUTH,
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "10.10.2.3"
assert result["data"]["host"] == "10.10.2.3"
assert result["data"]["username"] == "fake_username"
assert result["data"]["password"] == "fake_password"
assert len(mock_setup_entry.mock_calls) == 1
async def test_reauth_successful(hass):
"""Test starting a reauthentication flow."""
entry = MockConfigEntry(
domain=DOMAIN,
title="10.10.2.3",
unique_id="aa:bb:cc:dd:ee:ff",
data={"host": "10.10.2.3"},
)
entry.add_to_hass(hass)
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), 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(
DOMAIN,
context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id},
data=entry.data,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=VALID_AUTH,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "reauth_successful"
async def test_reauth_unsuccessful(hass):
"""Test starting a reauthentication flow."""
entry = MockConfigEntry(
domain=DOMAIN,
title="10.10.2.3",
unique_id="aa:bb:cc:dd:ee:ff",
data={"host": "10.10.2.3"},
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=ApiError("API Error"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id},
data=entry.data,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=VALID_AUTH,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "reauth_unsuccessful"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"error", "error",
[ [
(ApiError("Invalid response from device 10.10.2.3: 404"), "cannot_connect"), (ApiError("API Error"), "cannot_connect"),
(AuthFailed("Auth Error"), "invalid_auth"),
(asyncio.TimeoutError, "cannot_connect"),
(ValueError, "unknown"),
],
)
async def test_form_with_auth_errors(hass, error):
"""Test we handle errors when auth is required."""
exc, base_error = error
with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=AuthFailed("Auth Error"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data=VALID_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "credentials"
with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=exc,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
VALID_AUTH,
)
assert result["errors"] == {"base": base_error}
@pytest.mark.parametrize(
"error",
[
(ApiError("API Error"), "cannot_connect"),
(asyncio.TimeoutError, "cannot_connect"), (asyncio.TimeoutError, "cannot_connect"),
(ValueError, "unknown"), (ValueError, "unknown"),
], ],
@ -55,7 +198,7 @@ async def test_form_errors(hass, error):
"""Test we handle errors.""" """Test we handle errors."""
exc, base_error = error exc, base_error = error
with patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", "homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=exc, side_effect=exc,
): ):
@ -70,11 +213,10 @@ 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.""" """Test we handle abort after error."""
with patch( with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
side_effect=CannotGetMac("Cannot get MAC address from device"), 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,
context={"source": SOURCE_USER}, context={"source": SOURCE_USER},
@ -85,6 +227,34 @@ async def test_form_abort(hass):
assert result["reason"] == "device_unsupported" assert result["reason"] == "device_unsupported"
async def test_form_with_auth_abort(hass):
"""Test we handle abort after error."""
with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=AuthFailed("Auth Error"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
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["reason"] == "device_unsupported"
async def test_form_already_configured(hass): async def test_form_already_configured(hass):
"""Test that errors are shown when duplicates are added.""" """Test that errors are shown when duplicates are added."""
entry = MockConfigEntry( entry = MockConfigEntry(
@ -96,7 +266,7 @@ async def test_form_already_configured(hass):
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
with patch( 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",
): ):
@ -114,7 +284,7 @@ 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( 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",
): ):
@ -131,7 +301,7 @@ async def test_zeroconf(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {} assert result["errors"] == {}
assert context["title_placeholders"]["name"] == "NAM-12345" assert context["title_placeholders"]["host"] == "10.10.2.3"
assert context["confirm_only"] is True assert context["confirm_only"] is True
with patch( with patch(
@ -150,6 +320,48 @@ async def test_zeroconf(hass):
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_zeroconf_with_auth(hass):
"""Test that the zeroconf step with auth works."""
with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=AuthFailed("Auth Error"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
data=DISCOVERY_INFO,
context={"source": SOURCE_ZEROCONF},
)
context = next(
flow["context"]
for flow in hass.config_entries.flow.async_progress()
if flow["flow_id"] == result["flow_id"]
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "credentials"
assert result["errors"] == {}
assert context["title_placeholders"]["host"] == "10.10.2.3"
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
return_value="aa:bb:cc:dd:ee:ff",
), patch(
"homeassistant.components.nam.async_setup_entry", return_value=True
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
VALID_AUTH,
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "10.10.2.3"
assert result["data"]["host"] == "10.10.2.3"
assert result["data"]["username"] == "fake_username"
assert result["data"]["password"] == "fake_password"
assert len(mock_setup_entry.mock_calls) == 1
async def test_zeroconf_host_already_configured(hass): async def test_zeroconf_host_already_configured(hass):
"""Test that errors are shown when host is already configured.""" """Test that errors are shown when host is already configured."""
entry = MockConfigEntry( entry = MockConfigEntry(
@ -170,7 +382,7 @@ async def test_zeroconf_host_already_configured(hass):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"error", "error",
[ [
(ApiError("Invalid response from device 10.10.2.3: 404"), "cannot_connect"), (ApiError("API Error"), "cannot_connect"),
(CannotGetMac("Cannot get MAC address from device"), "device_unsupported"), (CannotGetMac("Cannot get MAC address from device"), "device_unsupported"),
], ],
) )
@ -178,10 +390,9 @@ async def test_zeroconf_errors(hass, error):
"""Test we handle errors.""" """Test we handle errors."""
exc, reason = error exc, reason = error
with patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", "homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=exc, side_effect=exc,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
data=DISCOVERY_INFO, data=DISCOVERY_INFO,

View File

@ -1,7 +1,7 @@
"""Test init of Nettigo Air Monitor integration.""" """Test init of Nettigo Air Monitor integration."""
from unittest.mock import patch from unittest.mock import patch
from nettigo_air_monitor import ApiError from nettigo_air_monitor import ApiError, AuthFailed
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
from homeassistant.components.nam.const import DOMAIN from homeassistant.components.nam.const import DOMAIN
@ -33,7 +33,7 @@ async def test_config_not_ready(hass):
) )
with patch( with patch(
"homeassistant.components.nam.NettigoAirMonitor._async_get_data", "homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=ApiError("API Error"), side_effect=ApiError("API Error"),
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -41,6 +41,24 @@ async def test_config_not_ready(hass):
assert entry.state is ConfigEntryState.SETUP_RETRY assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_config_auth_failed(hass):
"""Test for setup failure if the auth fails."""
entry = MockConfigEntry(
domain=DOMAIN,
title="10.10.2.3",
unique_id="aa:bb:cc:dd:ee:ff",
data={"host": "10.10.2.3"},
)
with patch(
"homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=AuthFailed("Authorization has failed"),
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.SETUP_ERROR
async def test_unload_entry(hass): async def test_unload_entry(hass):
"""Test successful unload of entry.""" """Test successful unload of entry."""
entry = await init_integration(hass) entry = await init_integration(hass)

View File

@ -1,6 +1,6 @@
"""Test sensor of Nettigo Air Monitor integration.""" """Test sensor of Nettigo Air Monitor integration."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch from unittest.mock import AsyncMock, Mock, patch
from nettigo_air_monitor import ApiError from nettigo_air_monitor import ApiError
@ -373,9 +373,10 @@ async def test_incompleta_data_after_device_restart(hass):
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
future = utcnow() + timedelta(minutes=6) future = utcnow() + timedelta(minutes=6)
with patch( update_response = Mock(json=AsyncMock(return_value=INCOMPLETE_NAM_DATA))
"homeassistant.components.nam.NettigoAirMonitor._async_get_data", with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
return_value=INCOMPLETE_NAM_DATA, "homeassistant.components.nam.NettigoAirMonitor._async_http_request",
return_value=update_response,
): ):
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -395,8 +396,8 @@ async def test_availability(hass):
assert state.state == "7.6" assert state.state == "7.6"
future = utcnow() + timedelta(minutes=6) future = utcnow() + timedelta(minutes=6)
with patch( with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
"homeassistant.components.nam.NettigoAirMonitor._async_get_data", "homeassistant.components.nam.NettigoAirMonitor._async_http_request",
side_effect=ApiError("API Error"), side_effect=ApiError("API Error"),
): ):
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
@ -407,9 +408,10 @@ async def test_availability(hass):
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
future = utcnow() + timedelta(minutes=12) future = utcnow() + timedelta(minutes=12)
with patch( update_response = Mock(json=AsyncMock(return_value=nam_data))
"homeassistant.components.nam.NettigoAirMonitor._async_get_data", with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
return_value=nam_data, "homeassistant.components.nam.NettigoAirMonitor._async_http_request",
return_value=update_response,
): ):
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -426,9 +428,10 @@ async def test_manual_update_entity(hass):
await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, "homeassistant", {})
with patch( update_response = Mock(json=AsyncMock(return_value=nam_data))
"homeassistant.components.nam.NettigoAirMonitor._async_get_data", with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
return_value=nam_data, "homeassistant.components.nam.NettigoAirMonitor._async_http_request",
return_value=update_response,
) as mock_get_data: ) as mock_get_data:
await hass.services.async_call( await hass.services.async_call(
"homeassistant", "homeassistant",