Skip to content

Commit 2e3e73a

Browse files
authored
Import YAML instances to config entries (#68)
* Import YAML config as config entries * Deprecate api_password option * Add reload service that reloads from YAML
1 parent b9aa3f3 commit 2e3e73a

File tree

4 files changed

+152
-65
lines changed

4 files changed

+152
-65
lines changed

README.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ remote_homeassistant:
6363
secure: true
6464
verify_ssl: false
6565
access_token: !secret access_token
66-
api_password: !secret http_password
6766
entity_prefix: "instance02_"
6867
include:
6968
domains:
@@ -113,11 +112,7 @@ verify_ssl:
113112
type: bool
114113
default: true
115114
access_token:
116-
description: Access token of the remote instance, if set. Mutually exclusive with api_password
117-
required: false
118-
type: string
119-
api_password:
120-
description: DEPRECTAED! API password of the remote instance, if set. Mutually exclusive with access_token
115+
description: Access token of the remote instance, if set.
121116
required: false
122117
type: string
123118
entity_prefix:

custom_components/remote_homeassistant/__init__.py

Lines changed: 127 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,29 @@
1818
from homeassistant.core import HomeAssistant, callback, Context
1919
import homeassistant.components.websocket_api.auth as api
2020
from homeassistant.core import EventOrigin, split_entity_id
21-
from homeassistant.config_entries import ConfigEntry
21+
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
2222
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
2323
from homeassistant.const import (CONF_HOST, CONF_PORT, EVENT_CALL_SERVICE,
2424
EVENT_HOMEASSISTANT_STOP,
2525
EVENT_STATE_CHANGED, EVENT_SERVICE_REGISTERED,
2626
CONF_EXCLUDE, CONF_ENTITIES, CONF_ENTITY_ID,
2727
CONF_DOMAINS, CONF_INCLUDE, CONF_UNIT_OF_MEASUREMENT,
28-
CONF_ABOVE, CONF_BELOW, CONF_VERIFY_SSL, CONF_ACCESS_TOKEN)
28+
CONF_ABOVE, CONF_BELOW, CONF_VERIFY_SSL, CONF_ACCESS_TOKEN,
29+
SERVICE_RELOAD)
2930
from homeassistant.config import DATA_CUSTOMIZE
3031
from homeassistant.helpers.aiohttp_client import async_get_clientsession
32+
from homeassistant.helpers.reload import async_integration_yaml_config
3133
import homeassistant.helpers.config_validation as cv
3234

3335
from .const import (CONF_REMOTE_CONNECTION, CONF_UNSUB_LISTENER, CONF_INCLUDE_DOMAINS,
34-
CONF_INCLUDE_ENTITIES, CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES, DOMAIN)
36+
CONF_INCLUDE_ENTITIES, CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES,
37+
CONF_OPTIONS, DOMAIN)
3538
from .rest_api import async_get_discovery_info
3639

3740
_LOGGER = logging.getLogger(__name__)
3841

3942
CONF_INSTANCES = 'instances'
4043
CONF_SECURE = 'secure'
41-
CONF_API_PASSWORD = 'api_password'
4244
CONF_SUBSCRIBE_EVENTS = 'subscribe_events'
4345
CONF_ENTITY_PREFIX = 'entity_prefix'
4446
CONF_FILTER = 'filter'
@@ -60,8 +62,7 @@
6062
vol.Optional(CONF_PORT, default=8123): cv.port,
6163
vol.Optional(CONF_SECURE, default=False): cv.boolean,
6264
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
63-
vol.Exclusive(CONF_ACCESS_TOKEN, 'auth'): cv.string,
64-
vol.Exclusive(CONF_API_PASSWORD, 'auth'): cv.string,
65+
vol.Required(CONF_ACCESS_TOKEN): cv.string,
6566
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema(
6667
{
6768
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
@@ -104,31 +105,98 @@
104105
}, extra=vol.ALLOW_EXTRA)
105106

