mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 18:27:09 +00:00
Add support for Minecraft Server Bedrock Edition (#100925)
This commit is contained in:
parent
deffa50142
commit
c6a3fa30f0
@ -745,6 +745,7 @@ omit =
|
|||||||
homeassistant/components/mill/climate.py
|
homeassistant/components/mill/climate.py
|
||||||
homeassistant/components/mill/sensor.py
|
homeassistant/components/mill/sensor.py
|
||||||
homeassistant/components/minecraft_server/__init__.py
|
homeassistant/components/minecraft_server/__init__.py
|
||||||
|
homeassistant/components/minecraft_server/api.py
|
||||||
homeassistant/components/minecraft_server/binary_sensor.py
|
homeassistant/components/minecraft_server/binary_sensor.py
|
||||||
homeassistant/components/minecraft_server/coordinator.py
|
homeassistant/components/minecraft_server/coordinator.py
|
||||||
homeassistant/components/minecraft_server/entity.py
|
homeassistant/components/minecraft_server/entity.py
|
||||||
|
@ -4,14 +4,21 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from mcstatus import JavaServer
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import ConfigEntryError
|
||||||
import homeassistant.helpers.device_registry as dr
|
import homeassistant.helpers.device_registry as dr
|
||||||
import homeassistant.helpers.entity_registry as er
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
|
||||||
|
from .api import MinecraftServer, MinecraftServerAddressError, MinecraftServerType
|
||||||
from .const import DOMAIN, KEY_LATENCY, KEY_MOTD
|
from .const import DOMAIN, KEY_LATENCY, KEY_MOTD
|
||||||
from .coordinator import MinecraftServerCoordinator
|
from .coordinator import MinecraftServerCoordinator
|
||||||
|
|
||||||
@ -23,8 +30,20 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Minecraft Server from a config entry."""
|
"""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.
|
# Create coordinator instance.
|
||||||
coordinator = MinecraftServerCoordinator(hass, entry)
|
coordinator = MinecraftServerCoordinator(hass, entry.data[CONF_NAME], api)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
# Store coordinator instance.
|
# Store coordinator instance.
|
||||||
@ -85,9 +104,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
# Migrate config entry.
|
# Migrate config entry.
|
||||||
try:
|
try:
|
||||||
address = config_data[CONF_HOST]
|
address = config_data[CONF_HOST]
|
||||||
JavaServer.lookup(address)
|
MinecraftServer(MinecraftServerType.JAVA_EDITION, address)
|
||||||
host_only_lookup_success = True
|
host_only_lookup_success = True
|
||||||
except ValueError as error:
|
except MinecraftServerAddressError as error:
|
||||||
host_only_lookup_success = False
|
host_only_lookup_success = False
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Hostname (without port) cannot be parsed (error: %s), trying again with port",
|
"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:
|
if not host_only_lookup_success:
|
||||||
try:
|
try:
|
||||||
address = f"{config_data[CONF_HOST]}:{config_data[CONF_PORT]}"
|
address = f"{config_data[CONF_HOST]}:{config_data[CONF_PORT]}"
|
||||||
JavaServer.lookup(address)
|
MinecraftServer(MinecraftServerType.JAVA_EDITION, address)
|
||||||
except ValueError as error:
|
except MinecraftServerAddressError as error:
|
||||||
_LOGGER.exception(
|
_LOGGER.exception(
|
||||||
"Can't migrate configuration entry due to error while parsing server address (error: %s), try again later",
|
"Can't migrate configuration entry due to error while parsing server address (error: %s), try again later",
|
||||||
error,
|
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.
|
# Add binary sensor entities.
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
MinecraftServerBinarySensorEntity(coordinator, description)
|
MinecraftServerBinarySensorEntity(coordinator, description, config_entry)
|
||||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -60,11 +60,12 @@ class MinecraftServerBinarySensorEntity(MinecraftServerEntity, BinarySensorEntit
|
|||||||
self,
|
self,
|
||||||
coordinator: MinecraftServerCoordinator,
|
coordinator: MinecraftServerCoordinator,
|
||||||
description: MinecraftServerBinarySensorEntityDescription,
|
description: MinecraftServerBinarySensorEntityDescription,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize binary sensor base entity."""
|
"""Initialize binary sensor base entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator, config_entry)
|
||||||
self.entity_description = description
|
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
|
self._attr_is_on = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
"""Config flow for Minecraft Server integration."""
|
"""Config flow for Minecraft Server integration."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from mcstatus import JavaServer
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow
|
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 homeassistant.data_entry_flow import FlowResult
|
||||||
|
|
||||||
|
from .api import MinecraftServer, MinecraftServerAddressError, MinecraftServerType
|
||||||
from .const import DEFAULT_NAME, DOMAIN
|
from .const import DEFAULT_NAME, DOMAIN
|
||||||
|
|
||||||
DEFAULT_ADDRESS = "localhost:25565"
|
DEFAULT_ADDRESS = "localhost:25565"
|
||||||
@ -27,11 +27,29 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input:
|
if user_input:
|
||||||
address = user_input[CONF_ADDRESS]
|
address = user_input[CONF_ADDRESS]
|
||||||
|
|
||||||
if await self._async_is_server_online(address):
|
# Prepare config entry data.
|
||||||
# No error was detected, create configuration entry.
|
config_data = {
|
||||||
config_data = {CONF_NAME: user_input[CONF_NAME], CONF_ADDRESS: address}
|
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)
|
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.
|
# Host or port invalid or server not reachable.
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
|
|
||||||
@ -59,37 +77,3 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
),
|
),
|
||||||
errors=errors,
|
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."""
|
"""The Minecraft Server integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
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.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .api import MinecraftServer, MinecraftServerConnectionError, MinecraftServerData
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=60)
|
SCAN_INTERVAL = timedelta(seconds=60)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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]):
|
class MinecraftServerCoordinator(DataUpdateCoordinator[MinecraftServerData]):
|
||||||
"""Minecraft Server data update coordinator."""
|
"""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."""
|
"""Initialize coordinator instance."""
|
||||||
config_data = config_entry.data
|
self._api = api
|
||||||
self.unique_id = config_entry.entry_id
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
name=config_data[CONF_NAME],
|
name=name,
|
||||||
logger=_LOGGER,
|
logger=_LOGGER,
|
||||||
update_interval=SCAN_INTERVAL,
|
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:
|
async def _async_update_data(self) -> MinecraftServerData:
|
||||||
"""Get server data from 3rd party library and update properties."""
|
"""Get updated data from the server."""
|
||||||
try:
|
try:
|
||||||
status_response = await self._server.async_status()
|
return await self._api.async_get_data()
|
||||||
except OSError as error:
|
except MinecraftServerConnectionError as error:
|
||||||
raise UpdateFailed(error) from 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."""
|
"""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.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .api import MinecraftServerType
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import MinecraftServerCoordinator
|
from .coordinator import MinecraftServerCoordinator
|
||||||
|
|
||||||
@ -17,13 +20,15 @@ class MinecraftServerEntity(CoordinatorEntity[MinecraftServerCoordinator]):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: MinecraftServerCoordinator,
|
coordinator: MinecraftServerCoordinator,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize base entity."""
|
"""Initialize base entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, coordinator.unique_id)},
|
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||||
manufacturer=MANUFACTURER,
|
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,
|
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.components.sensor import SensorEntity, SensorEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
|
from .api import MinecraftServerData, MinecraftServerType
|
||||||
from .const import DOMAIN, KEY_LATENCY, KEY_MOTD
|
from .const import DOMAIN, KEY_LATENCY, KEY_MOTD
|
||||||
from .coordinator import MinecraftServerCoordinator, MinecraftServerData
|
from .coordinator import MinecraftServerCoordinator
|
||||||
from .entity import MinecraftServerEntity
|
from .entity import MinecraftServerEntity
|
||||||
|
|
||||||
ATTR_PLAYERS_LIST = "players_list"
|
ATTR_PLAYERS_LIST = "players_list"
|
||||||
|
|
||||||
|
ICON_EDITION = "mdi:minecraft"
|
||||||
|
ICON_GAME_MODE = "mdi:cog"
|
||||||
|
ICON_MAP_NAME = "mdi:map"
|
||||||
ICON_LATENCY = "mdi:signal"
|
ICON_LATENCY = "mdi:signal"
|
||||||
ICON_PLAYERS_MAX = "mdi:account-multiple"
|
ICON_PLAYERS_MAX = "mdi:account-multiple"
|
||||||
ICON_PLAYERS_ONLINE = "mdi:account-multiple"
|
ICON_PLAYERS_ONLINE = "mdi:account-multiple"
|
||||||
@ -25,6 +29,9 @@ ICON_PROTOCOL_VERSION = "mdi:numeric"
|
|||||||
ICON_VERSION = "mdi:numeric"
|
ICON_VERSION = "mdi:numeric"
|
||||||
ICON_MOTD = "mdi:minecraft"
|
ICON_MOTD = "mdi:minecraft"
|
||||||
|
|
||||||
|
KEY_EDITION = "edition"
|
||||||
|
KEY_GAME_MODE = "game_mode"
|
||||||
|
KEY_MAP_NAME = "map_name"
|
||||||
KEY_PLAYERS_MAX = "players_max"
|
KEY_PLAYERS_MAX = "players_max"
|
||||||
KEY_PLAYERS_ONLINE = "players_online"
|
KEY_PLAYERS_ONLINE = "players_online"
|
||||||
KEY_PROTOCOL_VERSION = "protocol_version"
|
KEY_PROTOCOL_VERSION = "protocol_version"
|
||||||
@ -40,6 +47,7 @@ class MinecraftServerEntityDescriptionMixin:
|
|||||||
|
|
||||||
value_fn: Callable[[MinecraftServerData], StateType]
|
value_fn: Callable[[MinecraftServerData], StateType]
|
||||||
attributes_fn: Callable[[MinecraftServerData], MutableMapping[str, Any]] | None
|
attributes_fn: Callable[[MinecraftServerData], MutableMapping[str, Any]] | None
|
||||||
|
supported_server_types: list[MinecraftServerType]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -69,6 +77,10 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
icon=ICON_VERSION,
|
icon=ICON_VERSION,
|
||||||
value_fn=lambda data: data.version,
|
value_fn=lambda data: data.version,
|
||||||
attributes_fn=None,
|
attributes_fn=None,
|
||||||
|
supported_server_types=[
|
||||||
|
MinecraftServerType.JAVA_EDITION,
|
||||||
|
MinecraftServerType.BEDROCK_EDITION,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
MinecraftServerSensorEntityDescription(
|
MinecraftServerSensorEntityDescription(
|
||||||
key=KEY_PROTOCOL_VERSION,
|
key=KEY_PROTOCOL_VERSION,
|
||||||
@ -76,6 +88,10 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
icon=ICON_PROTOCOL_VERSION,
|
icon=ICON_PROTOCOL_VERSION,
|
||||||
value_fn=lambda data: data.protocol_version,
|
value_fn=lambda data: data.protocol_version,
|
||||||
attributes_fn=None,
|
attributes_fn=None,
|
||||||
|
supported_server_types=[
|
||||||
|
MinecraftServerType.JAVA_EDITION,
|
||||||
|
MinecraftServerType.BEDROCK_EDITION,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
MinecraftServerSensorEntityDescription(
|
MinecraftServerSensorEntityDescription(
|
||||||
key=KEY_PLAYERS_MAX,
|
key=KEY_PLAYERS_MAX,
|
||||||
@ -84,6 +100,10 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
icon=ICON_PLAYERS_MAX,
|
icon=ICON_PLAYERS_MAX,
|
||||||
value_fn=lambda data: data.players_max,
|
value_fn=lambda data: data.players_max,
|
||||||
attributes_fn=None,
|
attributes_fn=None,
|
||||||
|
supported_server_types=[
|
||||||
|
MinecraftServerType.JAVA_EDITION,
|
||||||
|
MinecraftServerType.BEDROCK_EDITION,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
MinecraftServerSensorEntityDescription(
|
MinecraftServerSensorEntityDescription(
|
||||||
key=KEY_LATENCY,
|
key=KEY_LATENCY,
|
||||||
@ -93,6 +113,10 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
icon=ICON_LATENCY,
|
icon=ICON_LATENCY,
|
||||||
value_fn=lambda data: data.latency,
|
value_fn=lambda data: data.latency,
|
||||||
attributes_fn=None,
|
attributes_fn=None,
|
||||||
|
supported_server_types=[
|
||||||
|
MinecraftServerType.JAVA_EDITION,
|
||||||
|
MinecraftServerType.BEDROCK_EDITION,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
MinecraftServerSensorEntityDescription(
|
MinecraftServerSensorEntityDescription(
|
||||||
key=KEY_MOTD,
|
key=KEY_MOTD,
|
||||||
@ -100,6 +124,10 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
icon=ICON_MOTD,
|
icon=ICON_MOTD,
|
||||||
value_fn=lambda data: data.motd,
|
value_fn=lambda data: data.motd,
|
||||||
attributes_fn=None,
|
attributes_fn=None,
|
||||||
|
supported_server_types=[
|
||||||
|
MinecraftServerType.JAVA_EDITION,
|
||||||
|
MinecraftServerType.BEDROCK_EDITION,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
MinecraftServerSensorEntityDescription(
|
MinecraftServerSensorEntityDescription(
|
||||||
key=KEY_PLAYERS_ONLINE,
|
key=KEY_PLAYERS_ONLINE,
|
||||||
@ -108,6 +136,40 @@ SENSOR_DESCRIPTIONS = [
|
|||||||
icon=ICON_PLAYERS_ONLINE,
|
icon=ICON_PLAYERS_ONLINE,
|
||||||
value_fn=lambda data: data.players_online,
|
value_fn=lambda data: data.players_online,
|
||||||
attributes_fn=get_extra_state_attributes_players_list,
|
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.
|
# Add sensor entities.
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
MinecraftServerSensorEntity(coordinator, description)
|
MinecraftServerSensorEntity(coordinator, description, config_entry)
|
||||||
for description in SENSOR_DESCRIPTIONS
|
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,
|
self,
|
||||||
coordinator: MinecraftServerCoordinator,
|
coordinator: MinecraftServerCoordinator,
|
||||||
description: MinecraftServerSensorEntityDescription,
|
description: MinecraftServerSensorEntityDescription,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize sensor base entity."""
|
"""Initialize sensor base entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator, config_entry)
|
||||||
self.entity_description = description
|
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()
|
self._update_properties()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"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": {
|
"entity": {
|
||||||
@ -38,6 +38,15 @@
|
|||||||
},
|
},
|
||||||
"motd": {
|
"motd": {
|
||||||
"name": "World message"
|
"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."""
|
"""Constants for Minecraft Server integration tests."""
|
||||||
from mcstatus.motd import Motd
|
from mcstatus.motd import Motd
|
||||||
from mcstatus.status_response import (
|
from mcstatus.status_response import (
|
||||||
|
BedrockStatusPlayers,
|
||||||
|
BedrockStatusResponse,
|
||||||
|
BedrockStatusVersion,
|
||||||
JavaStatusPlayers,
|
JavaStatusPlayers,
|
||||||
JavaStatusResponse,
|
JavaStatusResponse,
|
||||||
JavaStatusVersion,
|
JavaStatusVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.minecraft_server.api import MinecraftServerData
|
||||||
|
|
||||||
TEST_HOST = "mc.dummyserver.com"
|
TEST_HOST = "mc.dummyserver.com"
|
||||||
TEST_PORT = 25566
|
TEST_PORT = 25566
|
||||||
TEST_ADDRESS = f"{TEST_HOST}:{TEST_PORT}"
|
TEST_ADDRESS = f"{TEST_HOST}:{TEST_PORT}"
|
||||||
@ -32,3 +37,38 @@ TEST_JAVA_STATUS_RESPONSE = JavaStatusResponse(
|
|||||||
icon=None,
|
icon=None,
|
||||||
latency=5,
|
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 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.components.minecraft_server.const import DEFAULT_NAME, DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_USER
|
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.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
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 = {
|
USER_INPUT = {
|
||||||
CONF_NAME: DEFAULT_NAME,
|
CONF_NAME: DEFAULT_NAME,
|
||||||
@ -28,11 +30,12 @@ async def test_show_config_form(hass: HomeAssistant) -> None:
|
|||||||
assert result["step_id"] == "user"
|
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."""
|
"""Test error in case of a failed connection."""
|
||||||
with patch(
|
with patch(
|
||||||
"mcstatus.server.JavaServer.async_lookup",
|
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||||
side_effect=ValueError,
|
side_effect=[MinecraftServerAddressError, MinecraftServerAddressError],
|
||||||
|
return_value=None,
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
|
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"}
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
async def test_connection_failed(hass: HomeAssistant) -> None:
|
async def test_java_connection_failed(hass: HomeAssistant) -> None:
|
||||||
"""Test error in case of a failed connection."""
|
"""Test error in case of a failed connection to a Java Edition server."""
|
||||||
with patch(
|
with patch(
|
||||||
"mcstatus.server.JavaServer.async_lookup",
|
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||||
return_value=JavaServer(host=TEST_HOST, port=TEST_PORT),
|
side_effect=[MinecraftServerAddressError, None],
|
||||||
), patch("mcstatus.server.JavaServer.async_status", side_effect=OSError):
|
return_value=None,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.minecraft_server.api.MinecraftServer.async_is_online",
|
||||||
|
return_value=False,
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
|
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"}
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
async def test_connection_succeeded(hass: HomeAssistant) -> None:
|
async def test_bedrock_connection_failed(hass: HomeAssistant) -> None:
|
||||||
"""Test config entry in case of a successful connection with a host name."""
|
"""Test error in case of a failed connection to a Bedrock Edition server."""
|
||||||
with patch(
|
with patch(
|
||||||
"mcstatus.server.JavaServer.async_lookup",
|
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||||
return_value=JavaServer(host=TEST_HOST, port=TEST_PORT),
|
side_effect=[None, MinecraftServerAddressError],
|
||||||
|
return_value=None,
|
||||||
), patch(
|
), patch(
|
||||||
"mcstatus.server.JavaServer.async_status",
|
"homeassistant.components.minecraft_server.api.MinecraftServer.async_is_online",
|
||||||
return_value=TEST_JAVA_STATUS_RESPONSE,
|
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(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT
|
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["title"] == USER_INPUT[CONF_ADDRESS]
|
||||||
assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME]
|
assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME]
|
||||||
assert result["data"][CONF_ADDRESS] == TEST_ADDRESS
|
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."""
|
"""Tests for the Minecraft Server integration."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from mcstatus import JavaServer
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
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.minecraft_server.const import DEFAULT_NAME, DOMAIN
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_NAME, CONF_PORT
|
from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_NAME, CONF_PORT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
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
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -122,15 +121,16 @@ async def test_entry_migration(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
# Trigger migration.
|
# Trigger migration.
|
||||||
with patch(
|
with patch(
|
||||||
"mcstatus.server.JavaServer.lookup",
|
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||||
side_effect=[
|
side_effect=[
|
||||||
ValueError,
|
MinecraftServerAddressError, # async_migrate_entry
|
||||||
JavaServer(host=TEST_HOST, port=TEST_PORT),
|
None, # async_migrate_entry
|
||||||
JavaServer(host=TEST_HOST, port=TEST_PORT),
|
None, # async_setup_entry
|
||||||
],
|
],
|
||||||
|
return_value=None,
|
||||||
), patch(
|
), patch(
|
||||||
"mcstatus.server.JavaServer.async_status",
|
"homeassistant.components.minecraft_server.api.MinecraftServer.async_get_data",
|
||||||
return_value=TEST_JAVA_STATUS_RESPONSE,
|
return_value=TEST_JAVA_DATA,
|
||||||
):
|
):
|
||||||
assert await hass.config_entries.async_setup(config_entry_id)
|
assert await hass.config_entries.async_setup(config_entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -142,6 +142,7 @@ async def test_entry_migration(hass: HomeAssistant) -> None:
|
|||||||
CONF_NAME: DEFAULT_NAME,
|
CONF_NAME: DEFAULT_NAME,
|
||||||
CONF_ADDRESS: TEST_ADDRESS,
|
CONF_ADDRESS: TEST_ADDRESS,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert config_entry.version == 3
|
assert config_entry.version == 3
|
||||||
|
|
||||||
# Test migrated device entry.
|
# Test migrated device entry.
|
||||||
@ -174,14 +175,15 @@ async def test_entry_migration_host_only(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
# Trigger migration.
|
# Trigger migration.
|
||||||
with patch(
|
with patch(
|
||||||
"mcstatus.server.JavaServer.lookup",
|
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||||
side_effect=[
|
side_effect=[
|
||||||
JavaServer(host=TEST_HOST, port=TEST_PORT),
|
None, # async_migrate_entry
|
||||||
JavaServer(host=TEST_HOST, port=TEST_PORT),
|
None, # async_setup_entry
|
||||||
],
|
],
|
||||||
|
return_value=None,
|
||||||
), patch(
|
), patch(
|
||||||
"mcstatus.server.JavaServer.async_status",
|
"homeassistant.components.minecraft_server.api.MinecraftServer.async_get_data",
|
||||||
return_value=TEST_JAVA_STATUS_RESPONSE,
|
return_value=TEST_JAVA_DATA,
|
||||||
):
|
):
|
||||||
assert await hass.config_entries.async_setup(config_entry_id)
|
assert await hass.config_entries.async_setup(config_entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -205,11 +207,12 @@ async def test_entry_migration_v3_failure(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
# Trigger migration.
|
# Trigger migration.
|
||||||
with patch(
|
with patch(
|
||||||
"mcstatus.server.JavaServer.lookup",
|
"homeassistant.components.minecraft_server.api.MinecraftServer.__init__",
|
||||||
side_effect=[
|
side_effect=[
|
||||||
ValueError,
|
MinecraftServerAddressError, # async_migrate_entry
|
||||||
ValueError,
|
MinecraftServerAddressError, # async_migrate_entry
|
||||||
],
|
],
|
||||||
|
return_value=None,
|
||||||
):
|
):
|
||||||
assert not await hass.config_entries.async_setup(config_entry_id)
|
assert not await hass.config_entries.async_setup(config_entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user