mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Set up Almond Web to connect to HA (#28603)
* Set up Almond Web to connect to HA * Add missing string * Add type
This commit is contained in:
parent
a1f2b6d402
commit
5961215e6e
@ -10,16 +10,18 @@ from aiohttp import ClientSession, ClientError
|
||||
from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.const import CONF_TYPE, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant, CoreState
|
||||
from homeassistant.const import CONF_TYPE, CONF_HOST, EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
config_entry_oauth2_flow,
|
||||
event,
|
||||
intent,
|
||||
aiohttp_client,
|
||||
storage,
|
||||
network,
|
||||
)
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import conversation
|
||||
@ -33,6 +35,8 @@ CONF_CLIENT_SECRET = "client_secret"
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = DOMAIN
|
||||
|
||||
ALMOND_SETUP_DELAY = 30
|
||||
|
||||
DEFAULT_OAUTH2_HOST = "https://almond.stanford.edu"
|
||||
DEFAULT_LOCAL_HOST = "http://localhost:3000"
|
||||
|
||||
@ -93,7 +97,7 @@ async def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry):
|
||||
"""Set up Almond config entry."""
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
@ -112,12 +116,50 @@ async def async_setup_entry(hass, entry):
|
||||
api = WebAlmondAPI(auth)
|
||||
agent = AlmondAgent(hass, api, entry)
|
||||
|
||||
# Hass.io does its own configuration of Almond.
|
||||
if entry.data.get("is_hassio") or entry.data["type"] != TYPE_LOCAL:
|
||||
conversation.async_set_agent(hass, agent)
|
||||
return True
|
||||
# Hass.io does its own configuration.
|
||||
if not entry.data.get("is_hassio"):
|
||||
# If we're not starting or local, set up Almond right away
|
||||
if hass.state != CoreState.not_running or entry.data["type"] == TYPE_LOCAL:
|
||||
await _configure_almond_for_ha(hass, entry, api)
|
||||
|
||||
# Configure Almond to connect to Home Assistant
|
||||
else:
|
||||
# OAuth2 implementations can potentially rely on the HA Cloud url.
|
||||
# This url is not be available until 30 seconds after boot.
|
||||
|
||||
async def configure_almond(_now):
|
||||
try:
|
||||
await _configure_almond_for_ha(hass, entry, api)
|
||||
except ConfigEntryNotReady:
|
||||
_LOGGER.warning(
|
||||
"Unable to configure Almond to connect to Home Assistant"
|
||||
)
|
||||
|
||||
async def almond_hass_start(_event):
|
||||
event.async_call_later(hass, ALMOND_SETUP_DELAY, configure_almond)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, almond_hass_start)
|
||||
|
||||
conversation.async_set_agent(hass, agent)
|
||||
return True
|
||||
|
||||
|
||||
async def _configure_almond_for_ha(
|
||||
hass: HomeAssistant, entry: config_entries.ConfigEntry, api: WebAlmondAPI
|
||||
):
|
||||
"""Configure Almond to connect to HA."""
|
||||
|
||||
if entry.data["type"] == TYPE_OAUTH2:
|
||||
# If we're connecting over OAuth2, we will only set up connection
|
||||
# with Home Assistant if we're remotely accessible.
|
||||
hass_url = network.async_get_external_url(hass)
|
||||
else:
|
||||
hass_url = hass.config.api.base_url
|
||||
|
||||
# If hass_url is None, we're not going to configure Almond to connect to HA.
|
||||
if hass_url is None:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url)
|
||||
store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
data = await store.async_load()
|
||||
|
||||
@ -144,11 +186,11 @@ async def async_setup_entry(hass, entry):
|
||||
|
||||
# Store token in Almond
|
||||
try:
|
||||
with async_timeout.timeout(10):
|
||||
with async_timeout.timeout(30):
|
||||
await api.async_create_device(
|
||||
{
|
||||
"kind": "io.home-assistant",
|
||||
"hassUrl": hass.config.api.base_url,
|
||||
"hassUrl": hass_url,
|
||||
"accessToken": access_token,
|
||||
"refreshToken": "",
|
||||
# 5 years from now in ms.
|
||||
@ -169,9 +211,6 @@ async def async_setup_entry(hass, entry):
|
||||
if token.id != refresh_token.id:
|
||||
await hass.auth.async_remove_refresh_token(token)
|
||||
|
||||
conversation.async_set_agent(hass, agent)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload Almond."""
|
||||
@ -203,7 +242,9 @@ class AlmondOAuth(AbstractAlmondWebAuth):
|
||||
class AlmondAgent(conversation.AbstractConversationAgent):
|
||||
"""Almond conversation agent."""
|
||||
|
||||
def __init__(self, hass: core.HomeAssistant, api: WebAlmondAPI, entry):
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: WebAlmondAPI, entry: config_entries.ConfigEntry
|
||||
):
|
||||
"""Initialize the agent."""
|
||||
self.hass = hass
|
||||
self.api = api
|
||||
|
@ -1,5 +1,10 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "Pick Authentication Method"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_setup": "You can only configure one Almond account.",
|
||||
"cannot_connect": "Unable to connect to the Almond server.",
|
||||
|
38
homeassistant/helpers/network.py
Normal file
38
homeassistant/helpers/network.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""Network helpers."""
|
||||
from typing import Optional, cast
|
||||
from ipaddress import ip_address
|
||||
|
||||
import yarl
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.network import is_local
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_get_external_url(hass: HomeAssistant) -> Optional[str]:
|
||||
"""Get external url of this instance.
|
||||
|
||||
Note: currently it takes 30 seconds after Home Assistant starts for
|
||||
cloud.async_remote_ui_url to work.
|
||||
"""
|
||||
if "cloud" in hass.config.components:
|
||||
try:
|
||||
return cast(str, hass.components.cloud.async_remote_ui_url())
|
||||
except hass.components.cloud.CloudNotAvailable:
|
||||
pass
|
||||
|
||||
if hass.config.api is None:
|
||||
return None
|
||||
|
||||
base_url = yarl.URL(hass.config.api.base_url)
|
||||
|
||||
try:
|
||||
if is_local(ip_address(base_url.host)):
|
||||
return None
|
||||
except ValueError:
|
||||
# ip_address raises ValueError if host is not an IP address
|
||||
pass
|
||||
|
||||
return str(base_url)
|
113
tests/components/almond/test_init.py
Normal file
113
tests/components/almond/test_init.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""Tests for Almond set up."""
|
||||
from unittest.mock import patch
|
||||
from time import time
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, core
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components.almond import const
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from tests.common import MockConfigEntry, mock_coro, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_hass_state(hass):
|
||||
"""Mock the hass.state to be not_running."""
|
||||
hass.state = core.CoreState.not_running
|
||||
|
||||
|
||||
async def test_set_up_oauth_remote_url(hass, aioclient_mock):
|
||||
"""Test we set up Almond to connect to HA if we have external url."""
|
||||
entry = MockConfigEntry(
|
||||
domain="almond",
|
||||
data={
|
||||
"type": const.TYPE_OAUTH2,
|
||||
"auth_implementation": "local",
|
||||
"host": "http://localhost:9999",
|
||||
"token": {"expires_at": time() + 1000, "access_token": "abcd"},
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
return_value=mock_coro(),
|
||||
):
|
||||
assert await async_setup_component(hass, "almond", {})
|
||||
|
||||
assert entry.state == config_entries.ENTRY_STATE_LOADED
|
||||
|
||||
with patch("homeassistant.components.almond.ALMOND_SETUP_DELAY", 0), patch(
|
||||
"homeassistant.helpers.network.async_get_external_url",
|
||||
return_value="https://example.nabu.casa",
|
||||
), patch(
|
||||
"pyalmond.WebAlmondAPI.async_create_device", return_value=mock_coro()
|
||||
) as mock_create_device:
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, utcnow())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_create_device.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_set_up_oauth_no_external_url(hass, aioclient_mock):
|
||||
"""Test we do not set up Almond to connect to HA if we have no external url."""
|
||||
entry = MockConfigEntry(
|
||||
domain="almond",
|
||||
data={
|
||||
"type": const.TYPE_OAUTH2,
|
||||
"auth_implementation": "local",
|
||||
"host": "http://localhost:9999",
|
||||
"token": {"expires_at": time() + 1000, "access_token": "abcd"},
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
return_value=mock_coro(),
|
||||
), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device:
|
||||
assert await async_setup_component(hass, "almond", {})
|
||||
|
||||
assert entry.state == config_entries.ENTRY_STATE_LOADED
|
||||
assert len(mock_create_device.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_set_up_hassio(hass, aioclient_mock):
|
||||
"""Test we do not set up Almond to connect to HA if we use hassio."""
|
||||
entry = MockConfigEntry(
|
||||
domain="almond",
|
||||
data={
|
||||
"is_hassio": True,
|
||||
"type": const.TYPE_LOCAL,
|
||||
"host": "http://localhost:9999",
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device:
|
||||
assert await async_setup_component(hass, "almond", {})
|
||||
|
||||
assert entry.state == config_entries.ENTRY_STATE_LOADED
|
||||
assert len(mock_create_device.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_set_up_local(hass, aioclient_mock):
|
||||
"""Test we do not set up Almond to connect to HA if we use hassio."""
|
||||
entry = MockConfigEntry(
|
||||
domain="almond",
|
||||
data={"type": const.TYPE_LOCAL, "host": "http://localhost:9999"},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"pyalmond.WebAlmondAPI.async_create_device", return_value=mock_coro()
|
||||
) as mock_create_device:
|
||||
assert await async_setup_component(hass, "almond", {})
|
||||
|
||||
assert entry.state == config_entries.ENTRY_STATE_LOADED
|
||||
assert len(mock_create_device.mock_calls) == 1
|
34
tests/helpers/test_network.py
Normal file
34
tests/helpers/test_network.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""Test network helper."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant.helpers import network
|
||||
from homeassistant.components import cloud
|
||||
|
||||
|
||||
async def test_get_external_url(hass):
|
||||
"""Test get_external_url."""
|
||||
hass.config.api = Mock(base_url="http://192.168.1.100:8123")
|
||||
|
||||
assert network.async_get_external_url(hass) is None
|
||||
|
||||
hass.config.api = Mock(base_url="http://example.duckdns.org:8123")
|
||||
|
||||
assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123"
|
||||
|
||||
hass.config.components.add("cloud")
|
||||
|
||||
assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123"
|
||||
|
||||
with patch.object(
|
||||
hass.components.cloud,
|
||||
"async_remote_ui_url",
|
||||
side_effect=cloud.CloudNotAvailable,
|
||||
):
|
||||
assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123"
|
||||
|
||||
with patch.object(
|
||||
hass.components.cloud,
|
||||
"async_remote_ui_url",
|
||||
return_value="https://example.nabu.casa",
|
||||
):
|
||||
assert network.async_get_external_url(hass) == "https://example.nabu.casa"
|
Loading…
x
Reference in New Issue
Block a user