1818from homeassistant .core import HomeAssistant , callback , Context
1919import homeassistant .components .websocket_api .auth as api
2020from homeassistant .core import EventOrigin , split_entity_id
21- from homeassistant .config_entries import ConfigEntry
21+ from homeassistant .config_entries import SOURCE_IMPORT , ConfigEntry
2222from homeassistant .helpers .typing import HomeAssistantType , ConfigType
2323from 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 )
2930from homeassistant .config import DATA_CUSTOMIZE
3031from homeassistant .helpers .aiohttp_client import async_get_clientsession
32+ from homeassistant .helpers .reload import async_integration_yaml_config
3133import homeassistant .helpers .config_validation as cv
3234
3335from .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 )
3538from .rest_api import async_get_discovery_info
3639
3740_LOGGER = logging .getLogger (__name__ )
3841
3942CONF_INSTANCES = 'instances'
4043CONF_SECURE = 'secure'
41- CONF_API_PASSWORD = 'api_password'
4244CONF_SUBSCRIBE_EVENTS = 'subscribe_events'
4345CONF_ENTITY_PREFIX = 'entity_prefix'
4446CONF_FILTER = 'filter'
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 ,
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+
107158async 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
117196async 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+
150227async 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):
155232class 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
0 commit comments