Fix Minecraft Server config flow for Legacy Java Edition servers (#181948)

This commit is contained in:
elmurato
2026-09-29 20:26:32 +02:00
committed by GitHub
parent bd37df1491
commit 8f515ddc8a
5 changed files with 247 additions and 249 deletions
@@ -45,9 +45,9 @@ class MinecraftServerData:
class MinecraftServerType(StrEnum):
"""Enumeration of Minecraft Server types."""
BEDROCK_EDITION = "Bedrock Edition"
JAVA_EDITION = "Java Edition"
LEGACY_JAVA_EDITION = "Legacy Java Edition"
JAVA_EDITION = "Java Edition"
BEDROCK_EDITION = "Bedrock Edition"
class MinecraftServerAddressError(Exception):
@@ -28,37 +28,39 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input:
address = user_input[CONF_ADDRESS]
# Abort config flow if service is already configured.
self._async_abort_entries_match({CONF_ADDRESS: address})
if user_input is not None:
server_type = MinecraftServerType(user_input[CONF_TYPE])
server_address = user_input[CONF_ADDRESS]
# Prepare config entry data.
config_data = {
CONF_ADDRESS: address,
CONF_ADDRESS: server_address,
CONF_TYPE: server_type,
}
# Some Bedrock Edition servers mimic a Java Edition
# server, therefore check for Bedrock Edition first.
for server_type in MinecraftServerType:
api = MinecraftServer(self.hass, server_type, address)
# Abort config flow if service is already configured.
self._async_abort_entries_match({CONF_ADDRESS: server_address})
try:
await api.async_initialize()
except MinecraftServerAddressError as error:
_LOGGER.debug(
"Initialization of %s server failed: %s",
server_type,
error,
api = MinecraftServer(self.hass, server_type, server_address)
try:
await api.async_initialize()
except MinecraftServerAddressError as error:
_LOGGER.debug(
"Initialization of %s server failed: %s",
server_type,
error,
)
errors["base"] = "cannot_connect"
else:
if await api.async_is_online():
return self.async_create_entry(
title=server_address,
data=config_data,
)
else:
if await api.async_is_online():
config_data[CONF_TYPE] = server_type
return self.async_create_entry(title=address, data=config_data)
# Host or port invalid or server not reachable.
errors["base"] = "cannot_connect"
# Wrong edition selected, host or port invalid or server not reachable.
errors["base"] = "cannot_connect"
# Show configuration form (default form in case of no user_input,
# form filled with user_input and eventually with errors otherwise).
@@ -69,7 +71,7 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
user_input: dict[str, Any] | None = None,
errors: dict[str, str] | None = None,
) -> ConfigFlowResult:
"""Show the setup form to the user."""
"""Show the configuration form to the user."""
if user_input is None:
user_input = {}
@@ -77,6 +79,12 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="user",
data_schema=probatio.Schema(
{
probatio.Required(
CONF_TYPE,
default=user_input.get(
CONF_TYPE, MinecraftServerType.JAVA_EDITION
),
): probatio.In(list(MinecraftServerType)),
probatio.Required(
CONF_ADDRESS,
default=user_input.get(CONF_ADDRESS, DEFAULT_ADDRESS),
@@ -84,5 +92,4 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
}
),
errors=errors,
description_placeholders={"minimum_minecraft_version": "1.4"},
)
@@ -74,13 +74,13 @@ rules:
comment: |
No discovery possible. Users can use the (local or public) hostname instead of an IP address,
if static IP addresses cannot be configured.
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: todo
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: exempt
comment: A minecraft server can only have one device.
@@ -4,17 +4,19 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"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. If you are running a Minecraft Java Edition server, ensure that it is at least version {minimum_minecraft_version}."
"cannot_connect": "Failed to connect to server. Please re-check the server edition and server address and try again."
},
"step": {
"user": {
"data": {
"address": "Server address"
"address": "Server address",
"type": "Server edition"
},
"data_description": {
"address": "The hostname, IP address or SRV record of your Minecraft server, optionally including the port."
"address": "The hostname, IP address or SRV record of your Minecraft server, optionally including the port.",
"type": "For Java Edition beta version 1.8 till release version 1.6.4, select 'Legacy Java Edition'.\n For Java Edition release version 1.7+, select 'Java Edition'.\n For all Bedrock Edition versions, select 'Bedrock Edition'."
},
"description": "Set up your Minecraft Server instance to allow monitoring.",
"description": "Set up your Minecraft Server instance to allow monitoring.\n\nSelect the server edition and server address.",
"title": "Link your Minecraft Server"
}
}
@@ -3,6 +3,7 @@
from unittest.mock import patch
from mcstatus import BedrockServer, JavaServer, LegacyServer
import pytest
from homeassistant.components.minecraft_server.api import MinecraftServerType
from homeassistant.components.minecraft_server.const import DOMAIN
@@ -22,13 +23,55 @@ from .const import (
from tests.common import MockConfigEntry
USER_INPUT = {
CONF_ADDRESS: TEST_ADDRESS,
}
SERVER_EDITION_CASE_PARAM_NAMES = (
"server_type",
"lookup_target",
"lookup_result",
"status_target",
"status_response",
)
SERVER_EDITION_SUCCESS_CASES = [
(
MinecraftServerType.LEGACY_JAVA_EDITION,
"homeassistant.components.minecraft_server.api.LegacyServer.async_lookup",
lambda: LegacyServer(host=TEST_HOST, port=TEST_PORT),
"homeassistant.components.minecraft_server.api.LegacyServer.async_status",
TEST_LEGACY_JAVA_STATUS_RESPONSE,
),
(
MinecraftServerType.JAVA_EDITION,
"homeassistant.components.minecraft_server.api.JavaServer.async_lookup",
lambda: JavaServer(host=TEST_HOST, port=TEST_PORT),
"homeassistant.components.minecraft_server.api.JavaServer.async_status",
TEST_JAVA_STATUS_RESPONSE,
),
(
MinecraftServerType.BEDROCK_EDITION,
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
lambda: BedrockServer(host=TEST_HOST, port=TEST_PORT),
"homeassistant.components.minecraft_server.api.BedrockServer.async_status",
TEST_BEDROCK_STATUS_RESPONSE,
),
]
SERVER_EDITION_CASE_IDS = ["legacy_java", "java", "bedrock"]
async def test_full_flow_java(hass: HomeAssistant) -> None:
"""Test config entry in case of a successful connection to a Java Edition server."""
@pytest.mark.parametrize(
SERVER_EDITION_CASE_PARAM_NAMES,
SERVER_EDITION_SUCCESS_CASES,
ids=SERVER_EDITION_CASE_IDS,
)
async def test_full_flow(
hass: HomeAssistant,
server_type: MinecraftServerType,
lookup_target: str,
lookup_result: callable,
status_target: str,
status_response: dict,
) -> None:
"""Test config entry creation for all supported server editions."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@@ -37,210 +80,167 @@ async def test_full_flow_java(hass: HomeAssistant) -> None:
assert result["step_id"] == "user"
with (
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.JavaServer.async_lookup",
return_value=JavaServer(host=TEST_HOST, port=TEST_PORT),
),
patch(
"homeassistant.components.minecraft_server.api.JavaServer.async_status",
return_value=TEST_JAVA_STATUS_RESPONSE,
),
patch(lookup_target, return_value=lookup_result()),
patch(status_target, return_value=status_response),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=USER_INPUT,
user_input={
CONF_TYPE: server_type,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == USER_INPUT[CONF_ADDRESS]
assert result["data"][CONF_ADDRESS] == TEST_ADDRESS
assert result["data"][CONF_TYPE] == MinecraftServerType.JAVA_EDITION
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_ADDRESS
assert result["data"][CONF_ADDRESS] == TEST_ADDRESS
assert result["data"][CONF_TYPE] == server_type
async def test_full_flow_bedrock(hass: HomeAssistant) -> None:
"""Test config entry for successful Bedrock Edition connection."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@pytest.mark.parametrize(
SERVER_EDITION_CASE_PARAM_NAMES,
SERVER_EDITION_SUCCESS_CASES,
ids=SERVER_EDITION_CASE_IDS,
)
async def test_service_already_configured(
hass: HomeAssistant,
server_type: MinecraftServerType,
lookup_target: str,
lookup_result: callable,
status_target: str,
status_response: dict,
) -> None:
"""Test config flow abort if a server is already configured."""
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_ADDRESS: TEST_ADDRESS, CONF_TYPE: server_type},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
return_value=BedrockServer(host=TEST_HOST, port=TEST_PORT),
),
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.async_status",
return_value=TEST_BEDROCK_STATUS_RESPONSE,
),
patch(lookup_target, return_value=lookup_result()),
patch(status_target, return_value=status_response),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=USER_INPUT,
user_input={
CONF_TYPE: server_type,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == USER_INPUT[CONF_ADDRESS]
assert result["data"][CONF_ADDRESS] == TEST_ADDRESS
assert result["data"][CONF_TYPE] == MinecraftServerType.BEDROCK_EDITION
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_full_flow_legacy_java(hass: HomeAssistant) -> None:
"""Test config entry for successful legacy Java Edition connection."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@pytest.mark.parametrize(
SERVER_EDITION_CASE_PARAM_NAMES,
SERVER_EDITION_SUCCESS_CASES,
ids=SERVER_EDITION_CASE_IDS,
)
async def test_recovery(
hass: HomeAssistant,
server_type: MinecraftServerType,
lookup_target: str,
lookup_result: callable,
status_target: str,
status_response: dict,
) -> None:
"""Test recovery flow across all supported server editions."""
with (
patch(lookup_target, return_value=lookup_result()),
patch(status_target, side_effect=OSError),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_TYPE: server_type,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
with (
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.JavaServer.async_lookup",
side_effect=ValueError,
),
patch(
patch(lookup_target, return_value=lookup_result()),
patch(status_target, return_value=status_response),
):
result2 = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"],
user_input={
CONF_TYPE: server_type,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == TEST_ADDRESS
assert result2["data"][CONF_ADDRESS] == TEST_ADDRESS
assert result2["data"][CONF_TYPE] == server_type
@pytest.mark.parametrize(
("server_type", "lookup_target"),
[
(
MinecraftServerType.LEGACY_JAVA_EDITION,
"homeassistant.components.minecraft_server.api.LegacyServer.async_lookup",
return_value=LegacyServer(host=TEST_HOST, port=TEST_PORT),
),
patch(
"homeassistant.components.minecraft_server.api.LegacyServer.async_status",
return_value=TEST_LEGACY_JAVA_STATUS_RESPONSE,
),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=USER_INPUT,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == USER_INPUT[CONF_ADDRESS]
assert result["data"][CONF_ADDRESS] == TEST_ADDRESS
assert result["data"][CONF_TYPE] == MinecraftServerType.LEGACY_JAVA_EDITION
async def test_service_already_configured_java(
hass: HomeAssistant, java_mock_config_entry: MockConfigEntry
) -> None:
"""Test config flow abort if a Java Edition server is already configured."""
java_mock_config_entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
side_effect=ValueError,
),
patch(
(
MinecraftServerType.JAVA_EDITION,
"homeassistant.components.minecraft_server.api.JavaServer.async_lookup",
return_value=JavaServer(host=TEST_HOST, port=TEST_PORT),
),
patch(
"homeassistant.components.minecraft_server.api.JavaServer.async_status",
return_value=TEST_JAVA_STATUS_RESPONSE,
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=USER_INPUT,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_service_already_configured_bedrock(
hass: HomeAssistant, bedrock_mock_config_entry: MockConfigEntry
) -> None:
"""Test config flow abort if a Bedrock Edition server is already configured."""
bedrock_mock_config_entry.add_to_hass(hass)
with (
patch(
(
MinecraftServerType.BEDROCK_EDITION,
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
return_value=BedrockServer(host=TEST_HOST, port=TEST_PORT),
),
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.async_status",
return_value=TEST_BEDROCK_STATUS_RESPONSE,
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=USER_INPUT,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_service_already_configured_legacy_java(
hass: HomeAssistant, legacy_java_mock_config_entry: MockConfigEntry
],
ids=SERVER_EDITION_CASE_IDS,
)
async def test_address_lookup_error(
hass: HomeAssistant,
server_type: MinecraftServerType,
lookup_target: str,
) -> None:
"""Test config flow abort if a legacy Java Edition server is already configured."""
legacy_java_mock_config_entry.add_to_hass(hass)
"""Test config flow handles a server address lookup error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
with (
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.JavaServer.async_lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.LegacyServer.async_lookup",
return_value=LegacyServer(host=TEST_HOST, port=TEST_PORT),
),
patch(
"homeassistant.components.minecraft_server.api.LegacyServer.async_status",
return_value=TEST_LEGACY_JAVA_STATUS_RESPONSE,
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
with patch(lookup_target, side_effect=ValueError):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=USER_INPUT,
user_input={
CONF_TYPE: server_type,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"}
async def test_recovery_java(hass: HomeAssistant) -> None:
"""Test config flow recovery with a Java Edition server."""
with (
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.JavaServer.async_lookup",
return_value=JavaServer(host=TEST_HOST, port=TEST_PORT),
@@ -249,10 +249,6 @@ async def test_recovery_java(hass: HomeAssistant) -> None:
"homeassistant.components.minecraft_server.api.JavaServer.async_status",
side_effect=OSError,
),
patch(
"homeassistant.components.minecraft_server.api.LegacyServer.async_lookup",
side_effect=ValueError,
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -263,16 +259,15 @@ async def test_recovery_java(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=USER_INPUT,
user_input={
CONF_TYPE: MinecraftServerType.JAVA_EDITION,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
with (
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.JavaServer.async_lookup",
return_value=JavaServer(host=TEST_HOST, port=TEST_PORT),
@@ -283,10 +278,14 @@ async def test_recovery_java(hass: HomeAssistant) -> None:
),
):
result2 = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"], user_input=USER_INPUT
flow_id=result["flow_id"],
user_input={
CONF_TYPE: MinecraftServerType.JAVA_EDITION,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == USER_INPUT[CONF_ADDRESS]
assert result2["title"] == TEST_ADDRESS
assert result2["data"][CONF_ADDRESS] == TEST_ADDRESS
assert result2["data"][CONF_TYPE] == MinecraftServerType.JAVA_EDITION
@@ -302,14 +301,6 @@ async def test_recovery_bedrock(hass: HomeAssistant) -> None:
"homeassistant.components.minecraft_server.api.BedrockServer.async_status",
side_effect=OSError,
),
patch(
"homeassistant.components.minecraft_server.api.JavaServer.async_lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.LegacyServer.async_lookup",
side_effect=ValueError,
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@@ -320,7 +311,10 @@ async def test_recovery_bedrock(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=USER_INPUT,
user_input={
CONF_TYPE: MinecraftServerType.BEDROCK_EDITION,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
@@ -336,10 +330,14 @@ async def test_recovery_bedrock(hass: HomeAssistant) -> None:
),
):
result2 = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"], user_input=USER_INPUT
flow_id=result["flow_id"],
user_input={
CONF_TYPE: MinecraftServerType.BEDROCK_EDITION,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == USER_INPUT[CONF_ADDRESS]
assert result2["title"] == TEST_ADDRESS
assert result2["data"][CONF_ADDRESS] == TEST_ADDRESS
assert result2["data"][CONF_TYPE] == MinecraftServerType.BEDROCK_EDITION
@@ -347,14 +345,6 @@ async def test_recovery_bedrock(hass: HomeAssistant) -> None:
async def test_recovery_legacy_java(hass: HomeAssistant) -> None:
"""Test config flow recovery with a legacy Java Edition server."""
with (
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.JavaServer.async_lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.LegacyServer.async_lookup",
return_value=LegacyServer(host=TEST_HOST, port=TEST_PORT),
@@ -373,20 +363,15 @@ async def test_recovery_legacy_java(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=USER_INPUT,
user_input={
CONF_TYPE: MinecraftServerType.LEGACY_JAVA_EDITION,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
with (
patch(
"homeassistant.components.minecraft_server.api.BedrockServer.lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.JavaServer.async_lookup",
side_effect=ValueError,
),
patch(
"homeassistant.components.minecraft_server.api.LegacyServer.async_lookup",
return_value=LegacyServer(host=TEST_HOST, port=TEST_PORT),
@@ -397,9 +382,13 @@ async def test_recovery_legacy_java(hass: HomeAssistant) -> None:
),
):
result2 = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"], user_input=USER_INPUT
flow_id=result["flow_id"],
user_input={
CONF_TYPE: MinecraftServerType.LEGACY_JAVA_EDITION,
CONF_ADDRESS: TEST_ADDRESS,
},
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == USER_INPUT[CONF_ADDRESS]
assert result2["title"] == TEST_ADDRESS
assert result2["data"][CONF_ADDRESS] == TEST_ADDRESS
assert result2["data"][CONF_TYPE] == MinecraftServerType.LEGACY_JAVA_EDITION