Skip to content

Commit

Permalink
Initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
tjleach98 committed Dec 14, 2024
1 parent 0ce969d commit 1af4c44
Show file tree
Hide file tree
Showing 16 changed files with 908 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
with:
allowUpdates: true
generateReleaseNotes: true
artifacts: ${{ github.workspace }}/custom_components/pterodactyl-panel/pterodactyl-panel.zip
26 changes: 26 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -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"
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
virt-env

__pycache__/
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
83 changes: 83 additions & 0 deletions custom_components/pterodactyl-panel/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
64 changes: 64 additions & 0 deletions custom_components/pterodactyl-panel/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -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)
72 changes: 72 additions & 0 deletions custom_components/pterodactyl-panel/button.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 1af4c44

Please sign in to comment.