Intellifire DHCP Auto Discovery (#67053)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
8bbbd1947d
commit
ed94cc3673
6 changed files with 142 additions and 27 deletions
|
@ -1,6 +1,7 @@
|
|||
"""Config flow for IntelliFire integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientConnectionError
|
||||
|
@ -8,6 +9,7 @@ from intellifire4py import AsyncUDPFireplaceFinder, IntellifireAsync
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.dhcp import DhcpServiceInfo
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
|
@ -18,6 +20,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
|
|||
MANUAL_ENTRY_STRING = "IP Address" # Simplified so it does not have to be translated
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiscoveredHostInfo:
|
||||
"""Host info for discovery."""
|
||||
|
||||
ip: str
|
||||
serial: str | None
|
||||
|
||||
|
||||
async def validate_host_input(host: str) -> str:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
|
@ -39,7 +49,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
def __init__(self):
|
||||
"""Initialize the Config Flow Handler."""
|
||||
self._config_context = {}
|
||||
self._not_configured_hosts: list[str] = []
|
||||
self._not_configured_hosts: list[DiscoveredHostInfo] = []
|
||||
self._discovered_host: DiscoveredHostInfo
|
||||
|
||||
async def _find_fireplaces(self):
|
||||
"""Perform UDP discovery."""
|
||||
|
@ -52,7 +63,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
}
|
||||
|
||||
self._not_configured_hosts = [
|
||||
ip for ip in discovered_hosts if ip not in configured_hosts
|
||||
DiscoveredHostInfo(ip, None)
|
||||
for ip in discovered_hosts
|
||||
if ip not in configured_hosts
|
||||
]
|
||||
LOGGER.debug("Discovered Hosts: %s", discovered_hosts)
|
||||
LOGGER.debug("Configured Hosts: %s", configured_hosts)
|
||||
|
@ -62,7 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
"""Validate and create the entry."""
|
||||
self._async_abort_entries_match({CONF_HOST: host})
|
||||
serial = await validate_host_input(host)
|
||||
await self.async_set_unique_id(serial)
|
||||
await self.async_set_unique_id(serial, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
return self.async_create_entry(
|
||||
title=f"Fireplace {serial}",
|
||||
|
@ -108,7 +121,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): vol.In(
|
||||
self._not_configured_hosts + [MANUAL_ENTRY_STRING]
|
||||
[host.ip for host in self._not_configured_hosts]
|
||||
+ [MANUAL_ENTRY_STRING]
|
||||
)
|
||||
}
|
||||
),
|
||||
|
@ -127,3 +141,44 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
return await self.async_step_pick_device()
|
||||
LOGGER.debug("Running Step: manual_device_entry")
|
||||
return await self.async_step_manual_device_entry()
|
||||
|
||||
async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult:
|
||||
"""Handle DHCP Discovery."""
|
||||
|
||||
# Run validation logic on ip
|
||||
host = discovery_info.ip
|
||||
|
||||
self._async_abort_entries_match({CONF_HOST: host})
|
||||
try:
|
||||
serial = await validate_host_input(host)
|
||||
except (ConnectionError, ClientConnectionError):
|
||||
return self.async_abort(reason="not_intellifire_device")
|
||||
|
||||
await self.async_set_unique_id(serial)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
self._discovered_host = DiscoveredHostInfo(ip=host, serial=serial)
|
||||
|
||||
placeholders = {CONF_HOST: host, "serial": serial}
|
||||
self.context["title_placeholders"] = placeholders
|
||||
self._set_confirm_only()
|
||||
|
||||
return await self.async_step_dhcp_confirm()
|
||||
|
||||
async def async_step_dhcp_confirm(self, user_input=None):
|
||||
"""Attempt to confirm."""
|
||||
|
||||
# Add the hosts one by one
|
||||
host = self._discovered_host.ip
|
||||
serial = self._discovered_host.serial
|
||||
|
||||
if user_input is None:
|
||||
# Show the confirmation dialog
|
||||
return self.async_show_form(
|
||||
step_id="dhcp_confirm",
|
||||
description_placeholders={CONF_HOST: host, "serial": serial},
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Fireplace {serial}",
|
||||
data={CONF_HOST: host},
|
||||
)
|
||||
|
|
|
@ -6,5 +6,7 @@
|
|||
"requirements": ["intellifire4py==1.0.1"],
|
||||
"codeowners": ["@jeeftor"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["intellifire4py"]
|
||||
"loggers": ["intellifire4py"],
|
||||
"dhcp": [{"hostname": "zentrios-*"}]
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "{serial} ({host})",
|
||||
"step": {
|
||||
"manual_device_entry": {
|
||||
"description": "Local Configuration",
|
||||
|
@ -7,6 +8,9 @@
|
|||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
},
|
||||
"dhcp_confirm": {
|
||||
"description": "Do you want to setup {host}\nSerial: {serial}?"
|
||||
},
|
||||
"pick_device": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
|
@ -17,7 +21,8 @@
|
|||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"not_intellifire_device":"Not an IntelliFire Device."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Could not connect to a fireplace endpoint at url: http://{host}/poll\nVerify IP address and try again"
|
||||
},
|
||||
"step": {
|
||||
"manual_device_entry": {
|
||||
"title": "IntelliFire - Local Config",
|
||||
"description": "Enter the IP address of the IntelliFire unit on your local network.",
|
||||
"data": {
|
||||
"host": "Host (IP Address)"
|
||||
}
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"not_intellifire_device": "Not an IntelliFire Device."
|
||||
},
|
||||
"user": {
|
||||
"description": "Username and password are the same information used in your IntelliFire Android/iOS application.",
|
||||
"title": "IntelliFire Config"
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect"
|
||||
},
|
||||
"pick_device": {
|
||||
"title": "Device Selection",
|
||||
"description": "The following IntelliFire devices were discovered. Please select which you wish to configure."
|
||||
"flow_title": "{serial} ({host})",
|
||||
"step": {
|
||||
"dhcp_confirm": {
|
||||
"description": "Do you want to setup {host}\nSerial: {serial}?"
|
||||
},
|
||||
"manual_device_entry": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
},
|
||||
"description": "Local Configuration"
|
||||
},
|
||||
"pick_device": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@ DHCP: list[dict[str, str | bool]] = [
|
|||
{'domain': 'hunterdouglas_powerview',
|
||||
'hostname': 'hunter*',
|
||||
'macaddress': '002674*'},
|
||||
{'domain': 'intellifire', 'hostname': 'zentrios-*'},
|
||||
{'domain': 'isy994', 'registered_devices': True},
|
||||
{'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'},
|
||||
{'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'},
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import dhcp
|
||||
from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING
|
||||
from homeassistant.components.intellifire.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
@ -203,3 +204,53 @@ async def test_picker_already_discovered(
|
|||
assert result2["title"] == "Fireplace 12345"
|
||||
assert result2["data"] == {CONF_HOST: "192.168.1.4"}
|
||||
assert len(mock_setup_entry.mock_calls) == 2
|
||||
|
||||
|
||||
async def test_dhcp_discovery_intellifire_device(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_intellifire_config_flow: MagicMock,
|
||||
) -> None:
|
||||
"""Test successful DHCP Discovery."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=dhcp.DhcpServiceInfo(
|
||||
ip="1.1.1.1",
|
||||
macaddress="AA:BB:CC:DD:EE:FF",
|
||||
hostname="zentrios-Test",
|
||||
),
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "dhcp_confirm"
|
||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result2["type"] == RESULT_TYPE_FORM
|
||||
assert result2["step_id"] == "dhcp_confirm"
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"], user_input={}
|
||||
)
|
||||
assert result3["title"] == "Fireplace 12345"
|
||||
assert result3["data"] == {"host": "1.1.1.1"}
|
||||
|
||||
|
||||
async def test_dhcp_discovery_non_intellifire_device(
|
||||
hass: HomeAssistant,
|
||||
mock_intellifire_config_flow: MagicMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test failed DHCP Discovery."""
|
||||
|
||||
mock_intellifire_config_flow.poll.side_effect = ConnectionError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=dhcp.DhcpServiceInfo(
|
||||
ip="1.1.1.1",
|
||||
macaddress="AA:BB:CC:DD:EE:FF",
|
||||
hostname="zentrios-Evil",
|
||||
),
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "not_intellifire_device"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue