Skip to content

Commit

Permalink
Set up new endpoint for discovery (#136)
Browse files Browse the repository at this point in the history
Since the /api/discovery_info endpoint has been deprecated, we lost a
way to retrieve a unique identifier from the remote instance. This
change introduces /api/remote_homeassistant/discovery containing the
information we are missing. The downside is that the component now must
be installed on the remote instance as well. The upside however is that
we now can add any new endpoint we need for additional features in the
future, which is good.

Fixes #133
  • Loading branch information
postlund authored Oct 13, 2021
1 parent 4bf7702 commit f546580
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 100 deletions.
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ Platform | Description
-- | --
`remote_homeassistant` | Link multiple Home-Assistant instances together .

The master instance connects to the Websocket APIs of the secondary instances (already enabled out of box), the connection options are specified via the `host`, `port`, and `secure` configuration parameters. If the secondary instance requires an access token to connect (created on the Profile page), it can be set via the `access_token` parameter. To ignore SSL warnings in secure mode, set the `verify_ssl` parameter to false.
The main instance connects to the Websocket APIs of the remote instances (already enabled out of box), the connection options are specified via the `host`, `port`, and `secure` configuration parameters. If the remote instance requires an access token to connect (created on the Profile page), it can be set via the `access_token` parameter. To ignore SSL warnings in secure mode, set the `verify_ssl` parameter to false.

After the connection is completed, the remote states get populated into the master instance.
The entity ids can optionally be prefixed via the `entity_prefix` parameter.

The component keeps track which objects originate from which instance. Whenever a service is called on an object, the call gets forwarded to the particular secondary instance.
The component keeps track which objects originate from which instance. Whenever a service is called on an object, the call gets forwarded to the particular remote instance.

When the connection to the remote instance is lost, all previously published states are removed again from the local state registry.

Expand All @@ -29,7 +29,7 @@ A possible use case for this is to be able to use different Z-Wave networks, on

## Installation

This component should be installed on the main instance of Home Assistant
This component *must* be installed on both the main and remote instance of Home Assistant

If you use HACS:

Expand All @@ -38,7 +38,18 @@ If you use HACS:
Otherwise:

1. To use this plugin, copy the `remote_homeassistant` folder into your [custom_components folder](https://developers.home-assistant.io/docs/creating_integration_file_structure/#where-home-assistant-looks-for-integrations).
2. Add `remote_homeassistant:` to your HA configuration.


**Remote instance**

On the remote instance you also need to add this to `configuration.yaml`:

```yaml
remote_homeassistant:
instances:
```
This is not needed on the main instance.
## Configuration
Expand Down Expand Up @@ -229,7 +240,7 @@ services:
### Missing Components
If you have remote domains (e.g. `switch`), that are not loaded on the master instance you need to list them under `load_components`, otherwise you'll get a `Call service failed` error.
If you have remote domains (e.g. `switch`), that are not loaded on the main instance you need to list them under `load_components`, otherwise you'll get a `Call service failed` error.
E.g. on the master:
Expand Down
90 changes: 45 additions & 45 deletions custom_components/remote_homeassistant/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,44 @@
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/remote_homeassistant/
"""
import fnmatch
import logging
import copy
import asyncio
import copy
import fnmatch
import inspect
import logging
import re
from contextlib import suppress

import aiohttp
import re

import voluptuous as vol

from homeassistant.core import HomeAssistant, callback, Context
import homeassistant.components.websocket_api.auth as api
from homeassistant.core import EventOrigin, split_entity_id
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.const import (
CONF_HOST,
CONF_PORT,
EVENT_CALL_SERVICE,
EVENT_HOMEASSISTANT_STOP,
EVENT_STATE_CHANGED,
CONF_EXCLUDE,
CONF_ENTITIES,
CONF_ENTITY_ID,
CONF_DOMAINS,
CONF_INCLUDE,
CONF_UNIT_OF_MEASUREMENT,
CONF_ABOVE,
CONF_BELOW,
CONF_VERIFY_SSL,
CONF_ACCESS_TOKEN,
SERVICE_RELOAD,
)
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
from homeassistant.config import DATA_CUSTOMIZE
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (CONF_ABOVE, CONF_ACCESS_TOKEN, CONF_BELOW,
CONF_DOMAINS, CONF_ENTITIES, CONF_ENTITY_ID,
CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE,
CONF_PORT, CONF_UNIT_OF_MEASUREMENT,
CONF_VERIFY_SSL, EVENT_CALL_SERVICE,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
SERVICE_RELOAD)
from homeassistant.core import (Context, EventOrigin, HomeAssistant, callback,
split_entity_id)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.reload import async_integration_yaml_config
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.setup import async_setup_component

from .const import (
CONF_REMOTE_CONNECTION,
CONF_UNSUB_LISTENER,
CONF_INCLUDE_DOMAINS,
CONF_INCLUDE_ENTITIES,
CONF_EXCLUDE_DOMAINS,
CONF_EXCLUDE_ENTITIES,
CONF_OPTIONS,
CONF_LOAD_COMPONENTS,
CONF_SERVICE_PREFIX,
CONF_SERVICES,
DOMAIN,
)
from .rest_api import UnsupportedVersion, async_get_discovery_info
from .const import (CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES,
CONF_INCLUDE_DOMAINS, CONF_INCLUDE_ENTITIES,
CONF_LOAD_COMPONENTS, CONF_OPTIONS, CONF_REMOTE_CONNECTION,
CONF_SERVICE_PREFIX, CONF_SERVICES, CONF_UNSUB_LISTENER,
DOMAIN)
from .proxy_services import ProxyServices
from .rest_api import UnsupportedVersion, async_get_discovery_info

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -221,6 +201,8 @@ async def _handle_reload(service):

await asyncio.gather(*update_tasks)

hass.http.register_view(DiscoveryInfoView())

hass.helpers.service.async_register_admin_service(
DOMAIN,
SERVICE_RELOAD,
Expand All @@ -234,6 +216,7 @@ async def _handle_reload(service):
DOMAIN, context={"source": SOURCE_IMPORT}, data=instance
)
)

return True


Expand Down Expand Up @@ -746,3 +729,20 @@ def got_states(message):
await self.call(got_states, "get_states")

await self.proxy_services.load()


class DiscoveryInfoView(HomeAssistantView):
"""Get all logged errors and warnings."""

url = "/api/remote_homeassistant/discovery"
name = "api:remote_homeassistant:discovery"

async def get(self, request):
"""Get discovery information."""
hass = request.app["hass"]
return self.json(
{
"uuid": await hass.helpers.instance_id.async_get(),
"location_name": hass.config.location_name,
}
)
55 changes: 18 additions & 37 deletions custom_components/remote_homeassistant/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,25 @@
import logging
from urllib.parse import urlparse

import homeassistant.helpers.config_validation as cv
import voluptuous as vol

from homeassistant import config_entries, core
from homeassistant.helpers.instance_id import async_get
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_HOST,
CONF_PORT,
CONF_VERIFY_SSL,
CONF_ACCESS_TOKEN,
CONF_ENTITY_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_ABOVE,
CONF_BELOW,
)
from homeassistant.const import (CONF_ABOVE, CONF_ACCESS_TOKEN, CONF_BELOW,
CONF_ENTITY_ID, CONF_HOST, CONF_PORT,
CONF_UNIT_OF_MEASUREMENT, CONF_VERIFY_SSL)
from homeassistant.core import callback
from homeassistant.helpers.instance_id import async_get
from homeassistant.util import slugify

from . import async_yaml_to_config_entry
from .rest_api import (
ApiProblem,
CannotConnect,
InvalidAuth,
UnsupportedVersion,
async_get_discovery_info,
)
from .const import (
CONF_REMOTE_CONNECTION,
CONF_SECURE,
CONF_LOAD_COMPONENTS,
CONF_SERVICE_PREFIX,
CONF_SERVICES,
CONF_FILTER,
CONF_SUBSCRIBE_EVENTS,
CONF_ENTITY_PREFIX,
CONF_INCLUDE_DOMAINS,
CONF_INCLUDE_ENTITIES,
CONF_EXCLUDE_DOMAINS,
CONF_EXCLUDE_ENTITIES,
CONF_OPTIONS,
DOMAIN,
) # pylint:disable=unused-import
from .const import (CONF_ENTITY_PREFIX, # pylint:disable=unused-import
CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES, CONF_FILTER,
CONF_INCLUDE_DOMAINS, CONF_INCLUDE_ENTITIES,
CONF_LOAD_COMPONENTS, CONF_OPTIONS, CONF_REMOTE_CONNECTION,
CONF_SECURE, CONF_SERVICE_PREFIX, CONF_SERVICES,
CONF_SUBSCRIBE_EVENTS, DOMAIN)
from .rest_api import (ApiProblem, CannotConnect, EndpointMissing, InvalidAuth,
UnsupportedVersion, async_get_discovery_info)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -100,13 +77,16 @@ async def async_step_user(self, user_input=None):
try:
info = await validate_input(self.hass, user_input)
except ApiProblem:
_LOGGER.exception("test")
errors["base"] = "api_problem"
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except UnsupportedVersion:
errors["base"] = "unsupported_version"
except EndpointMissing:
errors["base"] = "missing_endpoint"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
Expand Down Expand Up @@ -135,6 +115,7 @@ async def async_step_user(self, user_input=None):
async def async_step_zeroconf(self, info):
"""Handle instance discovered via zeroconf."""
properties = info["properties"]
port = info["port"]
uuid = properties["uuid"]

await self.async_set_unique_id(uuid)
Expand All @@ -150,7 +131,7 @@ async def async_step_zeroconf(self, info):

self.prefill = {
CONF_HOST: url.hostname,
CONF_PORT: url.port,
CONF_PORT: port,
CONF_SECURE: url.scheme == "https",
}

Expand Down
4 changes: 2 additions & 2 deletions custom_components/remote_homeassistant/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Remote Home-Assistant",
"issue_tracker": "https://github.com/custom-components/remote_homeassistant/issues",
"documentation": "https://github.com/custom-components/remote_homeassistant",
"dependencies": [],
"dependencies": ["http"],
"config_flow": true,
"codeowners": [
"@lukas-hetzenecker",
Expand All @@ -13,6 +13,6 @@
"zeroconf": [
"_home-assistant._tcp.local."
],
"version": "3.4",
"version": "3.5",
"iot_class": "local_push"
}
2 changes: 1 addition & 1 deletion custom_components/remote_homeassistant/proxy_services.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for proxy services."""
import asyncio
import voluptuous as vol

import voluptuous as vol
from homeassistant.core import SERVICE_CALL_LIMIT
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.service import SERVICE_DESCRIPTION_CACHE
Expand Down
15 changes: 8 additions & 7 deletions custom_components/remote_homeassistant/rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from homeassistant import exceptions
from homeassistant.helpers.aiohttp_client import async_get_clientsession

API_URL = "{proto}://{host}:{port}/api/"
API_URL = "{proto}://{host}:{port}/api/remote_homeassistant/discovery"


class ApiProblem(exceptions.HomeAssistantError):
Expand All @@ -26,6 +26,10 @@ class UnsupportedVersion(exceptions.HomeAssistantError):
"""Error to indicate an unsupported version of Home Assistant."""


class EndpointMissing(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""


async def async_get_discovery_info(hass, host, port, secure, access_token, verify_ssl):
"""Get discovery information from server."""
url = API_URL.format(
Expand All @@ -39,17 +43,14 @@ async def async_get_discovery_info(hass, host, port, secure, access_token, verif
}
session = async_get_clientsession(hass, verify_ssl)

# Try to reach api endpoint solely for verifying auth
# Fetch discovery info location for name and unique UUID
async with session.get(url, headers=headers) as resp:
if resp.status == 404:
raise EndpointMissing()
if 400 <= resp.status < 500:
raise InvalidAuth()
if resp.status != 200:
raise ApiProblem()

# Fetch discovery info location for name and unique UUID
async with session.get(url + "discovery_info") as resp:
if resp.status != 200:
raise ApiProblem()
json = await resp.json()
if not isinstance(json, dict):
raise BadResponse(f"Bad response data: {json}")
Expand Down
4 changes: 2 additions & 2 deletions custom_components/remote_homeassistant/sensor.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Sensor platform for connection status.."""
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity

from .const import CONF_SECURE, CONF_ENTITY_PREFIX
from .const import CONF_ENTITY_PREFIX, CONF_SECURE


async def async_setup_entry(hass, config_entry, async_add_entities):
Expand Down
3 changes: 2 additions & 1 deletion custom_components/remote_homeassistant/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"cannot_connect": "Failed to connect to server",
"invalid_auth": "Invalid credentials",
"unsupported_version": "Unsupported version. At least version 0.111 is required.",
"unknown": "An unknown error occurred"
"unknown": "An unknown error occurred",
"missing_endpoint": "You need to install Remote Home Assistant on this host and add remote_homeassistant: to its configuration."
},
"abort": {
"already_configured": "Already configured"
Expand Down

0 comments on commit f546580

Please sign in to comment.