@@ -92,11 +92,15 @@ async def async_update_options(hass: HomeAssistant, entry: OllamaConfigEntry) ->
9292async def async_migrate_integration (hass : HomeAssistant ) -> None :
9393 """Migrate integration entry structure."""
9494
95- entries = hass .config_entries .async_entries (DOMAIN )
95+ # Make sure we get enabled config entries first
96+ entries = sorted (
97+ hass .config_entries .async_entries (DOMAIN ),
98+ key = lambda e : e .disabled_by is not None ,
99+ )
96100 if not any (entry .version == 1 for entry in entries ):
97101 return
98102
99- api_keys_entries : dict [str , ConfigEntry ] = {}
103+ url_entries : dict [str , tuple [ ConfigEntry , bool ] ] = {}
100104 entity_registry = er .async_get (hass )
101105 device_registry = dr .async_get (hass )
102106
@@ -112,33 +116,64 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
112116 title = entry .title ,
113117 unique_id = None ,
114118 )
115- if entry .data [CONF_URL ] not in api_keys_entries :
119+ if entry .data [CONF_URL ] not in url_entries :
116120 use_existing = True
117- api_keys_entries [entry .data [CONF_URL ]] = entry
121+ all_disabled = all (
122+ e .disabled_by is not None
123+ for e in entries
124+ if e .data [CONF_URL ] == entry .data [CONF_URL ]
125+ )
126+ url_entries [entry .data [CONF_URL ]] = (entry , all_disabled )
118127
119- parent_entry = api_keys_entries [entry .data [CONF_URL ]]
128+ parent_entry , all_disabled = url_entries [entry .data [CONF_URL ]]
120129
121130 hass .config_entries .async_add_subentry (parent_entry , subentry )
122131
123- conversation_entity = entity_registry .async_get_entity_id (
132+ conversation_entity_id = entity_registry .async_get_entity_id (
124133 "conversation" ,
125134 DOMAIN ,
126135 entry .entry_id ,
127136 )
128- if conversation_entity is not None :
137+ device = device_registry .async_get_device (
138+ identifiers = {(DOMAIN , entry .entry_id )}
139+ )
140+
141+ if conversation_entity_id is not None :
142+ conversation_entity_entry = entity_registry .entities [conversation_entity_id ]
143+ entity_disabled_by = conversation_entity_entry .disabled_by
144+ if (
145+ entity_disabled_by is er .RegistryEntryDisabler .CONFIG_ENTRY
146+ and not all_disabled
147+ ):
148+ # Device and entity registries don't update the disabled_by flag
149+ # when moving a device or entity from one config entry to another,
150+ # so we need to do it manually.
151+ entity_disabled_by = (
152+ er .RegistryEntryDisabler .DEVICE
153+ if device
154+ else er .RegistryEntryDisabler .USER
155+ )
129156 entity_registry .async_update_entity (
130- conversation_entity ,
157+ conversation_entity_id ,
131158 config_entry_id = parent_entry .entry_id ,
132159 config_subentry_id = subentry .subentry_id ,
160+ disabled_by = entity_disabled_by ,
133161 new_unique_id = subentry .subentry_id ,
134162 )
135163
136- device = device_registry .async_get_device (
137- identifiers = {(DOMAIN , entry .entry_id )}
138- )
139164 if device is not None :
165+ # Device and entity registries don't update the disabled_by flag when
166+ # moving a device or entity from one config entry to another, so we
167+ # need to do it manually.
168+ device_disabled_by = device .disabled_by
169+ if (
170+ device .disabled_by is dr .DeviceEntryDisabler .CONFIG_ENTRY
171+ and not all_disabled
172+ ):
173+ device_disabled_by = dr .DeviceEntryDisabler .USER
140174 device_registry .async_update_device (
141175 device .id ,
176+ disabled_by = device_disabled_by ,
142177 new_identifiers = {(DOMAIN , subentry .subentry_id )},
143178 add_config_subentry_id = subentry .subentry_id ,
144179 add_config_entry_id = parent_entry .entry_id ,
@@ -158,14 +193,15 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
158193 if not use_existing :
159194 await hass .config_entries .async_remove (entry .entry_id )
160195 else :
196+ _add_ai_task_subentry (hass , entry )
161197 hass .config_entries .async_update_entry (
162198 entry ,
163199 title = DEFAULT_NAME ,
164200 # Update parent entry to only keep URL, remove model
165201 data = {CONF_URL : entry .data [CONF_URL ]},
166202 options = {},
167203 version = 3 ,
168- minor_version = 1 ,
204+ minor_version = 3 ,
169205 )
170206
171207
@@ -211,32 +247,69 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OllamaConfigEntry) ->
211247 )
212248
213249 if entry .version == 3 and entry .minor_version == 1 :
214- # Add AI Task subentry with default options. We can only create a new
215- # subentry if we can find an existing model in the entry. The model
216- # was removed in the previous migration step, so we need to
217- # check the subentries for an existing model.
218- existing_model = next (
219- iter (
220- model
221- for subentry in entry .subentries .values ()
222- if (model := subentry .data .get (CONF_MODEL )) is not None
223- ),
224- None ,
225- )
226- if existing_model :
227- hass .config_entries .async_add_subentry (
228- entry ,
229- ConfigSubentry (
230- data = MappingProxyType ({CONF_MODEL : existing_model }),
231- subentry_type = "ai_task_data" ,
232- title = DEFAULT_AI_TASK_NAME ,
233- unique_id = None ,
234- ),
235- )
250+ _add_ai_task_subentry (hass , entry )
236251 hass .config_entries .async_update_entry (entry , minor_version = 2 )
237252
253+ if entry .version == 3 and entry .minor_version == 2 :
254+ # Fix migration where the disabled_by flag was not set correctly.
255+ # We can currently only correct this for enabled config entries,
256+ # because migration does not run for disabled config entries. This
257+ # is asserted in tests, and if that behavior is changed, we should
258+ # correct also disabled config entries.
259+ device_registry = dr .async_get (hass )
260+ entity_registry = er .async_get (hass )
261+ devices = dr .async_entries_for_config_entry (device_registry , entry .entry_id )
262+ entity_entries = er .async_entries_for_config_entry (
263+ entity_registry , entry .entry_id
264+ )
265+ if entry .disabled_by is None :
266+ # If the config entry is not disabled, we need to set the disabled_by
267+ # flag on devices to USER, and on entities to DEVICE, if they are set
268+ # to CONFIG_ENTRY.
269+ for device in devices :
270+ if device .disabled_by is not dr .DeviceEntryDisabler .CONFIG_ENTRY :
271+ continue
272+ device_registry .async_update_device (
273+ device .id ,
274+ disabled_by = dr .DeviceEntryDisabler .USER ,
275+ )
276+ for entity in entity_entries :
277+ if entity .disabled_by is not er .RegistryEntryDisabler .CONFIG_ENTRY :
278+ continue
279+ entity_registry .async_update_entity (
280+ entity .entity_id ,
281+ disabled_by = er .RegistryEntryDisabler .DEVICE ,
282+ )
283+ hass .config_entries .async_update_entry (entry , minor_version = 3 )
284+
238285 _LOGGER .debug (
239286 "Migration to version %s:%s successful" , entry .version , entry .minor_version
240287 )
241288
242289 return True
290+
291+
292+ def _add_ai_task_subentry (hass : HomeAssistant , entry : OllamaConfigEntry ) -> None :
293+ """Add AI Task subentry to the config entry."""
294+ # Add AI Task subentry with default options. We can only create a new
295+ # subentry if we can find an existing model in the entry. The model
296+ # was removed in the previous migration step, so we need to
297+ # check the subentries for an existing model.
298+ existing_model = next (
299+ iter (
300+ model
301+ for subentry in entry .subentries .values ()
302+ if (model := subentry .data .get (CONF_MODEL )) is not None
303+ ),
304+ None ,
305+ )
306+ if existing_model :
307+ hass .config_entries .async_add_subentry (
308+ entry ,
309+ ConfigSubentry (
310+ data = MappingProxyType ({CONF_MODEL : existing_model }),
311+ subentry_type = "ai_task_data" ,
312+ title = DEFAULT_AI_TASK_NAME ,
313+ unique_id = None ,
314+ ),
315+ )
0 commit comments