mirror of https://github.com/home-assistant/core
179 lines
5.8 KiB
Python
179 lines
5.8 KiB
Python
"""API for the Minecraft Server integration."""
|
|
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
import logging
|
|
|
|
from dns.resolver import LifetimeTimeout
|
|
from mcstatus import BedrockServer, JavaServer
|
|
from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse
|
|
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
LOOKUP_TIMEOUT: float = 10
|
|
DATA_UPDATE_TIMEOUT: float = 10
|
|
DATA_UPDATE_RETRIES: int = 3
|
|
|
|
|
|
@dataclass
|
|
class MinecraftServerData:
|
|
"""Representation of Minecraft Server data."""
|
|
|
|
# Common data
|
|
latency: float
|
|
motd: str
|
|
players_max: int
|
|
players_online: int
|
|
protocol_version: int
|
|
version: str
|
|
|
|
# Data available only in 'Java Edition'
|
|
players_list: list[str] | None = None
|
|
|
|
# Data available only in 'Bedrock Edition'
|
|
edition: str | None = None
|
|
game_mode: str | None = None
|
|
map_name: str | None = None
|
|
|
|
|
|
class MinecraftServerType(StrEnum):
|
|
"""Enumeration of Minecraft Server types."""
|
|
|
|
BEDROCK_EDITION = "Bedrock Edition"
|
|
JAVA_EDITION = "Java Edition"
|
|
|
|
|
|
class MinecraftServerAddressError(Exception):
|
|
"""Raised when the input address is invalid."""
|
|
|
|
|
|
class MinecraftServerConnectionError(Exception):
|
|
"""Raised when no data can be fechted from the server."""
|
|
|
|
|
|
class MinecraftServerNotInitializedError(Exception):
|
|
"""Raised when APIs are used although server instance is not initialized yet."""
|
|
|
|
|
|
class MinecraftServer:
|
|
"""Minecraft Server wrapper class for 3rd party library mcstatus."""
|
|
|
|
_server: BedrockServer | JavaServer | None
|
|
|
|
def __init__(
|
|
self, hass: HomeAssistant, server_type: MinecraftServerType, address: str
|
|
) -> None:
|
|
"""Initialize server instance."""
|
|
self._server = None
|
|
self._hass = hass
|
|
self._server_type = server_type
|
|
self._address = address
|
|
|
|
async def async_initialize(self) -> None:
|
|
"""Perform async initialization of server instance."""
|
|
try:
|
|
if self._server_type == MinecraftServerType.JAVA_EDITION:
|
|
self._server = await JavaServer.async_lookup(self._address)
|
|
else:
|
|
self._server = await self._hass.async_add_executor_job(
|
|
BedrockServer.lookup, self._address
|
|
)
|
|
except (ValueError, LifetimeTimeout) as error:
|
|
raise MinecraftServerAddressError(
|
|
f"Lookup of '{self._address}' failed: {self._get_error_message(error)}"
|
|
) from error
|
|
|
|
self._server.timeout = DATA_UPDATE_TIMEOUT
|
|
|
|
_LOGGER.debug(
|
|
"Initialized %s server instance with address '%s'",
|
|
self._server_type,
|
|
self._address,
|
|
)
|
|
|
|
async def async_is_online(self) -> bool:
|
|
"""Check if the server is online, supporting both Java and Bedrock Edition servers."""
|
|
try:
|
|
await self.async_get_data()
|
|
except (
|
|
MinecraftServerConnectionError,
|
|
MinecraftServerNotInitializedError,
|
|
) as error:
|
|
_LOGGER.debug(
|
|
"Connection check of %s server failed: %s",
|
|
self._server_type,
|
|
self._get_error_message(error),
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
async def async_get_data(self) -> MinecraftServerData:
|
|
"""Get updated data from the server, supporting both Java and Bedrock Edition servers."""
|
|
status_response: BedrockStatusResponse | JavaStatusResponse
|
|
|
|
if self._server is None:
|
|
raise MinecraftServerNotInitializedError(
|
|
f"Server instance with address '{self._address}' is not initialized"
|
|
)
|
|
|
|
try:
|
|
status_response = await self._server.async_status(tries=DATA_UPDATE_RETRIES)
|
|
except OSError as error:
|
|
raise MinecraftServerConnectionError(
|
|
f"Status request to '{self._address}' failed: {self._get_error_message(error)}"
|
|
) from error
|
|
|
|
if isinstance(status_response, JavaStatusResponse):
|
|
data = self._extract_java_data(status_response)
|
|
else:
|
|
data = self._extract_bedrock_data(status_response)
|
|
|
|
return data
|
|
|
|
def _extract_java_data(
|
|
self, status_response: JavaStatusResponse
|
|
) -> MinecraftServerData:
|
|
"""Extract Java Edition server data out of status response."""
|
|
players_list: list[str] = []
|
|
|
|
if players := status_response.players.sample:
|
|
players_list.extend(player.name for player in players)
|
|
players_list.sort()
|
|
|
|
return MinecraftServerData(
|
|
latency=status_response.latency,
|
|
motd=status_response.motd.to_plain(),
|
|
players_max=status_response.players.max,
|
|
players_online=status_response.players.online,
|
|
protocol_version=status_response.version.protocol,
|
|
version=status_response.version.name,
|
|
players_list=players_list,
|
|
)
|
|
|
|
def _extract_bedrock_data(
|
|
self, status_response: BedrockStatusResponse
|
|
) -> MinecraftServerData:
|
|
"""Extract Bedrock Edition server data out of status response."""
|
|
return MinecraftServerData(
|
|
latency=status_response.latency,
|
|
motd=status_response.motd.to_plain(),
|
|
players_max=status_response.players.max,
|
|
players_online=status_response.players.online,
|
|
protocol_version=status_response.version.protocol,
|
|
version=status_response.version.name,
|
|
edition=status_response.version.brand,
|
|
game_mode=status_response.gamemode,
|
|
map_name=status_response.map_name,
|
|
)
|
|
|
|
def _get_error_message(self, error: BaseException) -> str:
|
|
"""Get error message of an exception."""
|
|
if not str(error):
|
|
# Fallback to error type in case of an empty error message.
|
|
return repr(error)
|
|
|
|
return str(error)
|