mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
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:
parent
73a368c242
commit
4d09078114
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -6,3 +6,5 @@ import logging
|
|||||||
DOMAIN = "intellifire"
|
DOMAIN = "intellifire"
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
CONF_SERIAL = "serial"
|
||||||
|
@ -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",
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user