ha-core/homeassistant/components/minecraft_server/__init__.py

220 lines
7.2 KiB
Python

"""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: '<host>-srv-<entity_type>'
# 2) Host & port: '<host>-<port>-<entity_type>'
# 3) IP address & port: '<mac_address>-<port>-<entity_type>'
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}