Fix HTTP error code 409 in Pterodactyl by avoiding reading of utilization data if server is suspended (#181682)

This commit is contained in:
elmurato
2026-09-14 21:14:01 +02:00
committed by GitHub
parent 6c984bf444
commit 8cee738f23
13 changed files with 467 additions and 71 deletions
+82 -38
View File
@@ -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 -5
View File
@@ -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()
+10 -8
View File
@@ -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"
+11 -6
View File
@@ -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
)