Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 104 additions & 14 deletions homeassistant/components/youtube/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
"""Support for YouTube."""

from types import MappingProxyType

from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
)

from .api import AsyncConfigEntryAuth
from .const import (
ATTR_TITLE,
CONF_CHANNEL_ID,
CONF_CHANNELS,
DOMAIN,
SUBENTRY_TYPE_CHANNEL,
)
from .coordinator import YouTubeConfigEntry, YouTubeDataUpdateCoordinator

PLATFORMS = [Platform.SENSOR]
Expand All @@ -20,13 +30,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: YouTubeConfigEntry) -> b
session = OAuth2Session(hass, entry, implementation)
auth = AsyncConfigEntryAuth(hass, session)
await auth.check_and_refresh_token()
coordinator = YouTubeDataUpdateCoordinator(hass, entry, auth)

coordinator = YouTubeDataUpdateCoordinator(hass, entry, auth)
await coordinator.async_config_entry_first_refresh()

await delete_devices(hass, entry, coordinator)
data = coordinator.data
for subentry in entry.get_subentries_of_type(SUBENTRY_TYPE_CHANNEL):
channel = data.get(subentry.data[CONF_CHANNEL_ID])
if channel is not None and (title := channel[ATTR_TITLE]) != subentry.title:
hass.config_entries.async_update_subentry(entry, subentry, title=title)

entry.runtime_data = coordinator
entry.async_on_unload(entry.add_update_listener(async_update_listener))
Comment thread
Hugo1380 marked this conversation as resolved.
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True
Expand All @@ -37,15 +52,90 @@ async def async_unload_entry(hass: HomeAssistant, entry: YouTubeConfigEntry) ->
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


async def delete_devices(
hass: HomeAssistant,
entry: YouTubeConfigEntry,
coordinator: YouTubeDataUpdateCoordinator,
) -> None:
"""Delete all devices created by integration."""
channel_ids = list(coordinator.data)
async def async_update_listener(hass: HomeAssistant, entry: YouTubeConfigEntry) -> None:
"""Reload the config entry when channels are added or removed."""
subentry_ids = {
subentry.subentry_id
for subentry in entry.get_subentries_of_type(SUBENTRY_TYPE_CHANNEL)
}
if subentry_ids == entry.runtime_data.subentry_ids:
# Token refreshes update the entry data only; the coordinator
# uses the refreshed token without needing a reload.
return
await hass.config_entries.async_reload(entry.entry_id)


async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entries to the subentry structure."""
Comment thread
Hugo1380 marked this conversation as resolved.
device_registry = dr.async_get(hass)
dev_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
for dev_entry in dev_entries:
if any(identifier[1] in channel_ids for identifier in dev_entry.identifiers):
device_registry.async_remove_device(dev_entry.id)
entity_registry = er.async_get(hass)
prefix = f"{entry.entry_id}_"
channel_ids = dict.fromkeys(entry.options.get(CONF_CHANNELS, []))

subentries: dict[str, ConfigSubentry] = {}
for channel_id in channel_ids:
device = device_registry.async_get_device_by_identifier(
(DOMAIN, f"{prefix}{channel_id}"), entry.entry_id
)
title = channel_id
if device is not None and device.name is not None:
# Prefer the channel name of the existing device so the title
# survives even if the channel can no longer be fetched.
title = device.name
subentry = ConfigSubentry(
data=MappingProxyType({CONF_CHANNEL_ID: channel_id}),
subentry_type=SUBENTRY_TYPE_CHANNEL,
title=title,
unique_id=channel_id,
)
hass.config_entries.async_add_subentry(entry, subentry)
subentries[channel_id] = subentry

# Attach the entities of tracked channels to their subentry and remove
# entities left behind by channels which are no longer tracked.
channel_prefixes = {
f"{prefix}{channel_id}_": channel_id for channel_id in channel_ids
}
for entity_entry in er.async_entries_for_config_entry(
entity_registry, entry.entry_id
):
channel_subentry = next(
(
subentries[channel_id]
for channel_prefix, channel_id in channel_prefixes.items()
if entity_entry.unique_id.startswith(channel_prefix)
),
None,
)
if channel_subentry is None:
entity_registry.async_remove(entity_entry.entity_id)
else:
entity_registry.async_update_entity(
entity_entry.entity_id,
config_subentry_id=channel_subentry.subentry_id,
)

# Move the devices of tracked channels to their subentry and remove
# devices left behind by channels which are no longer tracked.
for device_entry in dr.async_entries_for_config_entry(
device_registry, entry.entry_id
):
channel_id = next(
(
identifier[1].removeprefix(prefix)
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN and identifier[1].startswith(prefix)
),
None,
)
if channel_id is not None and channel_id in subentries:
device_registry.async_update_device(
device_entry.id,
new_identifiers={(DOMAIN, channel_id)},
new_config_subentry_id=subentries[channel_id].subentry_id,
)
else:
device_registry.async_remove_device(device_entry.id)

hass.config_entries.async_update_entry(entry, version=2, options={})
return True
Loading
Loading