mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 09:23:17 -04:00
Fix HTTP error code 409 in Pterodactyl by avoiding reading of utilization data if server is suspended (#181682)
This commit is contained in:
@@ -22,21 +22,29 @@ class PterodactylConnectionError(Exception):
|
||||
|
||||
|
||||
@dataclass
|
||||
class PterodactylData:
|
||||
"""Data for the Pterodactyl server."""
|
||||
class PterodactylGameServer:
|
||||
"""Pterodactyl game server."""
|
||||
|
||||
identifier: str
|
||||
is_suspended: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class PterodactylGameServerData:
|
||||
"""Data of a Pterodactyl game server."""
|
||||
|
||||
name: str
|
||||
uuid: str
|
||||
identifier: str
|
||||
state: str
|
||||
cpu_utilization: float
|
||||
cpu_utilization: float | None
|
||||
cpu_limit: int
|
||||
disk_usage: int
|
||||
disk_usage: int | None
|
||||
disk_limit: int
|
||||
memory_usage: int
|
||||
memory_usage: int | None
|
||||
memory_limit: int
|
||||
network_inbound: int
|
||||
network_outbound: int
|
||||
network_inbound: int | None
|
||||
network_outbound: int | None
|
||||
uptime: int
|
||||
|
||||
|
||||
@@ -53,7 +61,7 @@ class PterodactylAPI:
|
||||
"""Wrapper for Pterodactyl's API."""
|
||||
|
||||
pterodactyl: PterodactylClient | None
|
||||
identifiers: list[str]
|
||||
game_servers: list[PterodactylGameServer]
|
||||
|
||||
def __init__(self, hass: HomeAssistant, host: str, api_key: str) -> None:
|
||||
"""Initialize the Pterodactyl API."""
|
||||
@@ -61,7 +69,7 @@ class PterodactylAPI:
|
||||
self.host = host
|
||||
self.api_key = api_key
|
||||
self.pterodactyl = None
|
||||
self.identifiers = []
|
||||
self.game_servers = []
|
||||
|
||||
def get_game_servers(self) -> list[str]:
|
||||
"""Get all game servers."""
|
||||
@@ -89,27 +97,40 @@ class PterodactylAPI:
|
||||
raise PterodactylConnectionError(error) from error
|
||||
else:
|
||||
for game_server in game_servers:
|
||||
self.identifiers.append(game_server["attributes"]["identifier"])
|
||||
self.game_servers.append(
|
||||
PterodactylGameServer(
|
||||
identifier=game_server["attributes"]["identifier"],
|
||||
is_suspended=game_server["attributes"]["is_suspended"],
|
||||
)
|
||||
)
|
||||
|
||||
_LOGGER.debug("Identifiers of Pterodactyl servers: %s", self.identifiers)
|
||||
_LOGGER.debug("Pterodactyl game servers: %s", self.game_servers)
|
||||
|
||||
def get_server_data(self, identifier: str) -> tuple[dict, dict]:
|
||||
"""Get all data from the Pterodactyl server."""
|
||||
server = self.pterodactyl.client.servers.get_server(identifier) # type: ignore[union-attr]
|
||||
utilization = self.pterodactyl.client.servers.get_server_utilization( # type: ignore[union-attr]
|
||||
identifier
|
||||
)
|
||||
def get_server_data(
|
||||
self, game_server: PterodactylGameServer
|
||||
) -> tuple[dict, dict | None]:
|
||||
"""Get all data from the Pterodactyl game server."""
|
||||
server = self.pterodactyl.client.servers.get_server(game_server.identifier) # type: ignore[union-attr]
|
||||
|
||||
game_server.is_suspended = server["is_suspended"]
|
||||
|
||||
if not game_server.is_suspended:
|
||||
utilization = self.pterodactyl.client.servers.get_server_utilization( # type: ignore[union-attr]
|
||||
game_server.identifier
|
||||
)
|
||||
else:
|
||||
utilization = None
|
||||
|
||||
return server, utilization
|
||||
|
||||
async def async_get_data(self) -> dict[str, PterodactylData]:
|
||||
"""Update the data from all Pterodactyl servers."""
|
||||
async def async_get_data(self) -> dict[str, PterodactylGameServerData]:
|
||||
"""Update the data from all Pterodactyl game servers."""
|
||||
data = {}
|
||||
|
||||
for identifier in self.identifiers:
|
||||
for game_server in self.game_servers:
|
||||
try:
|
||||
server, utilization = await self.hass.async_add_executor_job(
|
||||
self.get_server_data, identifier
|
||||
self.get_server_data, game_server
|
||||
)
|
||||
except (BadRequestError, PterodactylApiError, ConnectionError) as error:
|
||||
raise PterodactylConnectionError(error) from error
|
||||
@@ -119,30 +140,53 @@ class PterodactylAPI:
|
||||
|
||||
raise PterodactylConnectionError(error) from error
|
||||
else:
|
||||
data[identifier] = PterodactylData(
|
||||
name=server["name"],
|
||||
uuid=server["uuid"],
|
||||
identifier=identifier,
|
||||
state=utilization["current_state"],
|
||||
cpu_utilization=utilization["resources"]["cpu_absolute"],
|
||||
cpu_limit=server["limits"]["cpu"],
|
||||
memory_usage=utilization["resources"]["memory_bytes"],
|
||||
memory_limit=server["limits"]["memory"],
|
||||
disk_usage=utilization["resources"]["disk_bytes"],
|
||||
disk_limit=server["limits"]["disk"],
|
||||
network_inbound=utilization["resources"]["network_rx_bytes"],
|
||||
network_outbound=utilization["resources"]["network_tx_bytes"],
|
||||
uptime=utilization["resources"]["uptime"],
|
||||
)
|
||||
name = server["name"]
|
||||
uuid = server["uuid"]
|
||||
identifier = game_server.identifier
|
||||
cpu_limit = server["limits"]["cpu"]
|
||||
memory_limit = server["limits"]["memory"]
|
||||
disk_limit = server["limits"]["disk"]
|
||||
|
||||
_LOGGER.debug("%s", data[identifier])
|
||||
if utilization is None:
|
||||
state = "suspended"
|
||||
cpu_utilization = None
|
||||
memory_usage = None
|
||||
disk_usage = None
|
||||
network_inbound = None
|
||||
network_outbound = None
|
||||
uptime = 0
|
||||
else:
|
||||
state = utilization["current_state"]
|
||||
cpu_utilization = utilization["resources"]["cpu_absolute"]
|
||||
memory_usage = utilization["resources"]["memory_bytes"]
|
||||
disk_usage = utilization["resources"]["disk_bytes"]
|
||||
network_inbound = utilization["resources"]["network_rx_bytes"]
|
||||
network_outbound = utilization["resources"]["network_tx_bytes"]
|
||||
uptime = utilization["resources"]["uptime"]
|
||||
|
||||
data[game_server.identifier] = PterodactylGameServerData(
|
||||
name=name,
|
||||
uuid=uuid,
|
||||
identifier=identifier,
|
||||
state=state,
|
||||
cpu_utilization=cpu_utilization,
|
||||
cpu_limit=cpu_limit,
|
||||
disk_usage=disk_usage,
|
||||
disk_limit=disk_limit,
|
||||
memory_usage=memory_usage,
|
||||
memory_limit=memory_limit,
|
||||
network_inbound=network_inbound,
|
||||
network_outbound=network_outbound,
|
||||
uptime=uptime,
|
||||
)
|
||||
_LOGGER.debug("%s", data[game_server.identifier])
|
||||
|
||||
return data
|
||||
|
||||
async def async_send_command(
|
||||
self, identifier: str, command: PterodactylCommand
|
||||
) -> None:
|
||||
"""Send a command to the Pterodactyl server."""
|
||||
"""Send a command to the Pterodactyl game server."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.pterodactyl.client.servers.send_power_action, # type: ignore[union-attr]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Binary sensor platform of the Pterodactyl integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import override
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -10,17 +12,34 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .api import PterodactylGameServer, PterodactylGameServerData
|
||||
from .coordinator import PterodactylConfigEntry, PterodactylCoordinator
|
||||
from .entity import PterodactylEntity
|
||||
|
||||
KEY_STATUS = "status"
|
||||
KEY_SUSPENDED = "suspended"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PterodactylBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Class describing Pterodactyl binary sensor entities."""
|
||||
|
||||
value_fn: Callable[[PterodactylGameServer, PterodactylGameServerData], bool]
|
||||
|
||||
|
||||
BINARY_SENSOR_DESCRIPTIONS = [
|
||||
BinarySensorEntityDescription(
|
||||
PterodactylBinarySensorEntityDescription(
|
||||
key=KEY_STATUS,
|
||||
translation_key=KEY_STATUS,
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
value_fn=lambda game_server, game_server_data: (
|
||||
game_server_data.state == "running"
|
||||
),
|
||||
),
|
||||
PterodactylBinarySensorEntityDescription(
|
||||
key=KEY_SUSPENDED,
|
||||
translation_key=KEY_SUSPENDED,
|
||||
value_fn=lambda game_server, game_server_data: game_server.is_suspended,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -38,9 +57,9 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(
|
||||
PterodactylBinarySensorEntity(
|
||||
coordinator, identifier, description, config_entry
|
||||
coordinator, game_server, description, config_entry
|
||||
)
|
||||
for identifier in coordinator.api.identifiers
|
||||
for game_server in coordinator.api.game_servers
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
@@ -51,17 +70,19 @@ class PterodactylBinarySensorEntity(PterodactylEntity, BinarySensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PterodactylCoordinator,
|
||||
identifier: str,
|
||||
description: BinarySensorEntityDescription,
|
||||
game_server: PterodactylGameServer,
|
||||
description: PterodactylBinarySensorEntityDescription,
|
||||
config_entry: PterodactylConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize binary sensor base entity."""
|
||||
super().__init__(coordinator, identifier, config_entry)
|
||||
super().__init__(coordinator, game_server, config_entry)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{self.game_server_data.uuid}_{description.key}"
|
||||
|
||||
entity_description: PterodactylBinarySensorEntityDescription
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
"""Return binary sensor state."""
|
||||
return self.game_server_data.state == "running"
|
||||
return self.entity_description.value_fn(self.game_server, self.game_server_data)
|
||||
|
||||
@@ -12,6 +12,7 @@ from .api import (
|
||||
PterodactylAuthorizationError,
|
||||
PterodactylCommand,
|
||||
PterodactylConnectionError,
|
||||
PterodactylGameServer,
|
||||
)
|
||||
from .coordinator import PterodactylConfigEntry, PterodactylCoordinator
|
||||
from .entity import PterodactylEntity
|
||||
@@ -66,8 +67,8 @@ async def async_setup_entry(
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
PterodactylButtonEntity(coordinator, identifier, description, config_entry)
|
||||
for identifier in coordinator.api.identifiers
|
||||
PterodactylButtonEntity(coordinator, game_server, description, config_entry)
|
||||
for game_server in coordinator.api.game_servers
|
||||
for description in BUTTON_DESCRIPTIONS
|
||||
)
|
||||
|
||||
@@ -80,21 +81,27 @@ class PterodactylButtonEntity(PterodactylEntity, ButtonEntity):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PterodactylCoordinator,
|
||||
identifier: str,
|
||||
game_server: PterodactylGameServer,
|
||||
description: PterodactylButtonEntityDescription,
|
||||
config_entry: PterodactylConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the button entity."""
|
||||
super().__init__(coordinator, identifier, config_entry)
|
||||
super().__init__(coordinator, game_server, config_entry)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{self.game_server_data.uuid}_{description.key}"
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return button availability."""
|
||||
return super().available and not self.game_server.is_suspended
|
||||
|
||||
@override
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
try:
|
||||
await self.coordinator.api.async_send_command(
|
||||
self.identifier, self.entity_description.command
|
||||
self.game_server.identifier, self.entity_description.command
|
||||
)
|
||||
except PterodactylConnectionError as err:
|
||||
raise HomeAssistantError(
|
||||
|
||||
@@ -14,7 +14,7 @@ from .api import (
|
||||
PterodactylAPI,
|
||||
PterodactylAuthorizationError,
|
||||
PterodactylConnectionError,
|
||||
PterodactylData,
|
||||
PterodactylGameServerData,
|
||||
)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
@@ -24,7 +24,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
type PterodactylConfigEntry = ConfigEntry[PterodactylCoordinator]
|
||||
|
||||
|
||||
class PterodactylCoordinator(DataUpdateCoordinator[dict[str, PterodactylData]]):
|
||||
class PterodactylCoordinator(
|
||||
DataUpdateCoordinator[dict[str, PterodactylGameServerData]]
|
||||
):
|
||||
"""Pterodactyl data update coordinator."""
|
||||
|
||||
config_entry: PterodactylConfigEntry
|
||||
@@ -62,7 +64,7 @@ class PterodactylCoordinator(DataUpdateCoordinator[dict[str, PterodactylData]]):
|
||||
raise ConfigEntryAuthFailed(error) from error
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[str, PterodactylData]:
|
||||
async def _async_update_data(self) -> dict[str, PterodactylGameServerData]:
|
||||
"""Get updated data from the Pterodactyl server."""
|
||||
try:
|
||||
return await self.api.async_get_data()
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.const import CONF_URL
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .api import PterodactylData
|
||||
from .api import PterodactylGameServer, PterodactylGameServerData
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PterodactylCoordinator
|
||||
|
||||
@@ -24,21 +24,21 @@ class PterodactylEntity(CoordinatorEntity[PterodactylCoordinator]):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PterodactylCoordinator,
|
||||
identifier: str,
|
||||
game_server: PterodactylGameServer,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize base entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.identifier = identifier
|
||||
self.game_server = game_server
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, identifier)},
|
||||
identifiers={(DOMAIN, game_server.identifier)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=self.game_server_data.name,
|
||||
model=self.game_server_data.name,
|
||||
model_id=self.game_server_data.uuid,
|
||||
configuration_url=str(
|
||||
URL(config_entry.data[CONF_URL]) / "server" / identifier
|
||||
URL(config_entry.data[CONF_URL]) / "server" / game_server.identifier
|
||||
),
|
||||
)
|
||||
|
||||
@@ -46,9 +46,11 @@ class PterodactylEntity(CoordinatorEntity[PterodactylCoordinator]):
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return binary sensor availability."""
|
||||
return super().available and self.identifier in self.coordinator.data
|
||||
return (
|
||||
super().available and self.game_server.identifier in self.coordinator.data
|
||||
)
|
||||
|
||||
@property
|
||||
def game_server_data(self) -> PterodactylData:
|
||||
def game_server_data(self) -> PterodactylGameServerData:
|
||||
"""Return game server data."""
|
||||
return self.coordinator.data[self.identifier]
|
||||
return self.coordinator.data[self.game_server.identifier]
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"suspended": {
|
||||
"default": "mdi:pause"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"force_stop_server": {
|
||||
"default": "mdi:flash-alert"
|
||||
|
||||
@@ -17,7 +17,12 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .coordinator import PterodactylConfigEntry, PterodactylCoordinator, PterodactylData
|
||||
from .api import PterodactylGameServer
|
||||
from .coordinator import (
|
||||
PterodactylConfigEntry,
|
||||
PterodactylCoordinator,
|
||||
PterodactylGameServerData,
|
||||
)
|
||||
from .entity import PterodactylEntity
|
||||
|
||||
KEY_CPU_UTILIZATION = "cpu_utilization"
|
||||
@@ -38,7 +43,7 @@ PARALLEL_UPDATES = 0
|
||||
class PterodactylSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing Pterodactyl sensor entities."""
|
||||
|
||||
value_fn: Callable[[PterodactylData], StateType | datetime]
|
||||
value_fn: Callable[[PterodactylGameServerData], StateType | datetime]
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS = [
|
||||
@@ -155,8 +160,8 @@ async def async_setup_entry(
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
PterodactylSensorEntity(coordinator, identifier, description, config_entry)
|
||||
for identifier in coordinator.api.identifiers
|
||||
PterodactylSensorEntity(coordinator, game_server, description, config_entry)
|
||||
for game_server in coordinator.api.game_servers
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
@@ -169,12 +174,12 @@ class PterodactylSensorEntity(PterodactylEntity, SensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PterodactylCoordinator,
|
||||
identifier: str,
|
||||
game_server: PterodactylGameServer,
|
||||
description: PterodactylSensorEntityDescription,
|
||||
config_entry: PterodactylConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize sensor base entity."""
|
||||
super().__init__(coordinator, identifier, config_entry)
|
||||
super().__init__(coordinator, game_server, config_entry)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{self.game_server_data.uuid}_{description.key}"
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"binary_sensor": {
|
||||
"status": {
|
||||
"name": "Status"
|
||||
},
|
||||
"suspended": {
|
||||
"name": "Suspended"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"server_owner": true,
|
||||
"identifier": "1",
|
||||
"internal_id": 1,
|
||||
"uuid": "1-1-1-1-1",
|
||||
"name": "Test Server 1",
|
||||
"node": "default_node",
|
||||
"is_node_under_maintenance": false,
|
||||
"sftp_details": {
|
||||
"ip": "192.168.0.1",
|
||||
"port": 2022
|
||||
},
|
||||
"description": "",
|
||||
"limits": {
|
||||
"memory": 2048,
|
||||
"swap": 1024,
|
||||
"disk": 10240,
|
||||
"io": 500,
|
||||
"cpu": 100,
|
||||
"threads": null,
|
||||
"oom_disabled": true
|
||||
},
|
||||
"invocation": "java -jar test1.jar",
|
||||
"docker_image": "test_docker_image1",
|
||||
"egg_features": ["eula", "java_version", "pid_limit"],
|
||||
"feature_limits": {
|
||||
"databases": 0,
|
||||
"allocations": 0,
|
||||
"backups": 3
|
||||
},
|
||||
"status": null,
|
||||
"is_suspended": true,
|
||||
"is_installing": false,
|
||||
"is_transferring": false,
|
||||
"relationships": {
|
||||
"allocations": {},
|
||||
"variables": {}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
"internal_id": 1,
|
||||
"uuid": "1-1-1-1-1",
|
||||
"name": "Test Server 1",
|
||||
"is_suspended": false,
|
||||
"node": "default_node",
|
||||
"description": "Description of Test Server 1",
|
||||
"limits": {
|
||||
@@ -40,6 +41,7 @@
|
||||
"internal_id": 2,
|
||||
"uuid": "2-2-2-2-2",
|
||||
"name": "Test Server 2",
|
||||
"is_suspended": false,
|
||||
"node": "default_node",
|
||||
"description": "Description of Test Server 2",
|
||||
"limits": {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"total": 1,
|
||||
"count": 1,
|
||||
"per_page": 50,
|
||||
"current_page": 1
|
||||
}
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
"object": "server",
|
||||
"attributes": {
|
||||
"server_owner": true,
|
||||
"identifier": "1",
|
||||
"internal_id": 1,
|
||||
"uuid": "1-1-1-1-1",
|
||||
"name": "Test Server 1",
|
||||
"is_suspended": true,
|
||||
"node": "default_node",
|
||||
"description": "Description of Test Server 1",
|
||||
"limits": {
|
||||
"memory": 1024,
|
||||
"swap": 512,
|
||||
"disk": 10240,
|
||||
"io": 500,
|
||||
"cpu": 50,
|
||||
"threads": null,
|
||||
"oom_disabled": true
|
||||
},
|
||||
"invocation": "java -jar test1.jar",
|
||||
"docker_image": "test_docker_image1",
|
||||
"egg_features": ["java_version"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -50,6 +50,56 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_server_1_suspended-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.test_server_1_suspended',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Suspended',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Suspended',
|
||||
'platform': 'pterodactyl',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'suspended',
|
||||
'unique_id': '1-1-1-1-1_suspended',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_server_1_suspended-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Server 1 Suspended',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.test_server_1_suspended',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_server_2_status-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
@@ -101,3 +151,53 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_server_2_suspended-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.test_server_2_suspended',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Suspended',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Suspended',
|
||||
'platform': 'pterodactyl',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'suspended',
|
||||
'unique_id': '2-2-2-2-2_suspended',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_server_2_suspended-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test Server 2 Suspended',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.test_server_2_suspended',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -5,6 +5,7 @@ from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pydactyl.responses import PaginatedResponse
|
||||
import pytest
|
||||
from requests.exceptions import ConnectionError
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
@@ -15,7 +16,12 @@ from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_fire_time_changed,
|
||||
async_load_json_object_fixture,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_pterodactyl")
|
||||
@@ -31,7 +37,7 @@ async def test_binary_sensor(
|
||||
):
|
||||
mock_config_entry = await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 2
|
||||
assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 4
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, snapshot, mock_config_entry.entry_id
|
||||
)
|
||||
@@ -50,7 +56,7 @@ async def test_binary_sensor_update(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 2
|
||||
assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 4
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_status").state
|
||||
== STATE_ON
|
||||
@@ -59,6 +65,121 @@ async def test_binary_sensor_update(
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_2_status").state
|
||||
== STATE_ON
|
||||
)
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_suspended").state
|
||||
== "off"
|
||||
)
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_2_suspended").state
|
||||
== "off"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_pterodactyl")
|
||||
async def test_binary_sensor_suspended_server(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_pterodactyl: Generator[AsyncMock],
|
||||
) -> None:
|
||||
"""Test a suspended server does not fetch utilization data."""
|
||||
server_list_data_suspended = await async_load_json_object_fixture(
|
||||
hass, "server_list_data_suspended.json", "pterodactyl"
|
||||
)
|
||||
server_1_data_suspended = await async_load_json_object_fixture(
|
||||
hass, "server_1_data_suspended.json", "pterodactyl"
|
||||
)
|
||||
|
||||
mock_pterodactyl.client.servers.list_servers.return_value = PaginatedResponse(
|
||||
mock_pterodactyl,
|
||||
"client",
|
||||
server_list_data_suspended,
|
||||
)
|
||||
server_data = {"1": server_1_data_suspended}
|
||||
mock_pterodactyl.client.servers.get_server.side_effect = lambda identifier: (
|
||||
server_data[identifier]
|
||||
)
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_status").state == "off"
|
||||
)
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_suspended").state
|
||||
== STATE_ON
|
||||
)
|
||||
mock_pterodactyl.client.servers.get_server_utilization.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_pterodactyl")
|
||||
async def test_binary_sensor_suspended_server_transition_runtime(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_pterodactyl: Generator[AsyncMock],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test suspended state can change during runtime and skips utilization when suspended."""
|
||||
server_list_data_suspended = await async_load_json_object_fixture(
|
||||
hass, "server_list_data_suspended.json", "pterodactyl"
|
||||
)
|
||||
server_1_data = await async_load_json_object_fixture(
|
||||
hass, "server_1_data.json", "pterodactyl"
|
||||
)
|
||||
server_1_data_suspended = await async_load_json_object_fixture(
|
||||
hass, "server_1_data_suspended.json", "pterodactyl"
|
||||
)
|
||||
|
||||
mock_pterodactyl.client.servers.list_servers.return_value = PaginatedResponse(
|
||||
mock_pterodactyl,
|
||||
"client",
|
||||
server_list_data_suspended,
|
||||
)
|
||||
server_data = {"1": server_1_data}
|
||||
mock_pterodactyl.client.servers.get_server.side_effect = lambda identifier: (
|
||||
server_data[identifier]
|
||||
)
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_status").state
|
||||
== STATE_ON
|
||||
)
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_suspended").state
|
||||
== "off"
|
||||
)
|
||||
|
||||
mock_pterodactyl.client.servers.get_server_utilization.reset_mock()
|
||||
server_data["1"] = server_1_data_suspended
|
||||
freezer.tick(timedelta(seconds=90))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_status").state == "off"
|
||||
)
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_suspended").state
|
||||
== STATE_ON
|
||||
)
|
||||
mock_pterodactyl.client.servers.get_server_utilization.assert_not_called()
|
||||
|
||||
mock_pterodactyl.client.servers.get_server_utilization.reset_mock()
|
||||
server_data["1"] = server_1_data
|
||||
freezer.tick(timedelta(seconds=90))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_status").state
|
||||
== STATE_ON
|
||||
)
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_suspended").state
|
||||
== "off"
|
||||
)
|
||||
mock_pterodactyl.client.servers.get_server_utilization.assert_called_once()
|
||||
|
||||
|
||||
async def test_binary_sensor_update_failure(
|
||||
@@ -78,7 +199,7 @@ async def test_binary_sensor_update_failure(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 2
|
||||
assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 4
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_status").state
|
||||
== STATE_UNAVAILABLE
|
||||
@@ -87,3 +208,11 @@ async def test_binary_sensor_update_failure(
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_2_status").state
|
||||
== STATE_UNAVAILABLE
|
||||
)
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_suspended").state
|
||||
== STATE_UNAVAILABLE
|
||||
)
|
||||
assert (
|
||||
hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_2_suspended").state
|
||||
== STATE_UNAVAILABLE
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user