Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Minecraft Server integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 import dns.rdata
9 import dns.rdataclass
10 import dns.rdatatype
11 
12 from homeassistant.config_entries import ConfigEntry
13 from homeassistant.const import (
14  CONF_ADDRESS,
15  CONF_HOST,
16  CONF_NAME,
17  CONF_PORT,
18  CONF_TYPE,
19  Platform,
20 )
21 from homeassistant.core import HomeAssistant, callback
22 from homeassistant.exceptions import ConfigEntryNotReady
25 
26 from .api import MinecraftServer, MinecraftServerAddressError, MinecraftServerType
27 from .const import DOMAIN, KEY_LATENCY, KEY_MOTD
28 from .coordinator import MinecraftServerCoordinator
29 
30 PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
31 
32 _LOGGER = logging.getLogger(__name__)
33 
34 
36  """Load dnspython rdata classes used by mcstatus."""
37  for rdtype in dns.rdatatype.RdataType:
38  if not dns.rdatatype.is_metatype(rdtype) or rdtype == dns.rdatatype.OPT:
39  dns.rdata.get_rdata_class(dns.rdataclass.IN, rdtype) # type: ignore[no-untyped-call]
40 
41 
42 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
43  """Set up Minecraft Server from a config entry."""
44 
45  # Workaround to avoid blocking imports from dnspython (https://github.com/rthalley/dnspython/issues/1083)
46  hass.async_add_executor_job(load_dnspython_rdata_classes)
47 
48  # Create API instance.
49  api = MinecraftServer(
50  hass,
51  entry.data.get(CONF_TYPE, MinecraftServerType.JAVA_EDITION),
52  entry.data[CONF_ADDRESS],
53  )
54 
55  # Initialize API instance.
56  try:
57  await api.async_initialize()
58  except MinecraftServerAddressError as error:
59  raise ConfigEntryNotReady(f"Initialization failed: {error}") from error
60 
61  # Create coordinator instance.
62  coordinator = MinecraftServerCoordinator(hass, entry.data[CONF_NAME], api)
63  await coordinator.async_config_entry_first_refresh()
64 
65  # Store coordinator instance.
66  domain_data = hass.data.setdefault(DOMAIN, {})
67  domain_data[entry.entry_id] = coordinator
68 
69  # Set up platforms.
70  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
71 
72  return True
73 
74 
75 async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
76  """Unload Minecraft Server config entry."""
77 
78  # Unload platforms.
79  unload_ok = await hass.config_entries.async_unload_platforms(
80  config_entry, PLATFORMS
81  )
82 
83  # Clean up.
84  hass.data[DOMAIN].pop(config_entry.entry_id)
85 
86  return unload_ok
87 
88 
89 async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
90  """Migrate old config entry to a new format."""
91 
92  # 1 --> 2: Use config entry ID as base for unique IDs.
93  if config_entry.version == 1:
94  _LOGGER.debug("Migrating from version 1")
95 
96  old_unique_id = config_entry.unique_id
97  assert old_unique_id
98  config_entry_id = config_entry.entry_id
99 
100  # Migrate config entry.
101  _LOGGER.debug("Migrating config entry. Resetting unique ID: %s", old_unique_id)
102  hass.config_entries.async_update_entry(config_entry, unique_id=None, version=2)
103 
104  # Migrate device.
105  await _async_migrate_device_identifiers(hass, config_entry, old_unique_id)
106 
107  # Migrate entities.
108  await er.async_migrate_entries(hass, config_entry_id, _migrate_entity_unique_id)
109 
110  _LOGGER.debug("Migration to version 2 successful")
111 
112  # 2 --> 3: Use address instead of host and port in config entry.
113  if config_entry.version == 2:
114  _LOGGER.debug("Migrating from version 2")
115 
116  config_data = config_entry.data
117 
118  # Migrate config entry.
119  address = config_data[CONF_HOST]
120  api = MinecraftServer(hass, MinecraftServerType.JAVA_EDITION, address)
121 
122  try:
123  await api.async_initialize()
124  host_only_lookup_success = True
125  except MinecraftServerAddressError as error:
126  host_only_lookup_success = False
127  _LOGGER.debug(
128  "Hostname (without port) cannot be parsed, trying again with port: %s",
129  error,
130  )
131 
132  if not host_only_lookup_success:
133  address = f"{config_data[CONF_HOST]}:{config_data[CONF_PORT]}"
134  api = MinecraftServer(hass, MinecraftServerType.JAVA_EDITION, address)
135 
136  try:
137  await api.async_initialize()
138  except MinecraftServerAddressError:
139  _LOGGER.exception(
140  "Can't migrate configuration entry due to error while parsing server address, try again later"
141  )
142  return False
143 
144  _LOGGER.debug(
145  "Migrating config entry, replacing host '%s' and port '%s' with address '%s'",
146  config_data[CONF_HOST],
147  config_data[CONF_PORT],
148  address,
149  )
150 
151  new_data = config_data.copy()
152  new_data[CONF_ADDRESS] = address
153  del new_data[CONF_HOST]
154  del new_data[CONF_PORT]
155  hass.config_entries.async_update_entry(config_entry, data=new_data, version=3)
156 
157  _LOGGER.debug("Migration to version 3 successful")
158 
159  return True
160 
161 
163  hass: HomeAssistant, config_entry: ConfigEntry, old_unique_id: str | None
164 ) -> None:
165  """Migrate the device identifiers to the new format."""
166  device_registry = dr.async_get(hass)
167  device_entry_found = False
168  for device_entry in dr.async_entries_for_config_entry(
169  device_registry, config_entry.entry_id
170  ):
171  for identifier in device_entry.identifiers:
172  if identifier[1] == old_unique_id:
173  # Device found in registry. Update identifiers.
174  new_identifiers = {
175  (
176  DOMAIN,
177  config_entry.entry_id,
178  )
179  }
180  _LOGGER.debug(
181  "Migrating device identifiers from %s to %s",
182  device_entry.identifiers,
183  new_identifiers,
184  )
185  device_registry.async_update_device(
186  device_id=device_entry.id, new_identifiers=new_identifiers
187  )
188  # Device entry found. Leave inner for loop.
189  device_entry_found = True
190  break
191 
192  # Leave outer for loop if device entry is already found.
193  if device_entry_found:
194  break
195 
196 
197 @callback
198 def _migrate_entity_unique_id(entity_entry: er.RegistryEntry) -> dict[str, Any]:
199  """Migrate the unique ID of an entity to the new format."""
200 
201  # Different variants of unique IDs are available in version 1:
202  # 1) SRV record: '<host>-srv-<entity_type>'
203  # 2) Host & port: '<host>-<port>-<entity_type>'
204  # 3) IP address & port: '<mac_address>-<port>-<entity_type>'
205  unique_id_pieces = entity_entry.unique_id.split("-")
206  entity_type = unique_id_pieces[2]
207 
208  # Handle bug in version 1: Entity type names were used instead of
209  # keys (e.g. "Protocol Version" instead of "protocol_version").
210  new_entity_type = entity_type.lower()
211  new_entity_type = new_entity_type.replace(" ", "_")
212 
213  # Special case 'MOTD': Name and key differs.
214  if new_entity_type == "world_message":
215  new_entity_type = KEY_MOTD
216 
217  # Special case 'latency_time': Renamed to 'latency'.
218  if new_entity_type == "latency_time":
219  new_entity_type = KEY_LATENCY
220 
221  new_unique_id = f"{entity_entry.config_entry_id}-{new_entity_type}"
222  _LOGGER.debug(
223  "Migrating entity unique ID from %s to %s",
224  entity_entry.unique_id,
225  new_unique_id,
226  )
227 
228  return {"new_unique_id": new_unique_id}
dict[str, Any] _migrate_entity_unique_id(er.RegistryEntry entity_entry)
Definition: __init__.py:198
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:89
None _async_migrate_device_identifiers(HomeAssistant hass, ConfigEntry config_entry, str|None old_unique_id)
Definition: __init__.py:164
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:42
bool async_unload_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:75