Bump Nettigo Air Monitor backend library (#59675)
This commit is contained in:
parent
9a85c8d894
commit
1c11e7061d
10 changed files with 425 additions and 81 deletions
|
@ -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}/",
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue