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.""" """Config flow for IntelliFire integration."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import Any from typing import Any
from aiohttp import ClientConnectionError from aiohttp import ClientConnectionError
@ -8,6 +9,7 @@ from intellifire4py import AsyncUDPFireplaceFinder, IntellifireAsync
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.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.data_entry_flow import FlowResult 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 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: async def validate_host_input(host: str) -> str:
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
@ -39,7 +49,8 @@ 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._config_context = {}
self._not_configured_hosts: list[str] = [] self._not_configured_hosts: list[DiscoveredHostInfo] = []
self._discovered_host: DiscoveredHostInfo
async def _find_fireplaces(self): async def _find_fireplaces(self):
"""Perform UDP discovery.""" """Perform UDP discovery."""
@ -52,7 +63,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
} }
self._not_configured_hosts = [ 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("Discovered Hosts: %s", discovered_hosts)
LOGGER.debug("Configured Hosts: %s", configured_hosts) LOGGER.debug("Configured Hosts: %s", configured_hosts)
@ -62,7 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Validate and create the entry.""" """Validate and create the entry."""
self._async_abort_entries_match({CONF_HOST: host}) self._async_abort_entries_match({CONF_HOST: host})
serial = await validate_host_input(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}) self._abort_if_unique_id_configured(updates={CONF_HOST: host})
return self.async_create_entry( return self.async_create_entry(
title=f"Fireplace {serial}", title=f"Fireplace {serial}",
@ -108,7 +121,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema( data_schema=vol.Schema(
{ {
vol.Required(CONF_HOST): vol.In( 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() 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_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"], "requirements": ["intellifire4py==1.0.1"],
"codeowners": ["@jeeftor"], "codeowners": ["@jeeftor"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["intellifire4py"] "loggers": ["intellifire4py"],
"dhcp": [{"hostname": "zentrios-*"}]
} }

View file

@ -1,5 +1,6 @@
{ {
"config": { "config": {
"flow_title": "{serial} ({host})",
"step": { "step": {
"manual_device_entry": { "manual_device_entry": {
"description": "Local Configuration", "description": "Local Configuration",
@ -7,6 +8,9 @@
"host": "[%key:common::config_flow::data::host%]" "host": "[%key:common::config_flow::data::host%]"
} }
}, },
"dhcp_confirm": {
"description": "Do you want to setup {host}\nSerial: {serial}?"
},
"pick_device": { "pick_device": {
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]" "host": "[%key:common::config_flow::data::host%]"
@ -17,7 +21,8 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}, },
"abort": { "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,27 +1,28 @@
{ {
"config": { "config": {
"abort": { "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"
},
"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)"
}
}, },
"user": { "error": {
"description": "Username and password are the same information used in your IntelliFire Android/iOS application.", "cannot_connect": "Failed to connect"
"title": "IntelliFire Config"
}, },
"pick_device": { "flow_title": "{serial} ({host})",
"title": "Device Selection", "step": {
"description": "The following IntelliFire devices were discovered. Please select which you wish to configure." "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"
}
}
} }
}
} }
} }

View file

@ -49,6 +49,7 @@ DHCP: list[dict[str, str | bool]] = [
{'domain': 'hunterdouglas_powerview', {'domain': 'hunterdouglas_powerview',
'hostname': 'hunter*', 'hostname': 'hunter*',
'macaddress': '002674*'}, 'macaddress': '002674*'},
{'domain': 'intellifire', 'hostname': 'zentrios-*'},
{'domain': 'isy994', 'registered_devices': True}, {'domain': 'isy994', 'registered_devices': True},
{'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'}, {'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'},
{'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'},

View file

@ -2,6 +2,7 @@
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from homeassistant import config_entries from homeassistant import config_entries
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
@ -203,3 +204,53 @@ async def test_picker_already_discovered(
assert result2["title"] == "Fireplace 12345" assert result2["title"] == "Fireplace 12345"
assert result2["data"] == {CONF_HOST: "192.168.1.4"} assert result2["data"] == {CONF_HOST: "192.168.1.4"}
assert len(mock_setup_entry.mock_calls) == 2 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"