106107

108+
def async_yaml_to_config_entry(instance_conf):
109+
"""Convert YAML config into data and options used by a config entry."""
110+
conf = instance_conf.copy()
111+
options = {}
112+
113+
if CONF_INCLUDE in conf:
114+
include = conf.pop(CONF_INCLUDE)
115+
if CONF_ENTITIES in include:
116+
options[CONF_INCLUDE_ENTITIES] = include[CONF_ENTITIES]
117+
if CONF_DOMAINS in include:
118+
options[CONF_INCLUDE_DOMAINS] = include[CONF_DOMAINS]
119+
120+
if CONF_EXCLUDE in conf:
121+
exclude = conf.pop(CONF_EXCLUDE)
122+
if CONF_ENTITIES in exclude:
123+
options[CONF_EXCLUDE_ENTITIES] = exclude[CONF_ENTITIES]
124+
if CONF_DOMAINS in exclude:
125+
options[CONF_EXCLUDE_DOMAINS] = exclude[CONF_DOMAINS]
126+
127+
if CONF_FILTER in conf:
128+
options[CONF_FILTER] = conf.pop(CONF_FILTER)
129+
130+
if CONF_SUBSCRIBE_EVENTS in conf:
131+
options[CONF_SUBSCRIBE_EVENTS] = conf.pop(CONF_SUBSCRIBE_EVENTS)
132+
133+
if CONF_ENTITY_PREFIX in conf:
134+
options[CONF_ENTITY_PREFIX] = conf.pop(CONF_ENTITY_PREFIX)
135+
136+
return conf, options
137+
138+
139+
async def _async_update_config_entry_if_from_yaml(hass, entries_by_id, conf):
140+
"""Update a config entry with the latest yaml."""
141+
try:
142+
info = await async_get_discovery_info(
143+
hass,
144+
conf[CONF_HOST],
145+
conf[CONF_PORT],
146+
conf[CONF_SECURE],
147+
conf[CONF_ACCESS_TOKEN],
148+
conf[CONF_VERIFY_SSL])
149+
except Exception:
150+
_LOGGER.exception(f"reload of {conf[CONF_HOST]} failed")
151+
else:
152+
entry = entries_by_id.get(info["uuid"])
153+
if entry:
154+
data, options = async_yaml_to_config_entry(conf)
155+
hass.config_entries.async_update_entry(entry, data=data, options=options)
156+
157+
107158
async def async_setup(hass: HomeAssistantType, config: ConfigType):
108159
"""Set up the remote_homeassistant component."""
109160
hass.data.setdefault(DOMAIN, {})
110-
if DOMAIN in config:
111-
for instance in config[DOMAIN].get(CONF_INSTANCES, []):
112-
connection = RemoteConnection(hass, instance)
113-
asyncio.ensure_future(connection.async_connect())
161+
162+
async def _handle_reload(service):
163+
"""Handle reload service call."""
164+
config = await async_integration_yaml_config(hass, DOMAIN)
165+
166+
if not config or DOMAIN not in config:
167+
return
168+
169+
current_entries = hass.config_entries.async_entries(DOMAIN)
170+
entries_by_id = {entry.unique_id: entry for entry in current_entries}
171+
172+
instances = config[DOMAIN][CONF_INSTANCES]
173+
update_tasks = [
174+
_async_update_config_entry_if_from_yaml(hass, entries_by_id, instance)
175+
for instance in instances
176+
]
177+
178+
await asyncio.gather(*update_tasks)
179+
180+
hass.helpers.service.async_register_admin_service(
181+
DOMAIN,
182+
SERVICE_RELOAD,
183+
_handle_reload,
184+
)
185+
186+
instances = config.get(DOMAIN, {}).get(CONF_INSTANCES, [])
187+
for instance in instances:
188+
hass.async_create_task(
189+
hass.config_entries.flow.async_init(
190+
DOMAIN, context={"source": SOURCE_IMPORT}, data=instance
191+
)
192+
)
114193
return True
115194

