diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b81f248 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +name: Release + +on: + push: + tags: + - '*' + +jobs: + release_zip_file: + name: Publish HACS zip file asset + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Compress Custom Component + run: | + cd ${{ github.workspace }}/custom_components/pterodactyl-panel + zip pterodactyl-panel.zip -r ./ + + - uses: ncipollo/release-action@v1.13.0 + with: + allowUpdates: true + generateReleaseNotes: true + artifacts: ${{ github.workspace }}/custom_components/pterodactyl-panel/pterodactyl-panel.zip \ No newline at end of file diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..7dbb9bc --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,26 @@ +name: Validate + +on: + push: + branches: + - main + pull_request: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + validate-hacs: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v4" + - name: HACS validation + uses: "hacs/action@main" + with: + category: "integration" + + validate-ha: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v4" + - uses: "home-assistant/actions/hassfest@master" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4be1f2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +virt-env + +__pycache__/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0211004 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/tjleach98/homeassistant-pterodactyl-panel/.github%2Fworkflows%2Fvalidate.yml?style=flat-square&label=validate)](https://github.com/tjleach98/homeassistant-pterodactyl-panel/actions/workflows/validate.yml) +[![GitHub Release](https://img.shields.io/github/release/tjleach98/homeassistant-pterodactyl-panel.svg?style=flat-square)](https://github.com/tjleach98/homeassistant-pterodactyl-panel/releases) +[![GitHub](https://img.shields.io/github/license/tjleach98/homeassistant-pterodactyl-panel.svg?style=flat-square)](LICENSE) +[![Downloads](https://img.shields.io/github/downloads/tjleach98/homeassistant-pterodactyl-panel/total?style=flat-square)](https://github.com/tjleach98/homeassistant-pterodactyl-panel/releases) + +# Pterodactyl Panel Home Assistant Integration +This is a basic Home Assistant integration for the [Pterodactyl Panel](https://pterodactyl.io/). It uses the `py-dactyl` library available [here](https://github.com/iamkubi/pydactyl) + +## Influence +The source code for this project is influenced by the [Proxmox VE](https://github.com/dougiteixeira/proxmoxve) integration. + +## Installation +### Manual +Place the entire `custom_components/pterodactyl-panel` folder in this repo inside the `config/custom_components/` folder of your Home Assistant instance. + +If `custom_components` doesn't exist, create it. Click [here](https://developers.home-assistant.io/docs/creating_integration_file_structure/#where-home-assistant-looks-for-integrations) for more details. + +Once the files are in place, restart Home Assistant and the integration should be available. + +### HACS +Add this repository to HACS as a custom repository. Details for this can be found [here](https://hacs.xyz/docs/faq/custom_repositories). + +## Setup +Go to Account Settings -> API Credentials -> Create API Key. + +## Currently Available Sensors +### Button +#### Server +- Start +- Stop +- Restart + +### Binary Sensor +#### Server +- Is Running +- Is Under Maintenance + +### Sensor +#### Server +- Absolute CPU Usage +- Current State +- Disk Usage +- Memory Usage +- Network Upload/Download +- Current Node +- Uptime \ No newline at end of file diff --git a/custom_components/pterodactyl-panel/__init__.py b/custom_components/pterodactyl-panel/__init__.py new file mode 100644 index 0000000..ea74720 --- /dev/null +++ b/custom_components/pterodactyl-panel/__init__.py @@ -0,0 +1,83 @@ +"""Custom integration to integrate the Pterodactyl Panel with Home Assistant.""" + +from __future__ import annotations + +import logging +from typing import Final + +from pydactyl import PterodactylClient +from pydactyl.exceptions import ClientConfigError +from pydactyl.responses import PaginatedResponse +from requests.exceptions import HTTPError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady + +from .const import DOMAIN, PTERODACTYL_ATTRIBUTES +from .coordinator import PterodactylServerCoordinator + +_LOGGER: logging.Logger = logging.getLogger(__package__) + +PLATFORMS: Final[list[str]] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] + +STARTUP_MESSAGE: Final = f"Starting setup for {DOMAIN}" + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up Pterodactyl Panel from a config entry.""" + if hass.data.get(DOMAIN) is None: + hass.data.setdefault(DOMAIN, {}) + _LOGGER.info(STARTUP_MESSAGE) + + url = config_entry.data.get(CONF_HOST) + api_key = config_entry.data.get(CONF_API_KEY) + + try: + pterodactyl_api = PterodactylClient(url, api_key) + await hass.async_add_executor_job(pterodactyl_api.client.account.get_account) + except ClientConfigError as exception: + raise ConfigEntryAuthFailed from exception + except HTTPError as exception: + if exception.response.status_code == 401: + raise ConfigEntryAuthFailed from exception + + raise ConfigEntryNotReady from exception + + coordinators: list[PterodactylServerCoordinator] = [] + + server_list_pages: PaginatedResponse = await hass.async_add_executor_job( + pterodactyl_api.client.servers.list_servers + ) + server_list_data = server_list_pages.data + + # Get all servers if more than one page. + if server_list_pages.meta['pagination']['total_pages'] > 1: + server_list_data = await hass.async_add_executor_job(server_list_pages.collect) + + servers = [server_data[PTERODACTYL_ATTRIBUTES] for server_data in server_list_data] + + for server in servers: + coordinator_server = PterodactylServerCoordinator( + hass=hass, entry=config_entry, client=pterodactyl_api, server=server + ) + await coordinator_server.async_refresh() + coordinators.append(coordinator_server) + + config_entry.runtime_data = coordinators + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload config entry.""" + await async_unload_entry(hass, entry) + await async_setup_entry(hass, entry) diff --git a/custom_components/pterodactyl-panel/binary_sensor.py b/custom_components/pterodactyl-panel/binary_sensor.py new file mode 100644 index 0000000..fbf20e0 --- /dev/null +++ b/custom_components/pterodactyl-panel/binary_sensor.py @@ -0,0 +1,64 @@ +"""Binary Sensor for the Pterodactyl Panel.""" + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Final + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import PterodactylEntity, PterodactylEntityDescription + + +@dataclass(frozen=True, kw_only=True) +class PterodactylBinarySensorEntityDescription( + PterodactylEntityDescription, BinarySensorEntityDescription +): + """Class describing Pterodactyl Panel Binary Sensors.""" + + value_fn: Callable[[str | int | float], str | int | float] = lambda value: value + + +BINARY_SENSORS: Final[list[PterodactylBinarySensorEntityDescription]] = [ + PterodactylBinarySensorEntityDescription( + key="is_node_under_maintenance", + translation_key="pterodactyl_is_node_under_maintenance", + ), + PterodactylBinarySensorEntityDescription( + key="is_running", + device_class=BinarySensorDeviceClass.RUNNING, + translation_key="pterodactyl_is_running", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Pterodactyl Panel binary sensors.""" + for coordinator in config_entry.runtime_data: + async_add_entities( + [ + PterodactylBinarySensorEntity(coordinator, config_entry, sensor) + for sensor in BINARY_SENSORS + if sensor.key in coordinator.data + ] + ) + + +class PterodactylBinarySensorEntity(PterodactylEntity, BinarySensorEntity): + """Represents a Pterodactyl Panel binary sensor.""" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + val = self.coordinator.data.get(self.entity_description.key) + return self.entity_description.value_fn(val) diff --git a/custom_components/pterodactyl-panel/button.py b/custom_components/pterodactyl-panel/button.py new file mode 100644 index 0000000..16bde14 --- /dev/null +++ b/custom_components/pterodactyl-panel/button.py @@ -0,0 +1,72 @@ +"""Button for the Pterodactyl Panel.""" + +from dataclasses import dataclass +import logging +from typing import Final + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import PterodactylEntity, PterodactylEntityDescription + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class PterodactylButtonEntityDescription( + PterodactylEntityDescription, ButtonEntityDescription +): + """Describes Pterodactyl Panel button entity.""" + + +BUTTONS: Final[list[PterodactylButtonEntityDescription]] = [ + PterodactylButtonEntityDescription( + key="server_restart", + translation_key="pterodactyl_server_restart", + ), + PterodactylButtonEntityDescription( + key="server_start", + translation_key="pterodactyl_server_start", + ), + PterodactylButtonEntityDescription( + key="server_stop", + translation_key="pterodactyl_server_stop", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Pterodactyl Panel buttons.""" + for coordinator in config_entry.runtime_data: + async_add_entities( + [ + PterodactylButtonEntity(coordinator, config_entry, button) + for button in BUTTONS + ] + ) + + +class PterodactylButtonEntity(PterodactylEntity, ButtonEntity): + """Represents a Pterodactyl Panel button.""" + + async def async_press(self) -> None: + """Handle the button press.""" + power_action = "" + match self.entity_description.key: + case "server_start": + power_action = "start" + case "server_stop": + power_action = "stop" + case "server_restart": + power_action = "restart" + case _: + raise ServiceValidationError("Button must be start, stop, or restart") + + await self.coordinator.send_power_action(power_action) diff --git a/custom_components/pterodactyl-panel/config_flow.py b/custom_components/pterodactyl-panel/config_flow.py new file mode 100644 index 0000000..80b207c --- /dev/null +++ b/custom_components/pterodactyl-panel/config_flow.py @@ -0,0 +1,116 @@ +"""Config flow for the Pterodactyl Panel integration.""" + +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any, Final + +from pydactyl import PterodactylClient +from pydactyl.exceptions import ClientConfigError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import Unauthorized + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SCHEMA_HOST_AUTH: Final = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_API_KEY): str, + } +) + +SCHEMA_REAUTH: Final = vol.Schema( + { + vol.Required(CONF_API_KEY): str + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from SCHEMA_HOST_AUTH with values provided by the user. + """ + url = data[CONF_HOST] + api_key = data[CONF_API_KEY] + + try: + pterodactyl_api = PterodactylClient(url, api_key) + await hass.async_add_executor_job(pterodactyl_api.client.account.get_account) + except ClientConfigError as exception: + raise Unauthorized from exception + + return {CONF_HOST: url, CONF_API_KEY: api_key} + + +class ConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Pterodactyl Panel.""" + + host: str + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except Unauthorized: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info[CONF_HOST]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info[CONF_HOST], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=SCHEMA_HOST_AUTH, + errors=errors + ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an authentication error.""" + self.host = entry_data[CONF_HOST] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Dialog that informs the user that reauth is required.""" + errors: dict[str, str] = {} + if user_input: + try: + user_input[CONF_HOST] = self.host + info = await validate_input(self.hass, user_input) + except Unauthorized: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info[CONF_HOST]) + self._abort_if_unique_id_mismatch(reason="wrong_account") + return self.async_update_reload_and_abort( + self._get_reauth_entry(), + data_updates={CONF_API_KEY: info[CONF_API_KEY]}, + ) + return self.async_show_form( + step_id="reauth_confirm", + data_schema=SCHEMA_REAUTH, + errors=errors, + ) diff --git a/custom_components/pterodactyl-panel/const.py b/custom_components/pterodactyl-panel/const.py new file mode 100644 index 0000000..2ed07a0 --- /dev/null +++ b/custom_components/pterodactyl-panel/const.py @@ -0,0 +1,9 @@ +"""Constants for the Pterodactyl Panel integration.""" + +DOMAIN = "pterodactyl-panel" +PROPER_NAME = "Pterodactyl Panel" + +PTERODACTYL_ATTRIBUTES = "attributes" +PTERODACTYL_ID = "identifier" +PTERODACTYL_NAME = "name" +PTERODACTYL_DOCKER_IMAGE = "docker_image" diff --git a/custom_components/pterodactyl-panel/coordinator.py b/custom_components/pterodactyl-panel/coordinator.py new file mode 100644 index 0000000..ed19524 --- /dev/null +++ b/custom_components/pterodactyl-panel/coordinator.py @@ -0,0 +1,89 @@ +"""Data update coordinator for the Pterodactyl Panel integration.""" + +from datetime import timedelta +import logging +from typing import Any, Final + +from pydactyl import PterodactylClient +from requests.exceptions import HTTPError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import PTERODACTYL_ID + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SCAN_INTERVAL: Final = timedelta(seconds=60) +RUNNING_VALUE: Final[str] = "running" + + +class PterodactylServerCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Pterodactyl Panel data update coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + client: PterodactylClient, + server: str, + entry: ConfigEntry, + ) -> None: + """Initialize the Pterodactyl Panel coordinator.""" + self.pterodactyl_api = client + self.url = entry.data[CONF_HOST] + self.server = server + + super().__init__( + hass, + _LOGGER, + name=self.url, + update_interval=DEFAULT_SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch all Pterodactyl data.""" + data = {} + server_id = self.server[PTERODACTYL_ID] + + try: + # Pull from utilization endpoint + server_utilization = await self.hass.async_add_executor_job( + self.pterodactyl_api.client.servers.get_server_utilization, server_id + ) + # Pull from server info endpoint + server_info = await self.hass.async_add_executor_job( + self.pterodactyl_api.client.servers.get_server, server_id + ) + + except HTTPError as e: + if e.response.status_code == 401: + raise ConfigEntryAuthFailed from e + + raise UpdateFailed(f"Failed to get data from {server_id}") from e + + # Add server utilization data. + data["is_running"] = server_utilization["current_state"] == RUNNING_VALUE + data["current_state"] = server_utilization["current_state"] + data["memory"] = server_utilization["resources"]["memory_bytes"] + data["cpu"] = server_utilization["resources"]["cpu_absolute"] + data["disk"] = server_utilization["resources"]["disk_bytes"] + data["network_tx"] = server_utilization["resources"]["network_tx_bytes"] + data["network_rx"] = server_utilization["resources"]["network_rx_bytes"] + data["uptime"] = server_utilization["resources"]["uptime"] + + # Add server info data. + data["node"] = server_info["node"] + data["is_node_under_maintenance"] = server_info["is_node_under_maintenance"] + + return data + + async def send_power_action(self, action: str): + """Send power action to Pterodactyl Panel api.""" + await self.hass.async_add_executor_job( + self.pterodactyl_api.client.servers.send_power_action, + self.server[PTERODACTYL_ID], + action, + ) diff --git a/custom_components/pterodactyl-panel/entity.py b/custom_components/pterodactyl-panel/entity.py new file mode 100644 index 0000000..8d42668 --- /dev/null +++ b/custom_components/pterodactyl-panel/entity.py @@ -0,0 +1,52 @@ +"""Base entity for the Pterodactyl Panel integration.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + DOMAIN, + PROPER_NAME, + PTERODACTYL_DOCKER_IMAGE, + PTERODACTYL_ID, + PTERODACTYL_NAME, +) +from .coordinator import PterodactylServerCoordinator + + +class PterodactylEntityDescription(EntityDescription): + """Describe a Pterodactyl Panel entity.""" + + +class PterodactylEntity(CoordinatorEntity): + """Base Pterodactyl Panel entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: PterodactylServerCoordinator, + entry: ConfigEntry, + description: PterodactylEntityDescription, + ) -> None: + """Initialize the Pterodactyl Panel sensor.""" + super().__init__(coordinator) + self._attr_unique_id = ( + f"{entry.entry_id}_{coordinator.server[PTERODACTYL_ID]}_{description.key}" + ) + self._attr_device_info = DeviceInfo( + entry_type=dr.DeviceEntryType.SERVICE, + configuration_url=coordinator.url, + identifiers={ + ( + DOMAIN, + f"{entry.entry_id}_server_{coordinator.server[PTERODACTYL_ID]}", + ) + }, + name=f"Server {coordinator.server[PTERODACTYL_NAME]}", + manufacturer=PROPER_NAME, + sw_version=coordinator.server[PTERODACTYL_DOCKER_IMAGE], + ) + self.entity_description = description diff --git a/custom_components/pterodactyl-panel/manifest.json b/custom_components/pterodactyl-panel/manifest.json new file mode 100644 index 0000000..392b7d4 --- /dev/null +++ b/custom_components/pterodactyl-panel/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "pterodactyl-panel", + "name": "Pterodactyl Panel", + "codeowners": [ + "@tjleach98" + ], + "config_flow": true, + "integration_type": "service", + "dependencies": [], + "documentation": "https://github.com/tjleach98/homeassistant-pterodactyl-panel", + "issue_tracker": "https://github.com/tjleach98/homeassistant-pterodactyl-panel/issues", + "iot_class": "cloud_polling", + "requirements": [ + "py-dactyl==v2.0.4" + ], + "version":"0.0.1" +} diff --git a/custom_components/pterodactyl-panel/sensor.py b/custom_components/pterodactyl-panel/sensor.py new file mode 100644 index 0000000..f7c66db --- /dev/null +++ b/custom_components/pterodactyl-panel/sensor.py @@ -0,0 +1,125 @@ +"""Sensor for the Pterodactyl Panel.""" + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Final + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, UnitOfInformation, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import PterodactylEntity, PterodactylEntityDescription + + +@dataclass +class PterodactylSensorEntityDescription( + PterodactylEntityDescription, SensorEntityDescription +): + """Describes Pterodactyl sensor entity.""" + + conversion_fn: Callable | None = None + value_fn: Callable[[str | int | float], str | int | float] = lambda value: value + + +SENSORS: Final[list[PterodactylSensorEntityDescription]] = [ + PterodactylSensorEntityDescription( + key="cpu", + translation_key="pterodactyl_cpu", + icon="mdi:cpu-64-bit", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + conversion_fn=lambda x: (x * 100) if x >= 0 else 0, + suggested_display_precision=0, + ), + PterodactylSensorEntityDescription( + key="current_state", + translation_key="pterodactyl_current_state", + ), + PterodactylSensorEntityDescription( + key="disk", + icon="mdi:harddisk", + translation_key="pterodactyl_disk", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + suggested_display_precision=2, + ), + PterodactylSensorEntityDescription( + key="memory", + icon="mdi:memory", + translation_key="pterodactyl_memory", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + PterodactylSensorEntityDescription( + key="network_rx", + icon="mdi:download-network-outline", + translation_key="pterodactyl_network_rx", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + suggested_display_precision=2, + ), + PterodactylSensorEntityDescription( + key="network_tx", + icon="mdi:upload-network-outline", + translation_key="pterodactyl_network_tx", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + suggested_display_precision=2, + ), + PterodactylSensorEntityDescription( + key="node", + translation_key="pterodactyl_node", + ), + PterodactylSensorEntityDescription( + key="uptime", + icon="mdi:memory", + translation_key="pterodactyl_uptime", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, + entity_registry_enabled_default=False, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Pterodactyl sensors.""" + for coordinator in config_entry.runtime_data: + async_add_entities( + [ + PterodactylSensorEntity(coordinator, config_entry, sensor) + for sensor in SENSORS + if sensor.key in coordinator.data + ] + ) + + +class PterodactylSensorEntity(PterodactylEntity, SensorEntity): + """Represents a Pterodactyl sensor.""" + + @property + def native_value(self) -> str | int | float: + """Return the state for this sensor.""" + val = self.coordinator.data.get(self.entity_description.key) + return self.entity_description.value_fn(val) diff --git a/custom_components/pterodactyl-panel/strings.json b/custom_components/pterodactyl-panel/strings.json new file mode 100644 index 0000000..a42bb21 --- /dev/null +++ b/custom_components/pterodactyl-panel/strings.json @@ -0,0 +1,75 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "api_key": "API key" + } + }, + "reauth_confirm": { + "description": "The API key is invalid.", + "title": "Reauthenticate Integration", + "data": { + "api_key": "API key" + } + } + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + } + }, + "entity": { + "binary_sensor": { + "pterodactyl_is_running": { + "name": "Is Running?" + }, + "pterodactyl_is_node_under_maintenance": { + "name": "Is Node Under Maintenance?" + } + }, + "sensor": { + "pterodactyl_current_state": { + "name": "Current State" + }, + "pterodactyl_node": { + "name": "Node" + }, + "pterodactyl_uptime": { + "name": "Uptime" + }, + "pterodactyl_cpu": { + "name": "CPU Absolute" + }, + "pterodactyl_memory": { + "name": "Memory Usage" + }, + "pterodactyl_disk": { + "name": "Disk Usage" + }, + "pterodactyl_network_tx": { + "name": "Upload Network Usage" + }, + "pterodactyl_network_rx": { + "name": "Download Network Usage" + } + }, + "button": { + "pterodactyl_server_start": { + "name": "Start" + }, + "pterodactyl_server_stop": { + "name": "Stop" + }, + "pterodactyl_server_restart": { + "name": "Restart" + } + } + } +} diff --git a/custom_components/pterodactyl-panel/translations/en.json b/custom_components/pterodactyl-panel/translations/en.json new file mode 100644 index 0000000..9e5a0d8 --- /dev/null +++ b/custom_components/pterodactyl-panel/translations/en.json @@ -0,0 +1,93 @@ +{ + "custom": { + "config_flow": { + "data": { + "host": "Host", + "api_key": "API key" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured_service": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + } + } + }, + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "api_key": "API key" + } + }, + "reauth_confirm": { + "description": "The API key is invalid.", + "title": "Reauthenticate Integration", + "data": { + "api_key": "API key" + } + } + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + } + }, + "entity": { + "binary_sensor": { + "pterodactyl_is_running": { + "name": "Is Running?" + }, + "pterodactyl_is_node_under_maintenance": { + "name": "Is Node Under Maintenance?" + } + }, + "sensor": { + "pterodactyl_current_state": { + "name": "Current State" + }, + "pterodactyl_node": { + "name": "Node" + }, + "pterodactyl_uptime": { + "name": "Uptime" + }, + "pterodactyl_cpu": { + "name": "CPU Absolute" + }, + "pterodactyl_memory": { + "name": "Memory Usage" + }, + "pterodactyl_disk": { + "name": "Disk Usage" + }, + "pterodactyl_network_tx": { + "name": "Upload Network Usage" + }, + "pterodactyl_network_rx": { + "name": "Download Network Usage" + } + }, + "button": { + "pterodactyl_server_start": { + "name": "Start" + }, + "pterodactyl_server_stop": { + "name": "Stop" + }, + "pterodactyl_server_restart": { + "name": "Restart" + } + } + } + } + \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..115b4ad --- /dev/null +++ b/hacs.json @@ -0,0 +1,6 @@ +{ + "name": "Pterodactyl Panel", + "render_readme": true, + "zip_release": true, + "filename": "pterodactyl-panel.zip" +} \ No newline at end of file