Skip to content

Commit

Permalink
Reload ESPHome config entries when dashboard info received (home-assi…
Browse files Browse the repository at this point in the history
  • Loading branch information
balloob authored Jan 18, 2023
1 parent c40c37e commit 29337bc
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 70 deletions.
3 changes: 2 additions & 1 deletion homeassistant/components/esphome/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@
from homeassistant.helpers.template import Template

from .bluetooth import async_connect_scanner
from .const import DOMAIN
from .dashboard import async_get_dashboard
from .domain_data import DOMAIN, DomainData
from .domain_data import DomainData

# Import config flow so that it's added to the registry
from .entry_data import RuntimeEntryData
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/esphome/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.device_registry import format_mac

from . import CONF_DEVICE_NAME, CONF_NOISE_PSK, DOMAIN
from . import CONF_DEVICE_NAME, CONF_NOISE_PSK
from .const import DOMAIN
from .dashboard import async_get_dashboard, async_set_dashboard_info

ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
Expand Down Expand Up @@ -204,7 +205,7 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes

async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult:
"""Handle Supervisor service discovery."""
async_set_dashboard_info(
await async_set_dashboard_info(
self.hass,
discovery_info.slug,
discovery_info.config["host"],
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/esphome/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""ESPHome constants."""

DOMAIN = "esphome"
59 changes: 32 additions & 27 deletions homeassistant/components/esphome/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
import aiohttp
from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI

from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN

KEY_DASHBOARD = "esphome_dashboard"

Expand All @@ -21,23 +24,41 @@ def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None:
return hass.data.get(KEY_DASHBOARD)


def async_set_dashboard_info(
async def async_set_dashboard_info(
hass: HomeAssistant, addon_slug: str, host: str, port: int
) -> None:
"""Set the dashboard info."""
hass.data[KEY_DASHBOARD] = ESPHomeDashboard(
hass,
addon_slug,
f"http://{host}:{port}",
async_get_clientsession(hass),
)
url = f"http://{host}:{port}"

# Do nothing if we already have this data.
if (
(cur_dashboard := hass.data.get(KEY_DASHBOARD))
and cur_dashboard.addon_slug == addon_slug
and cur_dashboard.url == url
):
return

dashboard = ESPHomeDashboard(hass, addon_slug, url, async_get_clientsession(hass))
try:
await dashboard.async_request_refresh()
except UpdateFailed as err:
logging.getLogger(__name__).error("Ignoring dashboard info: %s", err)
return

hass.data[KEY_DASHBOARD] = dashboard

reloads = [
hass.config_entries.async_reload(entry.entry_id)
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.state == ConfigEntryState.LOADED
]
if reloads:
await asyncio.gather(*reloads)


class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]):
"""Class to interact with the ESPHome dashboard."""

_first_fetch_lock: asyncio.Lock | None = None

def __init__(
self,
hass: HomeAssistant,
Expand All @@ -53,25 +74,9 @@ def __init__(
update_interval=timedelta(minutes=5),
)
self.addon_slug = addon_slug
self.url = url
self.api = ESPHomeDashboardAPI(url, session)

async def ensure_data(self) -> None:
"""Ensure the update coordinator has data when this call finishes."""
if self.data:
return

if self._first_fetch_lock is not None:
async with self._first_fetch_lock:
# We know the data is fetched when lock is done
return

self._first_fetch_lock = asyncio.Lock()

async with self._first_fetch_lock:
await self.async_request_refresh()

self._first_fetch_lock = None

async def _async_update_data(self) -> dict:
"""Fetch device data."""
devices = await self.api.get_devices()
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/esphome/domain_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.storage import Store

from .const import DOMAIN
from .entry_data import RuntimeEntryData

STORAGE_VERSION = 1
DOMAIN = "esphome"
MAX_CACHED_SERVICES = 128

_DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData")
Expand Down
1 change: 0 additions & 1 deletion homeassistant/components/esphome/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ async def setup_update_entity() -> None:
unsub() # type: ignore[unreachable]

assert dashboard is not None
await dashboard.ensure_data()
async_add_entities([ESPHomeUpdateEntity(entry_data, dashboard)])

if entry_data.available:
Expand Down
11 changes: 11 additions & 0 deletions tests/components/esphome/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,14 @@ def mock_constructor(
"homeassistant.components.esphome.config_flow.APIClient", mock_client
):
yield mock_client


@pytest.fixture
def mock_dashboard():
"""Mock dashboard."""
data = {"configured": [], "importable": []}
with patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.get_devices",
return_value=data,
):
yield data
63 changes: 29 additions & 34 deletions tests/components/esphome/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,9 +471,10 @@ async def test_reauth_confirm_valid(hass, mock_client, mock_zeroconf):
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK


async def test_reauth_fixed_via_dashboard(hass, mock_client, mock_zeroconf):
async def test_reauth_fixed_via_dashboard(
hass, mock_client, mock_zeroconf, mock_dashboard
):
"""Test reauth fixed automatically via dashboard."""
dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052)

entry = MockConfigEntry(
domain=DOMAIN,
Expand All @@ -488,17 +489,16 @@ async def test_reauth_fixed_via_dashboard(hass, mock_client, mock_zeroconf):

mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")

mock_dashboard["configured"].append(
{
"name": "test",
"configuration": "test.yaml",
}
)

await dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052)

with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices",
return_value={
"configured": [
{
"name": "test",
"configuration": "test.yaml",
}
]
},
), patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key:
Expand All @@ -511,7 +511,7 @@ async def test_reauth_fixed_via_dashboard(hass, mock_client, mock_zeroconf):
},
)

assert result["type"] == FlowResultType.ABORT
assert result["type"] == FlowResultType.ABORT, result
assert result["reason"] == "reauth_successful"
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK

Expand Down Expand Up @@ -672,7 +672,9 @@ async def test_discovery_hassio(hass):
assert dash.addon_slug == "mock-slug"


async def test_zeroconf_encryption_key_via_dashboard(hass, mock_client, mock_zeroconf):
async def test_zeroconf_encryption_key_via_dashboard(
hass, mock_client, mock_zeroconf, mock_dashboard
):
"""Test encryption key retrieved from dashboard."""
service_info = zeroconf.ZeroconfServiceInfo(
host="192.168.43.183",
Expand All @@ -692,7 +694,14 @@ async def test_zeroconf_encryption_key_via_dashboard(hass, mock_client, mock_zer
assert flow["type"] == FlowResultType.FORM
assert flow["step_id"] == "discovery_confirm"

dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052)
mock_dashboard["configured"].append(
{
"name": "test8266",
"configuration": "test8266.yaml",
}
)

await dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052)

mock_client.device_info.side_effect = [
RequiresEncryptionAPIError,
Expand All @@ -704,16 +713,6 @@ async def test_zeroconf_encryption_key_via_dashboard(hass, mock_client, mock_zer
]

with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices",
return_value={
"configured": [
{
"name": "test8266",
"configuration": "test8266.yaml",
}
]
},
), patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key:
Expand All @@ -736,7 +735,7 @@ async def test_zeroconf_encryption_key_via_dashboard(hass, mock_client, mock_zer


async def test_zeroconf_no_encryption_key_via_dashboard(
hass, mock_client, mock_zeroconf
hass, mock_client, mock_zeroconf, mock_dashboard
):
"""Test encryption key not retrieved from dashboard."""
service_info = zeroconf.ZeroconfServiceInfo(
Expand All @@ -757,17 +756,13 @@ async def test_zeroconf_no_encryption_key_via_dashboard(
assert flow["type"] == FlowResultType.FORM
assert flow["step_id"] == "discovery_confirm"

dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052)
await dashboard.async_set_dashboard_info(hass, "mock-slug", "mock-host", 6052)

mock_client.device_info.side_effect = RequiresEncryptionAPIError

with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices",
return_value={"configured": []},
):
result = await hass.config_entries.flow.async_configure(
flow["flow_id"], user_input={}
)
result = await hass.config_entries.flow.async_configure(
flow["flow_id"], user_input={}
)

assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "encryption_key"
22 changes: 22 additions & 0 deletions tests/components/esphome/test_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Test ESPHome dashboard features."""
from unittest.mock import patch

from homeassistant.components.esphome import dashboard
from homeassistant.config_entries import ConfigEntryState


async def test_new_info_reload_config_entries(hass, init_integration, mock_dashboard):
"""Test config entries are reloaded when new info is set."""
assert init_integration.state == ConfigEntryState.LOADED

with patch("homeassistant.components.esphome.async_setup_entry") as mock_setup:
await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052)

assert len(mock_setup.mock_calls) == 1
assert mock_setup.mock_calls[0][1][1] == init_integration

# Test it's a no-op when the same info is set
with patch("homeassistant.components.esphome.async_setup_entry") as mock_setup:
await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052)

assert len(mock_setup.mock_calls) == 0
7 changes: 3 additions & 4 deletions tests/components/esphome/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,20 @@ async def test_update_entity(
hass,
mock_config_entry,
mock_device_info,
mock_dashboard,
devices_payload,
expected_state,
expected_attributes,
):
"""Test ESPHome update entity."""
async_set_dashboard_info(hass, "mock-addon-slug", "mock-addon-host", 1234)
mock_dashboard["configured"] = devices_payload
await async_set_dashboard_info(hass, "mock-addon-slug", "mock-addon-host", 1234)

mock_config_entry.add_to_hass(hass)

with patch(
"homeassistant.components.esphome.update.DomainData.get_entry_data",
return_value=Mock(available=True, device_info=mock_device_info),
), patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_devices",
return_value={"configured": devices_payload},
):
assert await hass.config_entries.async_forward_entry_setup(
mock_config_entry, "update"
Expand Down

0 comments on commit 29337bc

Please sign in to comment.