116195

117196
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
118197
"""Set up Remote Home-Assistant from a config entry."""
119-
# TODO: Temporary work-around until YAML import has been implemented
120-
conf = entry.data.copy()
121-
conf.update(entry.options)
122-
conf[CONF_INCLUDE] = {
123-
CONF_ENTITIES: conf.get(CONF_INCLUDE_ENTITIES, []),
124-
CONF_DOMAINS: conf.get(CONF_INCLUDE_DOMAINS, [])
125-
}
126-
conf[CONF_EXCLUDE] = {
127-
CONF_ENTITIES: conf.get(CONF_EXCLUDE_ENTITIES, []),
128-
CONF_DOMAINS: conf.get(CONF_EXCLUDE_DOMAINS, [])
129-
}
130-
131-
remote = RemoteConnection(hass, conf, entry.unique_id)
198+
_async_import_options_from_yaml(hass, entry)
199+
remote = RemoteConnection(hass, entry)
132200

133201
hass.data[DOMAIN][entry.entry_id] = {
134202
CONF_REMOTE_CONNECTION: remote,
@@ -147,6 +215,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
147215
return True
148216

149217

218+
@callback
219+
def _async_import_options_from_yaml(hass: HomeAssistant, entry: ConfigEntry):
220+
"""Import options from YAML into options section of config entry."""
221+
if CONF_OPTIONS in entry.data:
222+
data = entry.data.copy()
223+
options = data.pop(CONF_OPTIONS)
224+
hass.config_entries.async_update_entry(entry, data=data, options=options)
225+
226+
150227
async def _update_listener(hass, config_entry):
151228
"""Update listener."""
152229
await hass.config_entries.async_reload(config_entry.entry_id)
@@ -155,25 +232,20 @@ async def _update_listener(hass, config_entry):
155232
class RemoteConnection(object):
156233
"""A Websocket connection to a remote home-assistant instance."""
157234

158-
def __init__(self, hass, conf, instance_uuid=None):
235+
def __init__(self, hass, config_entry):
159236
"""Initialize the connection."""
160237
self._hass = hass
161-
self._host = conf.get(CONF_HOST)
162-
self._port = conf.get(CONF_PORT)
163-
self._secure = conf.get(CONF_SECURE)
164-
self._verify_ssl = conf.get(CONF_VERIFY_SSL)
165-
self._access_token = conf.get(CONF_ACCESS_TOKEN)
166-
self._password = conf.get(CONF_API_PASSWORD)
167-
self._instance_uuid = instance_uuid
238+
self._entry = config_entry
239+
self._secure = config_entry.data.get(CONF_SECURE, False)
240+
self._verify_ssl = config_entry.data.get(CONF_VERIFY_SSL, False)
241+
self._access_token = config_entry.data.get(CONF_ACCESS_TOKEN)
168242

169243
# see homeassistant/components/influxdb/__init__.py
170244
# for include/exclude logic
171-
include = conf.get(CONF_INCLUDE, {})
172-
exclude = conf.get(CONF_EXCLUDE, {})
173-
self._whitelist_e = set(include.get(CONF_ENTITIES, []))
174-
self._whitelist_d = set(include.get(CONF_DOMAINS, []))
175-
self._blacklist_e = set(exclude.get(CONF_ENTITIES, []))
176-
self._blacklist_d = set(exclude.get(CONF_DOMAINS, []))
245+
self._whitelist_e = set(config_entry.options.get(CONF_INCLUDE_ENTITIES, []))
246+
self._whitelist_d = set(config_entry.options.get(CONF_INCLUDE_DOMAINS, []))
247+
self._blacklist_e = set(config_entry.options.get(CONF_EXCLUDE_ENTITIES, []))
248+
self._blacklist_d = set(config_entry.options.get(CONF_EXCLUDE_DOMAINS, []))
177249

178250
self._filter = [
179251
{
@@ -182,18 +254,18 @@ def __init__(self, hass, conf, instance_uuid=None):
182254
CONF_ABOVE: f.get(CONF_ABOVE),
183255
CONF_BELOW: f.get(CONF_BELOW)
184256
}
185-
for f in conf.get(CONF_FILTER, [])
257+
for f in config_entry.options.get(CONF_FILTER, [])
186258
]
187259

188-
self._subscribe_events = conf.get(CONF_SUBSCRIBE_EVENTS, [])
189-
self._entity_prefix = conf.get(CONF_ENTITY_PREFIX, "")
260+
self._subscribe_events = config_entry.options.get(CONF_SUBSCRIBE_EVENTS, [])
261+
self._entity_prefix = config_entry.options.get(CONF_ENTITY_PREFIX, "")
190262

191263
self._connection_state_entity = 'sensor.'
192264

193265
if self._entity_prefix != '':
194266
self._connection_state_entity = '{}{}'.format(self._connection_state_entity, self._entity_prefix)
195267

196-
self._connection_state_entity = '{}remote_connection_{}_{}'.format(self._connection_state_entity, self._host.replace('.', '_').replace('-', '_'), self._port)
268+
self._connection_state_entity = '{}remote_connection_{}_{}'.format(self._connection_state_entity, self._entry.data[CONF_HOST].replace('.', '_').replace('-', '_'), self._entry.data[CONF_PORT])
197269

198270
self._connection = None
199271
self._is_stopping = False
@@ -203,11 +275,12 @@ def __init__(self, hass, conf, instance_uuid=None):
203275
self._remove_listener = None
204276

205277
self._instance_attrs = {
206-
'host': self._host,
207-
'port': self._port,
278+
'host': self._entry.data[CONF_HOST],
279+
'port': self._entry.data[CONF_PORT],
208280
'secure': self._secure,
209281
'verify_ssl': self._verify_ssl,
210-
'entity_prefix': self._entity_prefix
282+
'entity_prefix': self._entity_prefix,
283+
'uuid': config_entry.unique_id,
211284
}
212285

213286
self._entities.add(self._connection_state_entity)
@@ -232,7 +305,7 @@ def set_connection_state(self, state):
232305
def _get_url(self):
233306
"""Get url to connect to."""
234307
return '%s://%s:%s/api/websocket' % (
235-
'wss' if self._secure else 'ws', self._host, self._port)
308+
'wss' if self._secure else 'ws', self._entry.data[CONF_HOST], self._entry.data[CONF_PORT])
236309

