44
55import voluptuous as vol
66
7+ from homeassistant .config import load_yaml_config_file
8+ from homeassistant .config_entries import SOURCE_IMPORT
79from homeassistant .requirements import async_process_requirements , RequirementsNotFound
810from homeassistant .components .device_tracker import DOMAIN as DT_DOMAIN
9- from homeassistant .const import CONF_PLATFORM
11+ from homeassistant .components .device_tracker .legacy import YAML_DEVICES
12+ from homeassistant .components .persistent_notification import (
13+ async_create as pn_async_create ,
14+ )
15+ from homeassistant .const import CONF_ID , CONF_NAME , CONF_PLATFORM
16+
17+ # Platform class did not exist before 2021.12
18+ try :
19+ from homeassistant .const import Platform
20+
21+ PLATFORMS = [Platform .DEVICE_TRACKER ]
22+ except ImportError :
23+ PLATFORMS = [DT_DOMAIN ]
24+
25+ from homeassistant .exceptions import HomeAssistantError
1026import homeassistant .helpers .config_validation as cv
27+ from homeassistant .util import slugify
1128
12- from .const import CONF_TIME_AS , DOMAIN , TZ_DEVICE_LOCAL , TZ_DEVICE_UTC
29+ from .const import (
30+ CONF_OPTS ,
31+ CONF_TIME_AS ,
32+ CONF_TRACKERS ,
33+ DOMAIN ,
34+ TZ_DEVICE_LOCAL ,
35+ TZ_DEVICE_UTC ,
36+ )
37+ from .device_tracker import COMPOSITE_TRACKER
1338
1439CONF_TZ_FINDER = "tz_finder"
1540DEFAULT_TZ_FINDER = "timezonefinderL==4.0.2"
1641CONF_TZ_FINDER_CLASS = "tz_finder_class"
1742TZ_FINDER_CLASS_OPTS = ["TimezoneFinder" , "TimezoneFinderL" ]
43+ TRACKER = COMPOSITE_TRACKER .copy ()
44+ TRACKER .update ({vol .Required (CONF_NAME ): cv .string , vol .Optional (CONF_ID ): cv .slugify })
45+
46+
47+ def _tracker_ids (value ):
48+ """Determine tracker ID."""
49+ ids = []
50+ for conf in value :
51+ if CONF_ID not in conf :
52+ name = conf [CONF_NAME ]
53+ if name == slugify (name ):
54+ conf [CONF_ID ] = name
55+ conf [CONF_NAME ] = name .replace ("_" , " " ).title ()
56+ else :
57+ conf [CONF_ID ] = cv .slugify (conf [CONF_NAME ])
58+ ids .append (conf [CONF_ID ])
59+ if len (ids ) != len (set (ids )):
60+ raise vol .Invalid ("id's must be unique" )
61+ return value
62+
1863
1964CONFIG_SCHEMA = vol .Schema (
2065 {
2469 vol .Optional (
2570 CONF_TZ_FINDER_CLASS , default = TZ_FINDER_CLASS_OPTS [0 ]
2671 ): vol .In (TZ_FINDER_CLASS_OPTS ),
72+ vol .Optional (CONF_TRACKERS , default = list ): vol .All (
73+ cv .ensure_list , [TRACKER ], _tracker_ids
74+ ),
2775 }
28- ),
76+ )
2977 },
3078 extra = vol .ALLOW_EXTRA ,
3179)
3280
3381_LOGGER = logging .getLogger (__name__ )
3482
3583
36- def setup (hass , config ):
84+ async def async_setup (hass , config ):
85+ # Get a list of all the object IDs in known_devices.yaml to see if any were created
86+ # when this integration was a legacy device tracker, or would otherwise conflict
87+ # with IDs in our config.
88+ try :
89+ legacy_devices = await hass .async_add_executor_job (
90+ load_yaml_config_file , hass .config .path (YAML_DEVICES )
91+ )
92+ except (HomeAssistantError , FileNotFoundError ):
93+ legacy_devices = {}
94+ try :
95+ legacy_ids = [
96+ cv .slugify (id )
97+ for id , dev in legacy_devices .items ()
98+ if cv .boolean (dev .get ("track" , False ))
99+ ]
100+ except vol .Invalid :
101+ legacy_ids = []
102+
103+ # Get all existing composite config entries.
104+ cfg_entries = {
105+ entry .data [CONF_ID ]: entry
106+ for entry in hass .config_entries .async_entries (DOMAIN )
107+ }
108+
109+ # For each tracker config, see if it conflicts with a known_devices.yaml entry.
110+ # If not, update the config entry if one already exists for it in case the config
111+ # has changed, or create a new config entry if one did not already exist.
112+ tracker_configs = config [DOMAIN ][CONF_TRACKERS ]
113+ conflict_ids = []
114+ for conf in tracker_configs :
115+ # These go in the "static" data field.
116+ id = conf [CONF_ID ]
117+ name = conf [CONF_NAME ]
118+ # These go in the options field, which can be updated via the UI (once support
119+ # for that is added.)
120+ options = {k : v for k , v in conf .items () if k in CONF_OPTS }
121+
122+ if id in legacy_ids :
123+ conflict_ids .append (id )
124+ elif id in cfg_entries :
125+ hass .config_entries .async_update_entry (
126+ cfg_entries [id ], data = {CONF_NAME : name , CONF_ID : id }, options = options
127+ )
128+ else :
129+
130+ async def create_config (conf ):
131+ """Create new config entry."""
132+ result = await hass .config_entries .flow .async_init (
133+ DOMAIN , context = {"source" : SOURCE_IMPORT }, data = conf
134+ )
135+ # Versions prior to 2021.6 did not support creating with options, so
136+ # update the created config entry with the options.
137+ hass .config_entries .async_update_entry (
138+ result ["result" ], options = options
139+ )
140+
141+ hass .async_create_task (create_config (conf ))
142+
143+ if conflict_ids :
144+ _LOGGER .warning ("%s in %s: skipping" , ", " .join (conflict_ids ), YAML_DEVICES )
145+ if len (conflict_ids ) == 1 :
146+ msg1 = "ID was"
147+ msg2 = "conflicts"
148+ else :
149+ msg1 = "IDs were"
150+ msg2 = "conflict"
151+ pn_async_create (
152+ hass ,
153+ title = "Conflicting IDs" ,
154+ message = f"The following { msg1 } found in { YAML_DEVICES } "
155+ f" which { msg2 } with the configuration of the { DOMAIN } integration."
156+ " Please remove from one or the other."
157+ f"\n \n { ', ' .join (conflict_ids )} " ,
158+ )
159+
160+ legacy_configs = [
161+ conf for conf in (config .get (DT_DOMAIN ) or []) if conf [CONF_PLATFORM ] == DOMAIN
162+ ]
37163 if any (
38164 conf [CONF_TIME_AS ] in (TZ_DEVICE_UTC , TZ_DEVICE_LOCAL )
39- for conf in (config .get (DT_DOMAIN ) or [])
40- if conf [CONF_PLATFORM ] == DOMAIN
165+ for conf in tracker_configs + legacy_configs
41166 ):
42167 pkg = config [DOMAIN ][CONF_TZ_FINDER ]
43168 try :
44- asyncio .run_coroutine_threadsafe (
45- async_process_requirements (
46- hass , "{}.{}" .format (DOMAIN , DT_DOMAIN ), [pkg ]
47- ),
48- hass .loop ,
49- ).result ()
169+ await async_process_requirements (hass , f"{ DOMAIN } .{ DT_DOMAIN } " , [pkg ])
50170 except RequirementsNotFound :
51171 _LOGGER .debug ("Process requirements failed: %s" , pkg )
52172 return False
@@ -68,3 +188,35 @@ def setup(hass, config):
68188 hass .data [DOMAIN ] = tf
69189
70190 return True
191+
192+
193+ async def async_setup_entry (hass , entry ):
194+ """Set up config entry."""
195+ # async_forward_entry_setups was new in 2022.8
196+ if hasattr (hass .config_entries , "async_forward_entry_setups" ):
197+ await hass .config_entries .async_forward_entry_setups (entry , PLATFORMS )
198+ # async_setup_platforms was new in 2021.5
199+ elif hasattr (hass .config_entries , "async_setup_platforms" ):
200+ await hass .config_entries .async_setup_platforms (entry , PLATFORMS )
201+ else :
202+ for platform in PLATFORMS :
203+ hass .async_create_task (
204+ hass .config_entries .async_forward_entry_setup (entry , platform )
205+ )
206+ return True
207+
208+
209+ async def async_unload_entry (hass , entry ):
210+ """Unload a config entry."""
211+ # async_unload_platforms was new in 2021.5
212+ if hasattr (hass .config_entries , "async_unload_platforms" ):
213+ return await hass .config_entries .async_unload_platforms (entry , PLATFORMS )
214+ else :
215+ return all (
216+ await asyncio .gather (
217+ * (
218+ hass .config_entries .async_forward_entry_unload (entry , platform )
219+ for platform in PLATFORMS
220+ )
221+ )
222+ )
0 commit comments