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."""
from __future__ import annotations
import asyncio
import logging
from typing import cast
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
from aiohttp.client_exceptions import ClientConnectorError, ClientError
import async_timeout
from nettigo_air_monitor import (
ApiError,
AuthFailed,
ConnectionOptions,
InvalidSensorData,
NAMSensors,
NettigoAirMonitor,
@ -16,13 +18,18 @@ from nettigo_air_monitor import (
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
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.helpers import entity_registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
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 (
ATTR_SDS011,
@ -41,10 +48,20 @@ PLATFORMS = ["sensor"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Nettigo as config entry."""
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)
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()
hass.data.setdefault(DOMAIN, {})
@ -81,14 +98,12 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator):
def __init__(
self,
hass: HomeAssistant,
session: ClientSession,
host: str,
nam: NettigoAirMonitor,
unique_id: str | None,
) -> None:
"""Initialize."""
self.host = host
self.nam = NettigoAirMonitor(session, host)
self._unique_id = unique_id
self.nam = nam
super().__init__(
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.
async with async_timeout.timeout(30):
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:
raise UpdateFailed(error) from error
@ -120,5 +137,5 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator):
name=DEFAULT_NAME,
sw_version=self.nam.software_version,
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 logging
from typing import Any, cast
from typing import Any
from aiohttp.client_exceptions import ClientConnectorError
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
from homeassistant import config_entries
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.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
@ -21,6 +28,23 @@ from .const import DOMAIN
_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):
"""Config flow for Nettigo Air Monitor."""
@ -29,18 +53,22 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize flow."""
self.host: str | None = None
self.host: str
self.entry: config_entries.ConfigEntry
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
errors = {}
errors: dict[str, str] = {}
if user_input is not None:
self.host = user_input[CONF_HOST]
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):
errors["base"] = "cannot_connect"
except CannotGetMac:
@ -49,36 +77,65 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
_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=cast(str, self.host),
title=self.host,
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=""): str,
}
),
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
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(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Handle zeroconf discovery."""
self.host = discovery_info[CONF_HOST]
self.context["title_placeholders"] = {"host": self.host}
# Do not probe the device if the host is already configured
self._async_abort_entries_match({CONF_HOST: self.host})
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):
return self.async_abort(reason="cannot_connect")
except CannotGetMac:
@ -87,21 +144,17 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(format_mac(mac))
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()
async def async_step_confirm_discovery(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle discovery confirm."""
errors: dict = {}
errors: dict[str, str] = {}
if user_input is not None:
return self.async_create_entry(
title=cast(str, self.host),
title=self.host,
data={CONF_HOST: self.host},
)
@ -109,16 +162,39 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="confirm_discovery",
description_placeholders={CONF_HOST: self.host},
description_placeholders={"host": self.host},
errors=errors,
)
async def _async_get_mac(self, host: str) -> str:
"""Get device MAC address."""
websession = async_get_clientsession(self.hass)
nam = NettigoAirMonitor(websession, host)
# 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()
async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult:
"""Handle configuration by re-auth."""
if entry := self.hass.config_entries.async_get_entry(self.context["entry_id"]):
self.entry = entry
self.host = data[CONF_HOST]
self.context["title_placeholders"] = {"host": self.host}
return await self.async_step_reauth_confirm()
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",
"documentation": "https://www.home-assistant.io/integrations/nam",
"codeowners": ["@bieniu"],
"requirements": ["nettigo-air-monitor==1.1.1"],
"requirements": ["nettigo-air-monitor==1.2.1"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View file

@ -1,6 +1,6 @@
{
"config": {
"flow_title": "{name}",
"flow_title": "{host}",
"step": {
"user": {
"description": "Set up Nettigo Air Monitor integration.",
@ -8,17 +8,34 @@
"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": {
"description": "Do you want to set up Nettigo Air Monitor at {host}?"
}
},
"error": {
"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": {
"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
# homeassistant.components.nam
nettigo-air-monitor==1.1.1
nettigo-air-monitor==1.2.1
# homeassistant.components.neurio_energy
neurio==0.3.1

View file

@ -642,7 +642,7 @@ netdisco==3.0.0
netmap==0.7.0.2
# homeassistant.components.nam
nettigo-air-monitor==1.1.1
nettigo-air-monitor==1.2.1
# homeassistant.components.nexia
nexia==0.9.11

View file

@ -1,5 +1,5 @@
"""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
@ -52,9 +52,11 @@ async def init_integration(hass, co2_sensor=True) -> MockConfigEntry:
# Remove conc_co2_ppm value
nam_data["sensordatavalues"].pop(6)
with patch(
"homeassistant.components.nam.NettigoAirMonitor._async_get_data",
return_value=nam_data,
update_response = Mock(json=AsyncMock(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)
await hass.config_entries.async_setup(entry.entry_id)

View file

@ -2,21 +2,22 @@
import asyncio
from unittest.mock import patch
from nettigo_air_monitor import ApiError, CannotGetMac
from nettigo_air_monitor import ApiError, AuthFailed, CannotGetMac
import pytest
from homeassistant import data_entry_flow
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
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_AUTH = {"username": "fake_username", "password": "fake_password"}
async def test_form_create_entry(hass):
"""Test that the user step works."""
async def test_form_create_entry_without_auth(hass):
"""Test that the user step without auth works."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@ -24,13 +25,12 @@ async def test_form_create_entry(hass):
assert result["step_id"] == SOURCE_USER
assert result["errors"] == {}
with patch(
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_CONFIG,
@ -43,10 +43,153 @@ async def test_form_create_entry(hass):
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(
"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"),
(ValueError, "unknown"),
],
@ -55,7 +198,7 @@ async def test_form_errors(hass, error):
"""Test we handle errors."""
exc, base_error = error
with patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
"homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=exc,
):
@ -70,11 +213,10 @@ async def test_form_errors(hass, error):
async def test_form_abort(hass):
"""Test we handle abort after error."""
with patch(
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},
@ -85,6 +227,34 @@ async def test_form_abort(hass):
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):
"""Test that errors are shown when duplicates are added."""
entry = MockConfigEntry(
@ -96,7 +266,7 @@ async def test_form_already_configured(hass):
DOMAIN, context={"source": SOURCE_USER}
)
with patch(
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
return_value="aa:bb:cc:dd:ee:ff",
):
@ -114,7 +284,7 @@ async def test_form_already_configured(hass):
async def test_zeroconf(hass):
"""Test we get the form."""
with patch(
with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
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["errors"] == {}
assert context["title_placeholders"]["name"] == "NAM-12345"
assert context["title_placeholders"]["host"] == "10.10.2.3"
assert context["confirm_only"] is True
with patch(
@ -150,6 +320,48 @@ async def test_zeroconf(hass):
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):
"""Test that errors are shown when host is already configured."""
entry = MockConfigEntry(
@ -170,7 +382,7 @@ async def test_zeroconf_host_already_configured(hass):
@pytest.mark.parametrize(
"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"),
],
)
@ -178,10 +390,9 @@ async def test_zeroconf_errors(hass, error):
"""Test we handle errors."""
exc, reason = error
with patch(
"homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address",
"homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=exc,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
data=DISCOVERY_INFO,

View file

@ -1,7 +1,7 @@
"""Test init of Nettigo Air Monitor integration."""
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.nam.const import DOMAIN
@ -33,7 +33,7 @@ async def test_config_not_ready(hass):
)
with patch(
"homeassistant.components.nam.NettigoAirMonitor._async_get_data",
"homeassistant.components.nam.NettigoAirMonitor.initialize",
side_effect=ApiError("API Error"),
):
entry.add_to_hass(hass)
@ -41,6 +41,24 @@ async def test_config_not_ready(hass):
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):
"""Test successful unload of entry."""
entry = await init_integration(hass)

View file

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