237310
async def async_connect(self):
238311
"""Connect to remote home-assistant websocket..."""
@@ -242,23 +315,20 @@ async def _async_stop_handler(event):
242315

243316
async def _async_instance_id_match():
244317
"""Verify if remote instance id matches the expected id."""
245-
if not self._instance_uuid:
246-
return True
247-
248318
try:
249319
info = await async_get_discovery_info(
250320
self._hass,
251-
self._host,
252-
self._port,
321+
self._entry.data[CONF_HOST],
322+
self._entry.data[CONF_PORT],
253323
self._secure,
254324
self._access_token,
255325
self._verify_ssl)
256326
except Exception:
257327
_LOGGER.exception("failed to verify instance id")
258328
return False
259329
else:
260-
if info["uuid"] != self._instance_uuid:
261-
_LOGGER.error("instance id not matching: %s != %s", info["uuid"], self._instance_uuid)
330+
if info["uuid"] != self._entry.unique_id:
331+
_LOGGER.error("instance id not matching: %s != %s", info["uuid"], self._entry.unique_id)
262332
return False
263333
return True
264334

@@ -277,7 +347,7 @@ async def _async_instance_id_match():
277347
try:
278348
_LOGGER.info('Connecting to %s', url)
279349
self._connection = await session.ws_connect(url)
280-
except aiohttp.client_exceptions.ClientError as err:
350+
except aiohttp.client_exceptions.ClientError:
281351
_LOGGER.error(
282352
'Could not connect to %s, retry in 10 seconds...', url)
283353
self.set_connection_state(STATE_RECONNECTING)
@@ -362,22 +432,20 @@ async def _recv(self):
362432
await self._init()
363433

