Add support for Minecraft Server Bedrock Edition (#100925)
This commit is contained in:
parent
deffa50142
commit
c6a3fa30f0
12 changed files with 447 additions and 144 deletions
|
@ -745,6 +745,7 @@ omit =
|
|||
homeassistant/components/mill/climate.py
|
||||
homeassistant/components/mill/sensor.py
|
||||
homeassistant/components/minecraft_server/__init__.py
|
||||
homeassistant/components/minecraft_server/api.py
|
||||
homeassistant/components/minecraft_server/binary_sensor.py
|
||||
homeassistant/components/minecraft_server/coordinator.py
|
||||
homeassistant/components/minecraft_server/entity.py
|
||||
|
|
|
@ -4,14 +4,21 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
from mcstatus import JavaServer
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_PORT, Platform
|
||||
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
|
||||
|
||||
|
@ -23,8 +30,20 @@ _LOGGER = logging.getLogger(__name__)
|
|||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Minecraft Server from a config entry."""
|
||||
|
||||
# Check and create API instance.
|
||||
try:
|
||||
api = await hass.async_add_executor_job(
|
||||
MinecraftServer,
|
||||
entry.data.get(CONF_TYPE, MinecraftServerType.JAVA_EDITION),
|
||||
entry.data[CONF_ADDRESS],
|
||||
)
|
||||
except MinecraftServerAddressError as error:
|
||||
raise ConfigEntryError(
|
||||
f"Server address in configuration entry is invalid (error: {error})"
|
||||
) from error
|
||||
|
||||
# Create coordinator instance.
|
||||
coordinator = MinecraftServerCoordinator(hass, entry)
|
||||
coordinator = MinecraftServerCoordinator(hass, entry.data[CONF_NAME], api)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Store coordinator instance.
|
||||
|
@ -85,9 +104,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||
# Migrate config entry.
|
||||
try:
|
||||
address = config_data[CONF_HOST]
|
||||
JavaServer.lookup(address)
|
||||
MinecraftServer(MinecraftServerType.JAVA_EDITION, address)
|
||||
host_only_lookup_success = True
|
||||
except ValueError as error:
|
||||
except MinecraftServerAddressError as error:
|
||||
host_only_lookup_success = False
|
||||
_LOGGER.debug(
|
||||
"Hostname (without port) cannot be parsed (error: %s), trying again with port",
|
||||
|
@ -97,8 +116,8 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||
if not host_only_lookup_success:
|
||||
try:
|
||||
address = f"{config_data[CONF_HOST]}:{config_data[CONF_PORT]}"
|
||||
JavaServer.lookup(address)
|
||||
except ValueError as error:
|
||||
MinecraftServer(MinecraftServerType.JAVA_EDITION, address)
|
||||
except MinecraftServerAddressError as error:
|
||||
_LOGGER.exception(
|
||||
"Can't migrate configuration entry due to error while parsing server address (error: %s), try again later",
|
||||
error,
|
||||
|
|
134
homeassistant/components/minecraft_server/api.py
Normal file
134
homeassistant/components/minecraft_server/api.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
"""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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@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 MinecraftServer:
|
||||
"""Minecraft Server wrapper class for 3rd party library mcstatus."""
|
||||
|
||||
_server: BedrockServer | JavaServer
|
||||
|
||||
def __init__(self, server_type: MinecraftServerType, address: str) -> None:
|
||||
"""Initialize server instance."""
|
||||
try:
|
||||
if server_type == MinecraftServerType.JAVA_EDITION:
|
||||
self._server = JavaServer.lookup(address)
|
||||
else:
|
||||
self._server = BedrockServer.lookup(address)
|
||||
except (ValueError, LifetimeTimeout) as error:
|
||||
raise MinecraftServerAddressError(
|
||||
f"{server_type} server address '{address}' is invalid (error: {error})"
|
||||
) from error
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s server instance created with address '%s'", server_type, 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:
|
||||
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
|
||||
|
||||
try:
|
||||
status_response = await self._server.async_status()
|
||||
except OSError as error:
|
||||
raise MinecraftServerConnectionError(
|
||||
f"Fetching data from the server failed (error: {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 = []
|
||||
|
||||
if players := status_response.players.sample:
|
||||
for player in players:
|
||||
players_list.append(player.name)
|
||||
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,
|
||||
)
|
|
@ -45,7 +45,7 @@ async def async_setup_entry(
|
|||
# Add binary sensor entities.
|
||||
async_add_entities(
|
||||
[
|
||||
MinecraftServerBinarySensorEntity(coordinator, description)
|
||||
MinecraftServerBinarySensorEntity(coordinator, description, config_entry)
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
]
|
||||
)
|
||||
|
@ -60,11 +60,12 @@ class MinecraftServerBinarySensorEntity(MinecraftServerEntity, BinarySensorEntit
|
|||
self,
|
||||
coordinator: MinecraftServerCoordinator,
|
||||
description: MinecraftServerBinarySensorEntityDescription,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize binary sensor base entity."""
|
||||
super().__init__(coordinator)
|
||||
super().__init__(coordinator, config_entry)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_id}-{description.key}"
|
||||
self._attr_unique_id = f"{config_entry.entry_id}-{description.key}"
|
||||
self._attr_is_on = False
|
||||
|
||||
@property
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"""Config flow for Minecraft Server integration."""
|
||||
import logging
|
||||
|
||||
from mcstatus import JavaServer
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .api import MinecraftServer, MinecraftServerAddressError, MinecraftServerType
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
|
||||
DEFAULT_ADDRESS = "localhost:25565"
|
||||
|
@ -27,10 +27,28 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
if user_input:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
|
||||
if await self._async_is_server_online(address):
|
||||
# No error was detected, create configuration entry.
|
||||
config_data = {CONF_NAME: user_input[CONF_NAME], CONF_ADDRESS: address}
|
||||
return self.async_create_entry(title=address, data=config_data)
|
||||
# Prepare config entry data.
|
||||
config_data = {
|
||||
CONF_NAME: user_input[CONF_NAME],
|
||||
CONF_ADDRESS: address,
|
||||
}
|
||||
|
||||
# Some Bedrock Edition servers mimic a Java Edition server, therefore check for a Bedrock Edition server first.
|
||||
for server_type in MinecraftServerType:
|
||||
try:
|
||||
api = await self.hass.async_add_executor_job(
|
||||
MinecraftServer, server_type, address
|
||||
)
|
||||
except MinecraftServerAddressError:
|
||||
pass
|
||||
else:
|
||||
if await api.async_is_online():
|
||||
config_data[CONF_TYPE] = server_type
|
||||
return self.async_create_entry(title=address, data=config_data)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Connection check to %s server '%s' failed", server_type, address
|
||||
)
|
||||
|
||||
# Host or port invalid or server not reachable.
|
||||
errors["base"] = "cannot_connect"
|
||||
|
@ -59,37 +77,3 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _async_is_server_online(self, address: str) -> bool:
|
||||
"""Check server connection using a 'status' request and return result."""
|
||||
|
||||
# Parse and check server address.
|
||||
try:
|
||||
server = await JavaServer.async_lookup(address)
|
||||
except ValueError as error:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Error occurred while parsing server address '%s' -"
|
||||
" ValueError: %s"
|
||||
),
|
||||
address,
|
||||
error,
|
||||
)
|
||||
return False
|
||||
|
||||
# Send a status request to the server.
|
||||
try:
|
||||
await server.async_status()
|
||||
return True
|
||||
except OSError as error:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Error occurred while trying to check the connection to '%s:%s' -"
|
||||
" OSError: %s"
|
||||
),
|
||||
server.address.host,
|
||||
server.address.port,
|
||||
error,
|
||||
)
|
||||
|
||||
return False
|
||||
|
|
|
@ -1,77 +1,36 @@
|
|||
"""The Minecraft Server integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from mcstatus.server import JavaServer
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .api import MinecraftServer, MinecraftServerConnectionError, MinecraftServerData
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MinecraftServerData:
|
||||
"""Representation of Minecraft Server data."""
|
||||
|
||||
latency: float
|
||||
motd: str
|
||||
players_max: int
|
||||
players_online: int
|
||||
players_list: list[str]
|
||||
protocol_version: int
|
||||
version: str
|
||||
|
||||
|
||||
class MinecraftServerCoordinator(DataUpdateCoordinator[MinecraftServerData]):
|
||||
"""Minecraft Server data update coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant, name: str, api: MinecraftServer) -> None:
|
||||
"""Initialize coordinator instance."""
|
||||
config_data = config_entry.data
|
||||
self.unique_id = config_entry.entry_id
|
||||
self._api = api
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
name=config_data[CONF_NAME],
|
||||
name=name,
|
||||
logger=_LOGGER,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
try:
|
||||
self._server = JavaServer.lookup(config_data[CONF_ADDRESS])
|
||||
except ValueError as error:
|
||||
raise HomeAssistantError(
|
||||
f"Address in configuration entry cannot be parsed (error: {error}), please remove this device and add it again"
|
||||
) from error
|
||||
|
||||
async def _async_update_data(self) -> MinecraftServerData:
|
||||
"""Get server data from 3rd party library and update properties."""
|
||||
"""Get updated data from the server."""
|
||||
try:
|
||||
status_response = await self._server.async_status()
|
||||
except OSError as error:
|
||||
return await self._api.async_get_data()
|
||||
except MinecraftServerConnectionError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
players_list = []
|
||||
if players := status_response.players.sample:
|
||||
for player in players:
|
||||
players_list.append(player.name)
|
||||
players_list.sort()
|
||||
|
||||
return MinecraftServerData(
|
||||
version=status_response.version.name,
|
||||
protocol_version=status_response.version.protocol,
|
||||
players_online=status_response.players.online,
|
||||
players_max=status_response.players.max,
|
||||
players_list=players_list,
|
||||
latency=status_response.latency,
|
||||
motd=status_response.motd.to_plain(),
|
||||
)
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
"""Base entity for the Minecraft Server integration."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TYPE
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .api import MinecraftServerType
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MinecraftServerCoordinator
|
||||
|
||||
|
@ -17,13 +20,15 @@ class MinecraftServerEntity(CoordinatorEntity[MinecraftServerCoordinator]):
|
|||
def __init__(
|
||||
self,
|
||||
coordinator: MinecraftServerCoordinator,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize base entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.unique_id)},
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=f"Minecraft Server ({coordinator.data.version})",
|
||||
model=f"Minecraft Server ({config_entry.data.get(CONF_TYPE, MinecraftServerType.JAVA_EDITION)})",
|
||||
name=coordinator.name,
|
||||
sw_version=str(coordinator.data.protocol_version),
|
||||
sw_version=f"{coordinator.data.version} ({coordinator.data.protocol_version})",
|
||||
)
|
||||
|
|
|
@ -7,17 +7,21 @@ from typing import Any
|
|||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTime
|
||||
from homeassistant.const import CONF_TYPE, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .api import MinecraftServerData, MinecraftServerType
|
||||
from .const import DOMAIN, KEY_LATENCY, KEY_MOTD
|
||||
from .coordinator import MinecraftServerCoordinator, MinecraftServerData
|
||||
from .coordinator import MinecraftServerCoordinator
|
||||
from .entity import MinecraftServerEntity
|
||||
|
||||
ATTR_PLAYERS_LIST = "players_list"
|
||||
|
||||
ICON_EDITION = "mdi:minecraft"
|
||||
ICON_GAME_MODE = "mdi:cog"
|
||||
ICON_MAP_NAME = "mdi:map"
|
||||
ICON_LATENCY = "mdi:signal"
|
||||
ICON_PLAYERS_MAX = "mdi:account-multiple"
|
||||
ICON_PLAYERS_ONLINE = "mdi:account-multiple"
|
||||
|
@ -25,6 +29,9 @@ ICON_PROTOCOL_VERSION = "mdi:numeric"
|
|||
ICON_VERSION = "mdi:numeric"
|
||||
ICON_MOTD = "mdi:minecraft"
|
||||
|
||||
KEY_EDITION = "edition"
|
||||
KEY_GAME_MODE = "game_mode"
|
||||
KEY_MAP_NAME = "map_name"
|
||||
KEY_PLAYERS_MAX = "players_max"
|
||||
KEY_PLAYERS_ONLINE = "players_online"
|
||||
KEY_PROTOCOL_VERSION = "protocol_version"
|
||||
|
@ -40,6 +47,7 @@ class MinecraftServerEntityDescriptionMixin:
|
|||
|
||||
value_fn: Callable[[MinecraftServerData], StateType]
|
||||
attributes_fn: Callable[[MinecraftServerData], MutableMapping[str, Any]] | None
|
||||
supported_server_types: list[MinecraftServerType]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -69,6 +77,10 @@ SENSOR_DESCRIPTIONS = [
|
|||
icon=ICON_VERSION,
|
||||
value_fn=lambda data: data.version,
|
||||
attributes_fn=None,
|
||||
supported_server_types=[
|
||||
MinecraftServerType.JAVA_EDITION,
|
||||
MinecraftServerType.BEDROCK_EDITION,
|
||||
],
|
||||
),
|
||||
MinecraftServerSensorEntityDescription(
|
||||
key=KEY_PROTOCOL_VERSION,
|
||||
|
@ -76,6 +88,10 @@ SENSOR_DESCRIPTIONS = [
|
|||
icon=ICON_PROTOCOL_VERSION,
|
||||
value_fn=lambda data: data.protocol_version,
|
||||
attributes_fn=None,
|
||||
supported_server_types=[
|
||||
MinecraftServerType.JAVA_EDITION,
|
||||
MinecraftServerType.BEDROCK_EDITION,
|
||||
],
|
||||
),
|
||||
MinecraftServerSensorEntityDescription(
|
||||
key=KEY_PLAYERS_MAX,
|
||||
|
@ -84,6 +100,10 @@ SENSOR_DESCRIPTIONS = [
|
|||
icon=ICON_PLAYERS_MAX,
|
||||
value_fn=lambda data: data.players_max,
|
||||
attributes_fn=None,
|
||||
supported_server_types=[
|
||||
MinecraftServerType.JAVA_EDITION,
|
||||
MinecraftServerType.BEDROCK_EDITION,
|
||||
],
|
||||
),
|
||||
MinecraftServerSensorEntityDescription(
|
||||
key=KEY_LATENCY,
|
||||
|
@ -93,6 +113,10 @@ SENSOR_DESCRIPTIONS = [
|
|||
icon=ICON_LATENCY,
|
||||
value_fn=lambda data: data.latency,
|
||||
attributes_fn=None,
|
||||
supported_server_types=[
|
||||
MinecraftServerType.JAVA_EDITION,
|
||||
MinecraftServerType.BEDROCK_EDITION,
|
||||
],
|
||||
),
|
||||
MinecraftServerSensorEntityDescription(
|
||||
key=KEY_MOTD,
|
||||
|
@ -100,6 +124,10 @@ SENSOR_DESCRIPTIONS = [
|
|||
icon=ICON_MOTD,
|
||||
value_fn=lambda data: data.motd,
|
||||
attributes_fn=None,
|
||||
supported_server_types=[
|
||||
MinecraftServerType.JAVA_EDITION,
|
||||
MinecraftServerType.BEDROCK_EDITION,
|
||||
],
|
||||
),
|
||||
MinecraftServerSensorEntityDescription(
|
||||
key=KEY_PLAYERS_ONLINE,
|
||||
|
@ -108,6 +136,40 @@ SENSOR_DESCRIPTIONS = [
|
|||
icon=ICON_PLAYERS_ONLINE,
|
||||
value_fn=lambda data: data.players_online,
|
||||
attributes_fn=get_extra_state_attributes_players_list,
|
||||
supported_server_types=[
|
||||
MinecraftServerType.JAVA_EDITION,
|
||||
MinecraftServerType.BEDROCK_EDITION,
|
||||
],
|
||||
),
|
||||
MinecraftServerSensorEntityDescription(
|
||||
key=KEY_EDITION,
|
||||
translation_key=KEY_EDITION,
|
||||
icon=ICON_EDITION,
|
||||
value_fn=lambda data: data.edition,
|
||||
attributes_fn=None,
|
||||
supported_server_types=[
|
||||
MinecraftServerType.BEDROCK_EDITION,
|
||||
],
|
||||
),
|
||||
MinecraftServerSensorEntityDescription(
|
||||
key=KEY_GAME_MODE,
|
||||
translation_key=KEY_GAME_MODE,
|
||||
icon=ICON_GAME_MODE,
|
||||
value_fn=lambda data: data.game_mode,
|
||||
attributes_fn=None,
|
||||
supported_server_types=[
|
||||
MinecraftServerType.BEDROCK_EDITION,
|
||||
],
|
||||
),
|
||||
MinecraftServerSensorEntityDescription(
|
||||
key=KEY_MAP_NAME,
|
||||
translation_key=KEY_MAP_NAME,
|
||||
icon=ICON_MAP_NAME,
|
||||
value_fn=lambda data: data.map_name,
|
||||
attributes_fn=None,
|
||||
supported_server_types=[
|
||||
MinecraftServerType.BEDROCK_EDITION,
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -123,8 +185,10 @@ async def async_setup_entry(
|
|||
# Add sensor entities.
|
||||
async_add_entities(
|
||||
[
|
||||
MinecraftServerSensorEntity(coordinator, description)
|
||||
MinecraftServerSensorEntity(coordinator, description, config_entry)
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
if config_entry.data.get(CONF_TYPE, MinecraftServerType.JAVA_EDITION)
|
||||
in description.supported_server_types
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -138,11 +202,12 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity):
|
|||
self,
|
||||
coordinator: MinecraftServerCoordinator,
|
||||
description: MinecraftServerSensorEntityDescription,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize sensor base entity."""
|
||||
super().__init__(coordinator)
|
||||
super().__init__(coordinator, config_entry)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.unique_id}-{description.key}"
|
||||
self._attr_unique_id = f"{config_entry.entry_id}-{description.key}"
|
||||
self._update_properties()
|
||||
|
||||
@callback
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect to server. Please check the address and try again. If a port was provided, it must be within a valid range. Also ensure that you are running at least version 1.7 of Minecraft Java Edition on your server."
|
||||
"cannot_connect": "Failed to connect to server. Please check the address and try again. If a port was provided, it must be within a valid range. If you are running a Minecraft Java Edition server, ensure that it is at least version 1.7."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@ -38,6 +38,15 @@
|
|||
},
|
||||
"motd": {
|
||||
"name": "World message"
|
||||
},
|
||||
"game_mode": {
|
||||
"name": "Game mode"
|
||||
},
|
||||
"map_name": {
|
||||
"name": "Map name"
|
||||
},
|
||||
"edition": {
|
||||
"name": "Edition"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
"""Constants for Minecraft Server integration tests."""
|
||||
from mcstatus.motd import Motd
|
||||
from mcstatus.status_response import (
|
||||
BedrockStatusPlayers,
|
||||
BedrockStatusResponse,
|
||||
BedrockStatusVersion,
|
||||
JavaStatusPlayers,
|
||||
JavaStatusResponse,
|
||||
JavaStatusVersion,
|
||||
)
|
||||
|
||||
from homeassistant.components.minecraft_server.api import MinecraftServerData
|
||||
|
||||
TEST_HOST = "mc.dummyserver.com"
|
||||
TEST_PORT = 25566
|
||||
TEST_ADDRESS = f"{TEST_HOST}:{TEST_PORT}"
|
||||
|
@ -32,3 +37,38 @@ TEST_JAVA_STATUS_RESPONSE = JavaStatusResponse(
|
|||
icon=None,
|
||||
latency=5,
|
||||
)
|
||||
|
||||
TEST_JAVA_DATA = MinecraftServerData(
|
||||
latency=5,
|
||||
motd="Dummy MOTD",
|
||||
players_max=10,
|
||||
players_online=3,
|
||||
protocol_version=123,
|
||||
version="Dummy Version",
|
||||
players_list=["Player 1", "Player 2", "Player 3"],
|
||||
edition=None,
|
||||
game_mode=None,
|
||||
map_name=None,
|
||||
)
|
||||
|
||||
TEST_BEDROCK_STATUS_RESPONSE = BedrockStatusResponse(
|
||||
players=BedrockStatusPlayers(online=3, max=10),
|
||||
version=BedrockStatusVersion(brand="MCPE", name="Dummy Version", protocol=123),
|
||||
motd=Motd.parse("Dummy Description", bedrock=True),
|
||||
latency=5,
|
||||
gamemode="Dummy Game Mode",
|
||||
map_name="Dummy Map Name",
|
||||
)
|
||||
|
||||
TEST_BEDROCK_DATA = MinecraftServerData(
|
||||
latency=5,
|
||||
motd="Dummy MOTD",
|
||||
players_max=10,
|
||||
players_online=3,
|
||||
protocol_version=123,
|
||||
version="Dummy Version",
|
||||
players_list=None,
|
||||
edition="Dummy Edition",
|
||||
game_mode="Dummy Game Mode",
|
||||
map_name="Dummy Map Name",
|
||||
)
|
||||
|
|
|
@ -2,15 +2,17 @@
|
|||
|
||||
from unittest.mock import patch
|
||||
|
||||
from mcstatus import JavaServer
|
||||
|
||||
from homeassistant.components.minecraft_server.api import (
|
||||
MinecraftServerAddressError,
|
||||
MinecraftServerType,
|
||||
)
|
||||
from homeassistant.components.minecraft_server.const import DEFAULT_NAME, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .const import TEST_ADDRESS, TEST_HOST, TEST_JAVA_STATUS_RESPONSE, TEST_PORT
|
||||
from .const import TEST_ADDRESS
|
||||
|
||||
USER_INPUT = {
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
|
@ -28,11 +30,12 @@ async def test_show_config_form(hass: HomeAssistant) -> None:
|
|||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
async def test_lookup_failed(hass: HomeAssistant) -> None:
|
||||
async def test_address_validation_failed(hass: HomeAssistant) -> None:
|
||||
"""Test error in case of a failed connection."""
|
||||
with patch(
|
||||
"mcstatus.server.JavaServer.async_lookup",
|
||||
side_effect=ValueError,
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||
side_effect=[MinecraftServerAddressError, MinecraftServerAddressError],
|
||||
return_value=None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
|
||||
|
@ -42,12 +45,16 @@ async def test_lookup_failed(hass: HomeAssistant) -> None:
|
|||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_connection_failed(hass: HomeAssistant) -> None:
|
||||
"""Test error in case of a failed connection."""
|
||||
async def test_java_connection_failed(hass: HomeAssistant) -> None:
|
||||
"""Test error in case of a failed connection to a Java Edition server."""
|
||||
with patch(
|
||||
"mcstatus.server.JavaServer.async_lookup",
|
||||
return_value=JavaServer(host=TEST_HOST, port=TEST_PORT),
|
||||
), patch("mcstatus.server.JavaServer.async_status", side_effect=OSError):
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||
side_effect=[MinecraftServerAddressError, None],
|
||||
return_value=None,
|
||||
), patch(
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.async_is_online",
|
||||
return_value=False,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
|
||||
)
|
||||
|
@ -56,14 +63,37 @@ async def test_connection_failed(hass: HomeAssistant) -> None:
|
|||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_connection_succeeded(hass: HomeAssistant) -> None:
|
||||
"""Test config entry in case of a successful connection with a host name."""
|
||||
async def test_bedrock_connection_failed(hass: HomeAssistant) -> None:
|
||||
"""Test error in case of a failed connection to a Bedrock Edition server."""
|
||||
with patch(
|
||||
"mcstatus.server.JavaServer.async_lookup",
|
||||
return_value=JavaServer(host=TEST_HOST, port=TEST_PORT),
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||
side_effect=[None, MinecraftServerAddressError],
|
||||
return_value=None,
|
||||
), patch(
|
||||
"mcstatus.server.JavaServer.async_status",
|
||||
return_value=TEST_JAVA_STATUS_RESPONSE,
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.async_is_online",
|
||||
return_value=False,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_java_connection_succeeded(hass: HomeAssistant) -> None:
|
||||
"""Test config entry in case of a successful connection to a Java Edition server."""
|
||||
with patch(
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||
side_effect=[
|
||||
MinecraftServerAddressError, # async_step_user (Bedrock Edition)
|
||||
None, # async_step_user (Java Edition)
|
||||
None, # async_setup_entry
|
||||
],
|
||||
return_value=None,
|
||||
), patch(
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.async_is_online",
|
||||
return_value=True,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
|
||||
|
@ -73,3 +103,56 @@ async def test_connection_succeeded(hass: HomeAssistant) -> None:
|
|||
assert result["title"] == USER_INPUT[CONF_ADDRESS]
|
||||
assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME]
|
||||
assert result["data"][CONF_ADDRESS] == TEST_ADDRESS
|
||||
assert result["data"][CONF_TYPE] == MinecraftServerType.JAVA_EDITION
|
||||
|
||||
|
||||
async def test_bedrock_connection_succeeded(hass: HomeAssistant) -> None:
|
||||
"""Test config entry in case of a successful connection to a Bedrock Edition server."""
|
||||
with patch(
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||
side_effect=None,
|
||||
return_value=None,
|
||||
), patch(
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.async_is_online",
|
||||
return_value=True,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == USER_INPUT[CONF_ADDRESS]
|
||||
assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME]
|
||||
assert result["data"][CONF_ADDRESS] == TEST_ADDRESS
|
||||
assert result["data"][CONF_TYPE] == MinecraftServerType.BEDROCK_EDITION
|
||||
|
||||
|
||||
async def test_recovery(hass: HomeAssistant) -> None:
|
||||
"""Test config flow recovery (successful connection after a failed connection)."""
|
||||
with patch(
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||
side_effect=[MinecraftServerAddressError, MinecraftServerAddressError],
|
||||
return_value=None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||
side_effect=None,
|
||||
return_value=None,
|
||||
), patch(
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.async_is_online",
|
||||
return_value=True,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
flow_id=result["flow_id"], user_input=USER_INPUT
|
||||
)
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == USER_INPUT[CONF_ADDRESS]
|
||||
assert result2["data"][CONF_NAME] == USER_INPUT[CONF_NAME]
|
||||
assert result2["data"][CONF_ADDRESS] == TEST_ADDRESS
|
||||
assert result2["data"][CONF_TYPE] == MinecraftServerType.BEDROCK_EDITION
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
"""Tests for the Minecraft Server integration."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from mcstatus import JavaServer
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.minecraft_server.api import MinecraftServerAddressError
|
||||
from homeassistant.components.minecraft_server.const import DEFAULT_NAME, DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_NAME, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .const import TEST_ADDRESS, TEST_HOST, TEST_JAVA_STATUS_RESPONSE, TEST_PORT
|
||||
from .const import TEST_ADDRESS, TEST_HOST, TEST_JAVA_DATA, TEST_PORT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
@ -122,15 +121,16 @@ async def test_entry_migration(hass: HomeAssistant) -> None:
|
|||
|
||||
# Trigger migration.
|
||||
with patch(
|
||||
"mcstatus.server.JavaServer.lookup",
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||
side_effect=[
|
||||
ValueError,
|
||||
JavaServer(host=TEST_HOST, port=TEST_PORT),
|
||||
JavaServer(host=TEST_HOST, port=TEST_PORT),
|
||||
MinecraftServerAddressError, # async_migrate_entry
|
||||
None, # async_migrate_entry
|
||||
None, # async_setup_entry
|
||||
],
|
||||
return_value=None,
|
||||
), patch(
|
||||
"mcstatus.server.JavaServer.async_status",
|
||||
return_value=TEST_JAVA_STATUS_RESPONSE,
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.async_get_data",
|
||||
return_value=TEST_JAVA_DATA,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -142,6 +142,7 @@ async def test_entry_migration(hass: HomeAssistant) -> None:
|
|||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ADDRESS: TEST_ADDRESS,
|
||||
}
|
||||
|
||||
assert config_entry.version == 3
|
||||
|
||||
# Test migrated device entry.
|
||||
|
@ -174,14 +175,15 @@ async def test_entry_migration_host_only(hass: HomeAssistant) -> None:
|
|||
|
||||
# Trigger migration.
|
||||
with patch(
|
||||
"mcstatus.server.JavaServer.lookup",
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||
side_effect=[
|
||||
JavaServer(host=TEST_HOST, port=TEST_PORT),
|
||||
JavaServer(host=TEST_HOST, port=TEST_PORT),
|
||||
None, # async_migrate_entry
|
||||
None, # async_setup_entry
|
||||
],
|
||||
return_value=None,
|
||||
), patch(
|
||||
"mcstatus.server.JavaServer.async_status",
|
||||
return_value=TEST_JAVA_STATUS_RESPONSE,
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.async_get_data",
|
||||
return_value=TEST_JAVA_DATA,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -205,11 +207,12 @@ async def test_entry_migration_v3_failure(hass: HomeAssistant) -> None:
|
|||
|
||||
# Trigger migration.
|
||||
with patch(
|
||||
"mcstatus.server.JavaServer.lookup",
|
||||
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||
side_effect=[
|
||||
ValueError,
|
||||
ValueError,
|
||||
MinecraftServerAddressError, # async_migrate_entry
|
||||
MinecraftServerAddressError, # async_migrate_entry
|
||||
],
|
||||
return_value=None,
|
||||
):
|
||||
assert not await hass.config_entries.async_setup(config_entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue