diff --git a/homeassistant/components/pterodactyl/api.py b/homeassistant/components/pterodactyl/api.py index 2aac359a5c67..7592dc548fdf 100644 --- a/homeassistant/components/pterodactyl/api.py +++ b/homeassistant/components/pterodactyl/api.py @@ -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] diff --git a/homeassistant/components/pterodactyl/binary_sensor.py b/homeassistant/components/pterodactyl/binary_sensor.py index 67adaeea0b12..dd4db4d17951 100644 --- a/homeassistant/components/pterodactyl/binary_sensor.py +++ b/homeassistant/components/pterodactyl/binary_sensor.py @@ -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) diff --git a/homeassistant/components/pterodactyl/button.py b/homeassistant/components/pterodactyl/button.py index e650b07e4343..383079c2a9b5 100644 --- a/homeassistant/components/pterodactyl/button.py +++ b/homeassistant/components/pterodactyl/button.py @@ -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( diff --git a/homeassistant/components/pterodactyl/coordinator.py b/homeassistant/components/pterodactyl/coordinator.py index 0d7b57f2a5cc..74fac1a48236 100644 --- a/homeassistant/components/pterodactyl/coordinator.py +++ b/homeassistant/components/pterodactyl/coordinator.py @@ -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() diff --git a/homeassistant/components/pterodactyl/entity.py b/homeassistant/components/pterodactyl/entity.py index 91750aa2e511..aa3af69d8279 100644 --- a/homeassistant/components/pterodactyl/entity.py +++ b/homeassistant/components/pterodactyl/entity.py @@ -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] diff --git a/homeassistant/components/pterodactyl/icons.json b/homeassistant/components/pterodactyl/icons.json index e25e032c5205..67a57b459f83 100644 --- a/homeassistant/components/pterodactyl/icons.json +++ b/homeassistant/components/pterodactyl/icons.json @@ -1,5 +1,10 @@ { "entity": { + "binary_sensor": { + "suspended": { + "default": "mdi:pause" + } + }, "button": { "force_stop_server": { "default": "mdi:flash-alert" diff --git a/homeassistant/components/pterodactyl/sensor.py b/homeassistant/components/pterodactyl/sensor.py index 7f5a6b7b85c7..eec379f58e37 100644 --- a/homeassistant/components/pterodactyl/sensor.py +++ b/homeassistant/components/pterodactyl/sensor.py @@ -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}" diff --git a/homeassistant/components/pterodactyl/strings.json b/homeassistant/components/pterodactyl/strings.json index 134875a8d199..3a0a354ae7ca 100644 --- a/homeassistant/components/pterodactyl/strings.json +++ b/homeassistant/components/pterodactyl/strings.json @@ -35,6 +35,9 @@ "binary_sensor": { "status": { "name": "Status" + }, + "suspended": { + "name": "Suspended" } }, "button": { diff --git a/tests/components/pterodactyl/fixtures/server_1_data_suspended.json b/tests/components/pterodactyl/fixtures/server_1_data_suspended.json new file mode 100644 index 000000000000..4b0c1ac76a22 --- /dev/null +++ b/tests/components/pterodactyl/fixtures/server_1_data_suspended.json @@ -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": {} + } +} diff --git a/tests/components/pterodactyl/fixtures/server_list_data.json b/tests/components/pterodactyl/fixtures/server_list_data.json index d8796ad533e7..eecd4ad14ae5 100644 --- a/tests/components/pterodactyl/fixtures/server_list_data.json +++ b/tests/components/pterodactyl/fixtures/server_list_data.json @@ -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": { diff --git a/tests/components/pterodactyl/fixtures/server_list_data_suspended.json b/tests/components/pterodactyl/fixtures/server_list_data_suspended.json new file mode 100644 index 000000000000..0ac38861bd13 --- /dev/null +++ b/tests/components/pterodactyl/fixtures/server_list_data_suspended.json @@ -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"] + } + } + ] +} diff --git a/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr b/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr index 99c6467e70d4..a36b253fe445 100644 --- a/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr +++ b/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr @@ -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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + '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': , + '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({ + : 'Test Server 1 Suspended', + }), + 'context': , + 'entity_id': 'binary_sensor.test_server_1_suspended', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + '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': , + '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({ + : 'Test Server 2 Suspended', + }), + 'context': , + 'entity_id': 'binary_sensor.test_server_2_suspended', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/pterodactyl/test_binary_sensor.py b/tests/components/pterodactyl/test_binary_sensor.py index 4bacd30e011f..19e0d480e015 100644 --- a/tests/components/pterodactyl/test_binary_sensor.py +++ b/tests/components/pterodactyl/test_binary_sensor.py @@ -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 + )