Home Assistant Unofficial Reference 2024.12.1
api.py
Go to the documentation of this file.
1 """API for the Minecraft Server integration."""
2 
3 from dataclasses import dataclass
4 from enum import StrEnum
5 import logging
6 
7 from dns.resolver import LifetimeTimeout
8 from mcstatus import BedrockServer, JavaServer
9 from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse
10 
11 from homeassistant.core import HomeAssistant
12 
13 _LOGGER = logging.getLogger(__name__)
14 
15 LOOKUP_TIMEOUT: float = 10
16 DATA_UPDATE_TIMEOUT: float = 10
17 DATA_UPDATE_RETRIES: int = 3
18 
19 
20 @dataclass
22  """Representation of Minecraft Server data."""
23 
24  # Common data
25  latency: float
26  motd: str
27  players_max: int
28  players_online: int
29  protocol_version: int
30  version: str
31 
32  # Data available only in 'Java Edition'
33  players_list: list[str] | None = None
34 
35  # Data available only in 'Bedrock Edition'
36  edition: str | None = None
37  game_mode: str | None = None
38  map_name: str | None = None
39 
40 
41 class MinecraftServerType(StrEnum):
42  """Enumeration of Minecraft Server types."""
43 
44  BEDROCK_EDITION = "Bedrock Edition"
45  JAVA_EDITION = "Java Edition"
46 
47 
48 class MinecraftServerAddressError(Exception):
49  """Raised when the input address is invalid."""
50 
51 
52 class MinecraftServerConnectionError(Exception):
53  """Raised when no data can be fechted from the server."""
54 
55 
57  """Raised when APIs are used although server instance is not initialized yet."""
58 
59 
61  """Minecraft Server wrapper class for 3rd party library mcstatus."""
62 
63  _server: BedrockServer | JavaServer | None
64 
65  def __init__(
66  self, hass: HomeAssistant, server_type: MinecraftServerType, address: str
67  ) -> None:
68  """Initialize server instance."""
69  self._server_server = None
70  self._hass_hass = hass
71  self._server_type_server_type = server_type
72  self._address_address = address
73 
74  async def async_initialize(self) -> None:
75  """Perform async initialization of server instance."""
76  try:
77  if self._server_type_server_type == MinecraftServerType.JAVA_EDITION:
78  self._server_server = await JavaServer.async_lookup(self._address_address)
79  else:
80  self._server_server = await self._hass_hass.async_add_executor_job(
81  BedrockServer.lookup, self._address_address
82  )
83  except (ValueError, LifetimeTimeout) as error:
85  f"Lookup of '{self._address}' failed: {self._get_error_message(error)}"
86  ) from error
87 
88  self._server_server.timeout = DATA_UPDATE_TIMEOUT
89 
90  _LOGGER.debug(
91  "Initialized %s server instance with address '%s'",
92  self._server_type_server_type,
93  self._address_address,
94  )
95 
96  async def async_is_online(self) -> bool:
97  """Check if the server is online, supporting both Java and Bedrock Edition servers."""
98  try:
99  await self.async_get_dataasync_get_data()
100  except (
101  MinecraftServerConnectionError,
102  MinecraftServerNotInitializedError,
103  ) as error:
104  _LOGGER.debug(
105  "Connection check of %s server failed: %s",
106  self._server_type_server_type,
107  self._get_error_message_get_error_message(error),
108  )
109  return False
110 
111  return True
112 
113  async def async_get_data(self) -> MinecraftServerData:
114  """Get updated data from the server, supporting both Java and Bedrock Edition servers."""
115  status_response: BedrockStatusResponse | JavaStatusResponse
116 
117  if self._server_server is None:
119  f"Server instance with address '{self._address}' is not initialized"
120  )
121 
122  try:
123  status_response = await self._server_server.async_status(tries=DATA_UPDATE_RETRIES)
124  except OSError as error:
126  f"Status request to '{self._address}' failed: {self._get_error_message(error)}"
127  ) from error
128 
129  if isinstance(status_response, JavaStatusResponse):
130  data = self._extract_java_data_extract_java_data(status_response)
131  else:
132  data = self._extract_bedrock_data_extract_bedrock_data(status_response)
133 
134  return data
135 
137  self, status_response: JavaStatusResponse
138  ) -> MinecraftServerData:
139  """Extract Java Edition server data out of status response."""
140  players_list: list[str] = []
141 
142  if players := status_response.players.sample:
143  players_list.extend(player.name for player in players)
144  players_list.sort()
145 
146  return MinecraftServerData(
147  latency=status_response.latency,
148  motd=status_response.motd.to_plain(),
149  players_max=status_response.players.max,
150  players_online=status_response.players.online,
151  protocol_version=status_response.version.protocol,
152  version=status_response.version.name,
153  players_list=players_list,
154  )
155 
157  self, status_response: BedrockStatusResponse
158  ) -> MinecraftServerData:
159  """Extract Bedrock Edition server data out of status response."""
160  return MinecraftServerData(
161  latency=status_response.latency,
162  motd=status_response.motd.to_plain(),
163  players_max=status_response.players.max,
164  players_online=status_response.players.online,
165  protocol_version=status_response.version.protocol,
166  version=status_response.version.name,
167  edition=status_response.version.brand,
168  game_mode=status_response.gamemode,
169  map_name=status_response.map_name,
170  )
171 
172  def _get_error_message(self, error: BaseException) -> str:
173  """Get error message of an exception."""
174  if not str(error):
175  # Fallback to error type in case of an empty error message.
176  return repr(error)
177 
178  return str(error)
None __init__(self, HomeAssistant hass, MinecraftServerType server_type, str address)
Definition: api.py:67
MinecraftServerData _extract_java_data(self, JavaStatusResponse status_response)
Definition: api.py:138
MinecraftServerData _extract_bedrock_data(self, BedrockStatusResponse status_response)
Definition: api.py:158
str _get_error_message(self, BaseException error)
Definition: api.py:172