From 82ec769ec531639f9530044b0415093694ae4efc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Jan 2023 08:30:52 -0500 Subject: [PATCH] Handle ESPHome dashboard discovery (#85662) --- homeassistant/components/esphome/__init__.py | 3 ++ .../components/esphome/config_flow.py | 12 +++++++ homeassistant/components/esphome/dashboard.py | 28 ++++++++++++++++ homeassistant/components/esphome/strings.json | 3 +- tests/components/esphome/test_config_flow.py | 33 +++++++++++++++++-- 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/esphome/dashboard.py diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index bfee5658679..979057a194c 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -56,6 +56,7 @@ from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.template import Template from .bluetooth import async_connect_scanner +from .dashboard import async_get_dashboard from .domain_data import DOMAIN, DomainData # Import config flow so that it's added to the registry @@ -352,6 +353,8 @@ def _async_setup_device_registry( configuration_url = None if device_info.webserver_port > 0: configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}" + elif dashboard := async_get_dashboard(hass): + configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}" manufacturer = "espressif" if device_info.manufacturer: diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 25025154f19..1c8f795c1a7 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -17,6 +17,7 @@ from aioesphomeapi import ( import voluptuous as vol from homeassistant.components import dhcp, zeroconf +from homeassistant.components.hassio.discovery import HassioServiceInfo from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback @@ -24,6 +25,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from . import CONF_NOISE_PSK, DOMAIN +from .dashboard import async_set_dashboard_info ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" ESPHOME_URL = "https://esphome.io/" @@ -173,6 +175,16 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): # for configured devices. return self.async_abort(reason="already_configured") + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + """Handle Supervisor service discovery.""" + async_set_dashboard_info( + self.hass, + discovery_info.slug, + discovery_info.config["host"], + discovery_info.config["port"], + ) + return self.async_abort(reason="service_received") + @callback def _async_get_entry(self) -> FlowResult: config_data = { diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py new file mode 100644 index 00000000000..4b95fa0d6fd --- /dev/null +++ b/homeassistant/components/esphome/dashboard.py @@ -0,0 +1,28 @@ +"""Files to interact with a the ESPHome dashboard.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.core import HomeAssistant, callback + +KEY_DASHBOARD = "esphome_dashboard" + + +@callback +def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None: + """Get an instance of the dashboard if set.""" + return hass.data.get(KEY_DASHBOARD) + + +def async_set_dashboard_info( + hass: HomeAssistant, addon_slug: str, _host: str, _port: int +) -> None: + """Set the dashboard info.""" + hass.data[KEY_DASHBOARD] = ESPHomeDashboard(addon_slug) + + +@dataclass +class ESPHomeDashboard: + """Class to interact with the ESPHome dashboard.""" + + addon_slug: str diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 4c863a9b488..49634fc830e 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -4,7 +4,8 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "mdns_missing_mac": "Missing MAC address in MDNS properties." + "mdns_missing_mac": "Missing MAC address in MDNS properties.", + "service_received": "Service received" }, "error": { "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address", diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 4a40f4ec4d7..8ac1b23eff0 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -11,9 +11,15 @@ from aioesphomeapi import ( ) import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp, zeroconf -from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData +from homeassistant.components.esphome import ( + CONF_NOISE_PSK, + DOMAIN, + DomainData, + dashboard, +) +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import FlowResultType @@ -620,3 +626,26 @@ async def test_discovery_dhcp_no_changes(hass, mock_client): assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "192.168.43.183" + + +async def test_discovery_hassio(hass): + """Test dashboard discovery.""" + result = await hass.config_entries.flow.async_init( + "esphome", + data=HassioServiceInfo( + config={ + "host": "mock-esphome", + "port": 6052, + }, + name="ESPHome", + slug="mock-slug", + ), + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "service_received" + + dash = dashboard.async_get_dashboard(hass) + assert dash is not None + assert dash.addon_slug == "mock-slug"