IntelliFire Config API Token Config Update (#68134)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Jeef 2022-04-21 10:14:13 -06:00 committed by GitHub
parent 73a368c242
commit 4d09078114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 338 additions and 69 deletions

View File

@ -1,11 +1,14 @@
"""The IntelliFire integration.""" """The IntelliFire integration."""
from __future__ import annotations from __future__ import annotations
from intellifire4py import IntellifireAsync from aiohttp import ClientConnectionError
from intellifire4py import IntellifireAsync, IntellifireControlAsync
from intellifire4py.exceptions import LoginException
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
from .coordinator import IntellifireDataUpdateCoordinator from .coordinator import IntellifireDataUpdateCoordinator
@ -17,17 +20,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up IntelliFire from a config entry.""" """Set up IntelliFire from a config entry."""
LOGGER.debug("Setting up config entry: %s", entry.unique_id) LOGGER.debug("Setting up config entry: %s", entry.unique_id)
# Define the API Object if CONF_USERNAME not in entry.data:
api_object = IntellifireAsync(entry.data[CONF_HOST]) LOGGER.debug("Old config entry format detected: %s", entry.unique_id)
raise ConfigEntryAuthFailed
# Define the API Objects
read_object = IntellifireAsync(entry.data[CONF_HOST])
ift_control = IntellifireControlAsync(
fireplace_ip=entry.data[CONF_HOST],
)
try:
await ift_control.login(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
)
except (ConnectionError, ClientConnectionError) as err:
raise ConfigEntryNotReady from err
except LoginException as err:
raise ConfigEntryAuthFailed(err) from err
finally:
await ift_control.close()
# Define the update coordinator # Define the update coordinator
coordinator = IntellifireDataUpdateCoordinator( coordinator = IntellifireDataUpdateCoordinator(
hass=hass, hass=hass, read_api=read_object, control_api=ift_control
api=api_object,
) )
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True return True

View File

@ -164,4 +164,4 @@ class IntellifireBinarySensor(IntellifireEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Use this to get the correct value.""" """Use this to get the correct value."""
return self.entity_description.value_fn(self.coordinator.api.data) return self.entity_description.value_fn(self.coordinator.read_api.data)

View File

@ -5,12 +5,17 @@ from dataclasses import dataclass
from typing import Any from typing import Any
from aiohttp import ClientConnectionError from aiohttp import ClientConnectionError
from intellifire4py import AsyncUDPFireplaceFinder, IntellifireAsync from intellifire4py import (
AsyncUDPFireplaceFinder,
IntellifireAsync,
IntellifireControlAsync,
)
from intellifire4py.exceptions import LoginException
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
@ -48,9 +53,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self): def __init__(self):
"""Initialize the Config Flow Handler.""" """Initialize the Config Flow Handler."""
self._config_context = {} self._host: str = ""
self._serial: str = ""
self._not_configured_hosts: list[DiscoveredHostInfo] = [] self._not_configured_hosts: list[DiscoveredHostInfo] = []
self._discovered_host: DiscoveredHostInfo self._discovered_host: DiscoveredHostInfo
self._reauth_needed: DiscoveredHostInfo
async def _find_fireplaces(self): async def _find_fireplaces(self):
"""Perform UDP discovery.""" """Perform UDP discovery."""
@ -71,31 +78,102 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
LOGGER.debug("Configured Hosts: %s", configured_hosts) LOGGER.debug("Configured Hosts: %s", configured_hosts)
LOGGER.debug("Not Configured Hosts: %s", self._not_configured_hosts) LOGGER.debug("Not Configured Hosts: %s", self._not_configured_hosts)
async def _async_validate_and_create_entry(self, host: str) -> FlowResult: async def validate_api_access_and_create_or_update(
"""Validate and create the entry.""" self, *, host: str, username: str, password: str, serial: str
self._async_abort_entries_match({CONF_HOST: host}) ):
serial = await validate_host_input(host) """Validate username/password against api."""
await self.async_set_unique_id(serial, raise_on_progress=False) ift_control = IntellifireControlAsync(fireplace_ip=host)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
return self.async_create_entry( LOGGER.debug("Attempting login to iftapi with: %s", username)
title=f"Fireplace {serial}", # This can throw an error which will be handled above
data={CONF_HOST: host}, try:
await ift_control.login(username=username, password=password)
await ift_control.get_username()
finally:
await ift_control.close()
data = {CONF_HOST: host, CONF_PASSWORD: password, CONF_USERNAME: username}
# Update or Create
existing_entry = await self.async_set_unique_id(serial)
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=data)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=f"Fireplace {serial}", data=data)
async def async_step_api_config(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Configure API access."""
errors = {}
control_schema = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
) )
if user_input is not None:
control_schema = vol.Schema(
{
vol.Required(
CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")
): str,
vol.Required(
CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")
): str,
}
)
if user_input[CONF_USERNAME] != "":
try:
return await self.validate_api_access_and_create_or_update(
host=self._host,
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
serial=self._serial,
)
except (ConnectionError, ClientConnectionError):
errors["base"] = "iftapi_connect"
LOGGER.info("ERROR: iftapi_connect")
except LoginException:
errors["base"] = "api_error"
LOGGER.info("ERROR: api_error")
return self.async_show_form(
step_id="api_config", errors=errors, data_schema=control_schema
)
async def _async_validate_ip_and_continue(self, host: str) -> FlowResult:
"""Validate local config and continue."""
self._async_abort_entries_match({CONF_HOST: host})
self._serial = await validate_host_input(host)
await self.async_set_unique_id(self._serial, raise_on_progress=False)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
# Store current data and jump to next stage
self._host = host
return await self.async_step_api_config()
async def async_step_manual_device_entry(self, user_input=None): async def async_step_manual_device_entry(self, user_input=None):
"""Handle manual input of local IP configuration.""" """Handle manual input of local IP configuration."""
LOGGER.debug("STEP: manual_device_entry")
errors = {} errors = {}
host = user_input.get(CONF_HOST) if user_input else None self._host = user_input.get(CONF_HOST) if user_input else None
if user_input is not None: if user_input is not None:
try: try:
return await self._async_validate_and_create_entry(host) return await self._async_validate_ip_and_continue(self._host)
except (ConnectionError, ClientConnectionError): except (ConnectionError, ClientConnectionError):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
return self.async_show_form( return self.async_show_form(
step_id="manual_device_entry", step_id="manual_device_entry",
errors=errors, errors=errors,
data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}), data_schema=vol.Schema({vol.Required(CONF_HOST, default=self._host): str}),
) )
async def async_step_pick_device( async def async_step_pick_device(
@ -103,15 +181,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult: ) -> FlowResult:
"""Pick which device to configure.""" """Pick which device to configure."""
errors = {} errors = {}
LOGGER.debug("STEP: pick_device")
if user_input is not None: if user_input is not None:
if user_input[CONF_HOST] == MANUAL_ENTRY_STRING: if user_input[CONF_HOST] == MANUAL_ENTRY_STRING:
return await self.async_step_manual_device_entry() return await self.async_step_manual_device_entry()
try: try:
return await self._async_validate_and_create_entry( return await self._async_validate_ip_and_continue(user_input[CONF_HOST])
user_input[CONF_HOST]
)
except (ConnectionError, ClientConnectionError): except (ConnectionError, ClientConnectionError):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
@ -135,30 +212,44 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# Launch fireplaces discovery # Launch fireplaces discovery
await self._find_fireplaces() await self._find_fireplaces()
LOGGER.debug("STEP: user")
if self._not_configured_hosts: if self._not_configured_hosts:
LOGGER.debug("Running Step: pick_device") LOGGER.debug("Running Step: pick_device")
return await self.async_step_pick_device() return await self.async_step_pick_device()
LOGGER.debug("Running Step: manual_device_entry") LOGGER.debug("Running Step: manual_device_entry")
return await self.async_step_manual_device_entry() return await self.async_step_manual_device_entry()
async def async_step_reauth(self, user_input=None):
"""Perform reauth upon an API authentication error."""
LOGGER.debug("STEP: reauth")
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
# populate the expected vars
self._serial = entry.unique_id
self._host = entry.data[CONF_HOST]
placeholders = {CONF_HOST: self._host, "serial": self._serial}
self.context["title_placeholders"] = placeholders
return await self.async_step_api_config()
async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult:
"""Handle DHCP Discovery.""" """Handle DHCP Discovery."""
LOGGER.debug("STEP: dhcp")
# Run validation logic on ip # Run validation logic on ip
host = discovery_info.ip host = discovery_info.ip
self._async_abort_entries_match({CONF_HOST: host}) self._async_abort_entries_match({CONF_HOST: host})
try: try:
serial = await validate_host_input(host) self._serial = await validate_host_input(host)
except (ConnectionError, ClientConnectionError): except (ConnectionError, ClientConnectionError):
return self.async_abort(reason="not_intellifire_device") return self.async_abort(reason="not_intellifire_device")
await self.async_set_unique_id(serial) await self.async_set_unique_id(self._serial)
self._abort_if_unique_id_configured(updates={CONF_HOST: host}) self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._discovered_host = DiscoveredHostInfo(ip=host, serial=serial) self._discovered_host = DiscoveredHostInfo(ip=host, serial=self._serial)
placeholders = {CONF_HOST: host, "serial": serial} placeholders = {CONF_HOST: host, "serial": self._serial}
self.context["title_placeholders"] = placeholders self.context["title_placeholders"] = placeholders
self._set_confirm_only() self._set_confirm_only()
@ -167,6 +258,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_dhcp_confirm(self, user_input=None): async def async_step_dhcp_confirm(self, user_input=None):
"""Attempt to confirm.""" """Attempt to confirm."""
LOGGER.debug("STEP: dhcp_confirm")
# Add the hosts one by one # Add the hosts one by one
host = self._discovered_host.ip host = self._discovered_host.ip
serial = self._discovered_host.serial serial = self._discovered_host.serial

View File

@ -6,3 +6,5 @@ import logging
DOMAIN = "intellifire" DOMAIN = "intellifire"
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
CONF_SERIAL = "serial"

View File

@ -5,7 +5,11 @@ from datetime import timedelta
from aiohttp import ClientConnectionError from aiohttp import ClientConnectionError
from async_timeout import timeout from async_timeout import timeout
from intellifire4py import IntellifireAsync, IntellifirePollData from intellifire4py import (
IntellifireAsync,
IntellifireControlAsync,
IntellifirePollData,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
@ -17,7 +21,12 @@ from .const import DOMAIN, LOGGER
class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData]): class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData]):
"""Class to manage the polling of the fireplace API.""" """Class to manage the polling of the fireplace API."""
def __init__(self, hass: HomeAssistant, api: IntellifireAsync) -> None: def __init__(
self,
hass: HomeAssistant,
read_api: IntellifireAsync,
control_api: IntellifireControlAsync,
) -> None:
"""Initialize the Coordinator.""" """Initialize the Coordinator."""
super().__init__( super().__init__(
hass, hass,
@ -25,21 +34,27 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData
name=DOMAIN, name=DOMAIN,
update_interval=timedelta(seconds=15), update_interval=timedelta(seconds=15),
) )
self._api = api self._read_api = read_api
self._control_api = control_api
async def _async_update_data(self) -> IntellifirePollData: async def _async_update_data(self) -> IntellifirePollData:
LOGGER.debug("Calling update loop on IntelliFire") LOGGER.debug("Calling update loop on IntelliFire")
async with timeout(100): async with timeout(100):
try: try:
await self._api.poll() await self._read_api.poll()
except (ConnectionError, ClientConnectionError) as exception: except (ConnectionError, ClientConnectionError) as exception:
raise UpdateFailed from exception raise UpdateFailed from exception
return self._api.data return self._read_api.data
@property @property
def api(self) -> IntellifireAsync: def read_api(self) -> IntellifireAsync:
"""Return the API pointer.""" """Return the Status API pointer."""
return self._api return self._read_api
@property
def control_api(self) -> IntellifireControlAsync:
"""Return the control API."""
return self._control_api
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
@ -48,6 +63,7 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData
manufacturer="Hearth and Home", manufacturer="Hearth and Home",
model="IFT-WFM", model="IFT-WFM",
name="IntelliFire Fireplace", name="IntelliFire Fireplace",
identifiers={("IntelliFire", f"{self.api.data.serial}]")}, identifiers={("IntelliFire", f"{self.read_api.data.serial}]")},
sw_version=self.api.data.fw_ver_str, sw_version=self.read_api.data.fw_ver_str,
configuration_url=f"http://{self.read_api.ip}/poll",
) )

View File

@ -22,6 +22,6 @@ class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]):
self.entity_description = description self.entity_description = description
# Set the Display name the User will see # Set the Display name the User will see
self._attr_name = f"Fireplace {description.name}" self._attr_name = f"Fireplace {description.name}"
self._attr_unique_id = f"{description.key}_{coordinator.api.data.serial}" self._attr_unique_id = f"{description.key}_{coordinator.read_api.data.serial}"
# Configure the Device Info # Configure the Device Info
self._attr_device_info = self.coordinator.device_info self._attr_device_info = self.coordinator.device_info

View File

@ -150,4 +150,4 @@ class IntellifireSensor(IntellifireEntity, SensorEntity):
@property @property
def native_value(self) -> int | str | datetime | None: def native_value(self) -> int | str | datetime | None:
"""Return the state.""" """Return the state."""
return self.entity_description.value_fn(self.coordinator.api.data) return self.entity_description.value_fn(self.coordinator.read_api.data)

View File

@ -5,23 +5,34 @@
"manual_device_entry": { "manual_device_entry": {
"description": "Local Configuration", "description": "Local Configuration",
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]" "host": "[%key:common::config_flow::data::host%] (IP Address)"
}
},
"api_config": {
"data": {
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
} }
}, },
"dhcp_confirm": { "dhcp_confirm": {
"description": "Do you want to setup {host}\nSerial: {serial}?" "description": "Do you want to setup {host}\nSerial: {serial}?"
}, },
"pick_device": { "pick_device": {
"title": "Device Selection",
"description": "The following IntelliFire devices were discovered. Please select which you wish to configure.",
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]" "host": "[%key:common::config_flow::data::host%]"
} }
} }
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"api_error": "Login failed",
"iftapi_connect": "Error conecting to iftapi.net"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"not_intellifire_device": "Not an IntelliFire Device." "not_intellifire_device": "Not an IntelliFire Device."
} }
} }

View File

@ -2,26 +2,39 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Device is already configured", "already_configured": "Device is already configured",
"not_intellifire_device": "Not an IntelliFire Device." "not_intellifire_device": "Not an IntelliFire Device.",
"reauth_successful": "Re-authentication was successful"
}, },
"error": { "error": {
"cannot_connect": "Failed to connect" "api_error": "Login failed",
"cannot_connect": "Could not connect to a fireplace endpoint at url: http://{host}/poll\nVerify IP address and try again",
"iftapi_connect": "Error conecting to iftapi.net"
}, },
"flow_title": "{serial} ({host})", "flow_title": "{serial} ({host})",
"step": { "step": {
"api_config": {
"data": {
"password": "Password",
"username": "Username (Email)"
},
"description": "IntelliFire will need to reach out to [iftapi.net](https://iftapi.net/webaccess/login.html) in order to obtain an API key. Once it has obtained this API key, the rest of its interactions will occur completely within the local network. If the API key were to expire it would again need to reach out to https://iftapi.net/webaccess/login.html\n\nUsername and Password are the same information used in your IntelliFire Android/iOS application. ",
"title": "IntelliFire - API Configuration"
},
"dhcp_confirm": { "dhcp_confirm": {
"description": "Do you want to setup {host}\nSerial: {serial}?" "description": "Do you want to setup {host}\nSerial: {serial}?"
}, },
"manual_device_entry": { "manual_device_entry": {
"data": { "data": {
"host": "Host" "host": "Host (IP Address)"
}, },
"description": "Local Configuration" "description": "Local Configuration"
}, },
"pick_device": { "pick_device": {
"data": { "data": {
"host": "Host" "host": "Host"
} },
"description": "The following IntelliFire devices were discovered. Please select which you wish to configure.",
"title": "Device Selection"
} }
} }
} }

View File

@ -2,6 +2,7 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
from aiohttp.client_reqrep import ConnectionKey
import pytest import pytest
@ -49,3 +50,10 @@ def mock_intellifire_config_flow() -> Generator[None, MagicMock, None]:
intellifire = intellifire_mock.return_value intellifire = intellifire_mock.return_value
intellifire.data = data_mock intellifire.data = data_mock
yield intellifire yield intellifire
def mock_api_connection_error() -> ConnectionError:
"""Return a fake a ConnectionError for iftapi.net."""
ret = ConnectionError()
ret.args = [ConnectionKey("iftapi.net", 443, False, None, None, None, None)]
return ret

View File

@ -1,17 +1,29 @@
"""Test the IntelliFire config flow.""" """Test the IntelliFire config flow."""
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from intellifire4py.exceptions import LoginException
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import dhcp from homeassistant.components import dhcp
from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING
from homeassistant.components.intellifire.const import DOMAIN from homeassistant.components.intellifire.const import DOMAIN
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.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.components.intellifire.conftest import mock_api_connection_error
@patch.multiple(
"homeassistant.components.intellifire.config_flow.IntellifireControlAsync",
login=AsyncMock(),
get_username=AsyncMock(return_value="intellifire"),
)
async def test_no_discovery( async def test_no_discovery(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
@ -36,12 +48,31 @@ async def test_no_discovery(
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "Fireplace 12345" assert result2["type"] == RESULT_TYPE_FORM
assert result2["data"] == {CONF_HOST: "1.1.1.1"} assert result2["step_id"] == "api_config"
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "Fireplace 12345"
assert result3["data"] == {
CONF_HOST: "1.1.1.1",
CONF_USERNAME: "test",
CONF_PASSWORD: "AROONIE",
}
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@patch.multiple(
"homeassistant.components.intellifire.config_flow.IntellifireControlAsync",
login=AsyncMock(side_effect=mock_api_connection_error()),
get_username=AsyncMock(return_value="intellifire"),
)
async def test_single_discovery( async def test_single_discovery(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
@ -56,16 +87,48 @@ async def test_single_discovery(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
result2 = await hass.config_entries.flow.async_configure( await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: "192.168.1.69"} result["flow_id"], {CONF_HOST: "192.168.1.69"}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
print("Result:", result) result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_FORM
assert result3["errors"] == {"base": "iftapi_connect"}
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "Fireplace 12345" @patch.multiple(
assert result2["data"] == {CONF_HOST: "192.168.1.69"} "homeassistant.components.intellifire.config_flow.IntellifireControlAsync",
assert len(mock_setup_entry.mock_calls) == 1 login=AsyncMock(side_effect=LoginException()),
)
async def test_single_discovery_loign_error(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock,
) -> None:
"""Test single fireplace UDP discovery."""
with patch(
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace",
return_value=["192.168.1.69"],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: "192.168.1.69"}
)
await hass.async_block_till_done()
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_FORM
assert result3["errors"] == {"base": "api_error"}
async def test_manual_entry( async def test_manual_entry(
@ -73,7 +136,7 @@ async def test_manual_entry(
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock, mock_intellifire_config_flow: MagicMock,
) -> None: ) -> None:
"""Test for multiple firepalce discovery - involing a pick_device step.""" """Test for multiple Fireplace discovery - involving a pick_device step."""
with patch( with patch(
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace",
return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], return_value=["192.168.1.69", "192.168.1.33", "192.168.169"],
@ -107,15 +170,12 @@ async def test_multi_discovery(
) )
assert result["step_id"] == "pick_device" assert result["step_id"] == "pick_device"
await hass.config_entries.flow.async_configure(
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} result["flow_id"], user_input={CONF_HOST: "192.168.1.33"}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["step_id"] == "pick_device" assert result["step_id"] == "pick_device"
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
async def test_multi_discovery_cannot_connect( async def test_multi_discovery_cannot_connect(
hass: HomeAssistant, hass: HomeAssistant,
@ -200,10 +260,56 @@ async def test_picker_already_discovered(
CONF_HOST: "192.168.1.4", CONF_HOST: "192.168.1.4",
}, },
) )
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["type"] == RESULT_TYPE_FORM
assert result2["title"] == "Fireplace 12345" assert len(mock_setup_entry.mock_calls) == 0
assert result2["data"] == {CONF_HOST: "192.168.1.4"}
assert len(mock_setup_entry.mock_calls) == 2
@patch.multiple(
"homeassistant.components.intellifire.config_flow.IntellifireControlAsync",
login=AsyncMock(),
get_username=AsyncMock(return_value="intellifire"),
)
async def test_reauth_flow(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock,
) -> None:
"""Test the reauth flow."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"host": "192.168.1.3",
},
title="Fireplace 1234",
version=1,
unique_id="4444",
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": "reauth",
"unique_id": entry.unique_id,
"entry_id": entry.entry_id,
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "api_config"
with patch(
"homeassistant.config_entries.ConfigFlow.async_set_unique_id",
return_value=entry,
):
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_ABORT
async def test_dhcp_discovery_intellifire_device( async def test_dhcp_discovery_intellifire_device(