Intellifire DHCP Auto Discovery (#67053)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Jeef 2022-03-20 17:51:54 -06:00 committed by GitHub
parent 8bbbd1947d
commit ed94cc3673
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 142 additions and 27 deletions

View file

@ -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},
)

View file

@ -6,5 +6,7 @@
"requirements": ["intellifire4py==1.0.1"],
"codeowners": ["@jeeftor"],
"iot_class": "local_polling",
"loggers": ["intellifire4py"]
"loggers": ["intellifire4py"],
"dhcp": [{"hostname": "zentrios-*"}]
}

View file

@ -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."
}
}
}

View file

@ -1,26 +1,27 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
"already_configured": "Device is already configured",
"not_intellifire_device": "Not an IntelliFire Device."
},
"error": {
"cannot_connect": "Could not connect to a fireplace endpoint at url: http://{host}/poll\nVerify IP address and try again"
"cannot_connect": "Failed to connect"
},
"flow_title": "{serial} ({host})",
"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)"
}
"dhcp_confirm": {
"description": "Do you want to setup {host}\nSerial: {serial}?"
},
"user": {
"description": "Username and password are the same information used in your IntelliFire Android/iOS application.",
"title": "IntelliFire Config"
"manual_device_entry": {
"data": {
"host": "Host"
},
"description": "Local Configuration"
},
"pick_device": {
"title": "Device Selection",
"description": "The following IntelliFire devices were discovered. Please select which you wish to configure."
"data": {
"host": "Host"
}
}
}
}

View file

@ -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*'},

View file

@ -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"