"""The Minecraft Server integration.""" from __future__ import annotations import logging from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ADDRESS, CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE, Platform, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryError import homeassistant.helpers.device_registry as dr import homeassistant.helpers.entity_registry as er from .api import MinecraftServer, MinecraftServerAddressError, MinecraftServerType from .const import DOMAIN, KEY_LATENCY, KEY_MOTD from .coordinator import MinecraftServerCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Minecraft Server from a config entry.""" # Create API instance. api = MinecraftServer( hass, entry.data.get(CONF_TYPE, MinecraftServerType.JAVA_EDITION), entry.data[CONF_ADDRESS], ) # Initialize API instance. try: await api.async_initialize() except MinecraftServerAddressError as error: raise ConfigEntryError( f"Server address in configuration entry is invalid: {error}" ) from error # Create coordinator instance. coordinator = MinecraftServerCoordinator(hass, entry.data[CONF_NAME], api) await coordinator.async_config_entry_first_refresh() # Store coordinator instance. domain_data = hass.data.setdefault(DOMAIN, {}) domain_data[entry.entry_id] = coordinator # Set up platforms. await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Minecraft Server config entry.""" # Unload platforms. unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS ) # Clean up. hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old config entry to a new format.""" # 1 --> 2: Use config entry ID as base for unique IDs. if config_entry.version == 1: _LOGGER.debug("Migrating from version 1") old_unique_id = config_entry.unique_id assert old_unique_id config_entry_id = config_entry.entry_id # Migrate config entry. _LOGGER.debug("Migrating config entry. Resetting unique ID: %s", old_unique_id) config_entry.unique_id = None config_entry.version = 2 hass.config_entries.async_update_entry(config_entry) # Migrate device. await _async_migrate_device_identifiers(hass, config_entry, old_unique_id) # Migrate entities. await er.async_migrate_entries(hass, config_entry_id, _migrate_entity_unique_id) _LOGGER.debug("Migration to version 2 successful") # 2 --> 3: Use address instead of host and port in config entry. if config_entry.version == 2: _LOGGER.debug("Migrating from version 2") config_data = config_entry.data # Migrate config entry. address = config_data[CONF_HOST] api = MinecraftServer(hass, MinecraftServerType.JAVA_EDITION, address) try: await api.async_initialize() host_only_lookup_success = True except MinecraftServerAddressError as error: host_only_lookup_success = False _LOGGER.debug( "Hostname (without port) cannot be parsed, trying again with port: %s", error, ) if not host_only_lookup_success: address = f"{config_data[CONF_HOST]}:{config_data[CONF_PORT]}" api = MinecraftServer(hass, MinecraftServerType.JAVA_EDITION, address) try: await api.async_initialize() except MinecraftServerAddressError as error: _LOGGER.exception( "Can't migrate configuration entry due to error while parsing server address, try again later: %s", error, ) return False _LOGGER.debug( "Migrating config entry, replacing host '%s' and port '%s' with address '%s'", config_data[CONF_HOST], config_data[CONF_PORT], address, ) new_data = config_data.copy() new_data[CONF_ADDRESS] = address del new_data[CONF_HOST] del new_data[CONF_PORT] config_entry.version = 3 hass.config_entries.async_update_entry(config_entry, data=new_data) _LOGGER.debug("Migration to version 3 successful") return True async def _async_migrate_device_identifiers( hass: HomeAssistant, config_entry: ConfigEntry, old_unique_id: str | None ) -> None: """Migrate the device identifiers to the new format.""" device_registry = dr.async_get(hass) device_entry_found = False for device_entry in dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ): for identifier in device_entry.identifiers: if identifier[1] == old_unique_id: # Device found in registry. Update identifiers. new_identifiers = { ( DOMAIN, config_entry.entry_id, ) } _LOGGER.debug( "Migrating device identifiers from %s to %s", device_entry.identifiers, new_identifiers, ) device_registry.async_update_device( device_id=device_entry.id, new_identifiers=new_identifiers ) # Device entry found. Leave inner for loop. device_entry_found = True break # Leave outer for loop if device entry is already found. if device_entry_found: break @callback def _migrate_entity_unique_id(entity_entry: er.RegistryEntry) -> dict[str, Any]: """Migrate the unique ID of an entity to the new format.""" # Different variants of unique IDs are available in version 1: # 1) SRV record: '-srv-' # 2) Host & port: '--' # 3) IP address & port: '--' unique_id_pieces = entity_entry.unique_id.split("-") entity_type = unique_id_pieces[2] # Handle bug in version 1: Entity type names were used instead of # keys (e.g. "Protocol Version" instead of "protocol_version"). new_entity_type = entity_type.lower() new_entity_type = new_entity_type.replace(" ", "_") # Special case 'MOTD': Name and key differs. if new_entity_type == "world_message": new_entity_type = KEY_MOTD # Special case 'latency_time': Renamed to 'latency'. if new_entity_type == "latency_time": new_entity_type = KEY_LATENCY new_unique_id = f"{entity_entry.config_entry_id}-{new_entity_type}" _LOGGER.debug( "Migrating entity unique ID from %s to %s", entity_entry.unique_id, new_unique_id, ) return {"new_unique_id": new_unique_id}