364434
elif message['type'] == api.TYPE_AUTH_REQUIRED:
365-
if not (self._access_token or self._password):
366-
_LOGGER.error('Access token or api password required, but not provided')
367-
self.set_connection_state(STATE_AUTH_REQUIRED)
368-
return
369435
if self._access_token:
370436
data = {'type': api.TYPE_AUTH, 'access_token': self._access_token}
371437
else:
372-
data = {'type': api.TYPE_AUTH, 'api_password': self._password}
438+
_LOGGER.error('Access token required, but not provided')
439+
self.set_connection_state(STATE_AUTH_REQUIRED)
440+
return
373441
try:
374442
await self._connection.send_json(data)
375443
except Exception as err:
376444
_LOGGER.error('could not send data to remote connection: %s', err)
377445
break
378446

379447
elif message['type'] == api.TYPE_AUTH_INVALID:
380-
_LOGGER.error('Auth invalid, check your access token or API password')
448+
_LOGGER.error('Auth invalid, check your access token')
381449
self.set_connection_state(STATE_AUTH_INVALID)
382450
await self._connection.close()
383451
return

custom_components/remote_homeassistant/config_flow.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919
CONF_EXCLUDE,
2020
CONF_ABOVE,
2121
CONF_BELOW,
22+
CONF_ENTITIES,
23+
CONF_DOMAINS,
2224
)
2325
from homeassistant.core import callback
2426

27+
from . import async_yaml_to_config_entry
2528
from .rest_api import ApiProblem, CannotConnect, InvalidAuth, async_get_discovery_info
2629
from .const import (
2730
CONF_REMOTE_CONNECTION,
@@ -33,6 +36,7 @@
3336
CONF_INCLUDE_ENTITIES,
3437
CONF_EXCLUDE_DOMAINS,
3538
CONF_EXCLUDE_ENTITIES,
39+
CONF_OPTIONS,
3640
DOMAIN,
3741
) # pylint:disable=unused-import
3842

@@ -148,6 +152,25 @@ async def async_step_zeroconf(self, info):
148152
self.context["title_placeholders"] = {"name": properties["location_name"]}
149153
return await self.async_step_user()
150154

155+
async def async_step_import(self, user_input):
156+
"""Handle import from YAML."""
157+
try:
158+
info = await validate_input(self.hass, user_input)
159+
except Exception:
160+
_LOGGER.exception(f"import of {user_input[CONF_HOST]} failed")
161+
return self.async_abort(reason="import_failed")
162+
163+
conf, options = async_yaml_to_config_entry(user_input)
164+
165+
# Options cannot be set here, so store them in a special key and import them
166+
# before setting up an entry
167+
conf[CONF_OPTIONS] = options
168+
169+
await self.async_set_unique_id(info["uuid"])
170+
self._abort_if_unique_id_configured(updates=conf)
171+
172+
return self.async_create_entry(title=f"{info['title']} (YAML)", data=conf)
173+
151174

152175
class OptionsFlowHandler(config_entries.OptionsFlow):
153176
"""Handle options flow for the Home Assistant remote integration."""

custom_components/remote_homeassistant/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
CONF_REMOTE_CONNECTION = "remote_connection"
44
CONF_UNSUB_LISTENER = "unsub_listener"
5+
CONF_OPTIONS = "options"
56

67
CONF_FILTER = "filter"
78
CONF_SECURE = "secure"

0 commit comments

Comments
 (0)