Skip to content

Commit

Permalink
Custom UNRAID Integration for Home Assistant
Browse files Browse the repository at this point in the history
  • Loading branch information
MrD3y5eL committed Oct 15, 2024
1 parent f6efb08 commit ec7bf17
Show file tree
Hide file tree
Showing 13 changed files with 1,358 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.DS_Store
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,62 @@
# ha-unraid
Unraid integration for Home Assistant
# Unraid Integration for Home Assistant

This custom integration allows you to monitor and control your Unraid server from Home Assistant.

## Features

- Monitor CPU, RAM, Boot, Cache, Array Disks, and Array usage
- Monitor UPS connected to Unraid
- Control Docker containers
- Manage VMs
- Execute shell commands
- Manage user scripts

## Installation

1. Copy the `unraid` folder into your `custom_components` directory.
2. Restart Home Assistant.
3. Go to Configuration > Integrations.
4. Click the "+ ADD INTEGRATION" button.
5. Search for "Unraid" and select it.
6. Follow the configuration steps.

## Configuration

During the setup, you'll need to provide:

- Host: The IP address or hostname of your Unraid server
- Username: Your Unraid username (usually 'root')
- Password: Your Unraid password
- Port: SSH port (usually 22)
- Ping Interval: How often to check if the server is online (in seconds)
- Update Interval: How often to update sensor data (in seconds)

## Sensors

- CPU Usage
- RAM Usage
- Array Usage
- Individual Array Disks
- Uptime

## Switches

- Docker Containers: Turn on/off Docker containers
- VMs: Turn on/off Virtual Machines

## Services

- `unraid.execute_command`: Execute a shell command on the Unraid server
- `unraid.execute_in_container`: Execute a command in a Docker container
- `unraid.execute_user_script`: Execute a user script
- `unraid.stop_user_script`: Stop a running user script

## Examples

### Execute a shell command

```yaml
service: unraid.execute_command
data:
entry_id: YOUR_ENTRY_ID
command: "echo 'Hello from Home Assistant' > /boot/config/plugins/user.scripts/scripts/ha_test.sh"
94 changes: 94 additions & 0 deletions custom_components/unraid/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""The Unraid integration."""
from __future__ import annotations

import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv

from .const import DOMAIN, PLATFORMS
from .coordinator import UnraidDataUpdateCoordinator
from .unraid import UnraidAPI

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Unraid from a config entry."""
api = UnraidAPI(
host=entry.data[CONF_HOST],
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
port=entry.data[CONF_PORT],
)

try:
await api.connect()
except Exception as err:
await api.disconnect()
raise ConfigEntryNotReady from err

coordinator = UnraidDataUpdateCoordinator(hass, api, entry)
await coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator

await coordinator.start_ping_task() # Start the ping task

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
await coordinator.stop_ping_task() # Stop the ping task

if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
await coordinator.api.disconnect()

return unload_ok

def register_services(hass: HomeAssistant):
"""Register services for Unraid."""

async def execute_command(call: ServiceCall):
"""Execute a command on Unraid."""
entry_id = call.data.get("entry_id")
command = call.data.get("command")
coordinator: UnraidDataUpdateCoordinator = hass.data[DOMAIN][entry_id]
result = await coordinator.api.execute_command(command)
return {"result": result}

async def execute_in_container(call: ServiceCall):
"""Execute a command in a Docker container."""
entry_id = call.data.get("entry_id")
container = call.data.get("container")
command = call.data.get("command")
detached = call.data.get("detached", False)
coordinator: UnraidDataUpdateCoordinator = hass.data[DOMAIN][entry_id]
result = await coordinator.api.execute_in_container(container, command, detached)
return {"result": result}

async def execute_user_script(call: ServiceCall):
"""Execute a user script."""
entry_id = call.data.get("entry_id")
script_name = call.data.get("script_name")
background = call.data.get("background", False)
coordinator: UnraidDataUpdateCoordinator = hass.data[DOMAIN][entry_id]
result = await coordinator.api.execute_user_script(script_name, background)
return {"result": result}

async def stop_user_script(call: ServiceCall):
"""Stop a user script."""
entry_id = call.data.get("entry_id")
script_name = call.data.get("script_name")
coordinator: UnraidDataUpdateCoordinator = hass.data[DOMAIN][entry_id]
result = await coordinator.api.stop_user_script(script_name)
return {"result": result}

hass.services.async_register(DOMAIN, "execute_command", execute_command)
hass.services.async_register(DOMAIN, "execute_in_container", execute_in_container)
hass.services.async_register(DOMAIN, "execute_user_script", execute_user_script)
hass.services.async_register(DOMAIN, "stop_user_script", stop_user_script)
63 changes: 63 additions & 0 deletions custom_components/unraid/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Config flow for Unraid integration."""
from __future__ import annotations

import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError

from .unraid import UnraidAPI
from .const import DOMAIN, DEFAULT_PORT, DEFAULT_PING_INTERVAL, DEFAULT_CHECK_INTERVAL

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
vol.Optional("ping_interval", default=DEFAULT_PING_INTERVAL): int,
vol.Optional("check_interval", default=DEFAULT_CHECK_INTERVAL): int,
}
)

async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect."""
api = UnraidAPI(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PORT])

try:
await api.connect()
await api.disconnect()
except Exception as err:
raise CannotConnect from err

# Return info that you want to store in the config entry.
return {"title": f"Unraid Server ({data[CONF_HOST]})"}

class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Unraid."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
errors["base"] = "unknown"
else:
return self.async_create_entry(title=info["title"], data=user_input)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
9 changes: 9 additions & 0 deletions custom_components/unraid/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Constants for the Unraid integration."""
from homeassistant.const import Platform

DOMAIN = "unraid"
DEFAULT_PORT = 22
DEFAULT_PING_INTERVAL = 60
DEFAULT_CHECK_INTERVAL = 300

PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
82 changes: 82 additions & 0 deletions custom_components/unraid/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""DataUpdateCoordinator for Unraid."""
import asyncio
from datetime import timedelta
import logging

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN
from .unraid import UnraidAPI

_LOGGER = logging.getLogger(__name__)

class UnraidDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Unraid data."""

def __init__(self, hass: HomeAssistant, api: UnraidAPI, entry: ConfigEntry) -> None:
"""Initialize the data update coordinator."""
self.api = api
self.entry = entry
self.ping_interval = entry.data["ping_interval"]
self._is_online = True
self._ping_task = None

super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=entry.data["check_interval"]),
)

async def _async_update_data(self):
"""Fetch data from Unraid."""
if not self._is_online:
raise UpdateFailed("Unraid server is offline")

try:
system_stats = await self.api.get_system_stats()
docker_containers = await self.api.get_docker_containers()
vms = await self.api.get_vms()
user_scripts = await self.api.get_user_scripts()

return {
"system_stats": system_stats,
"docker_containers": docker_containers,
"vms": vms,
"user_scripts": user_scripts,
}
except Exception as err:
raise UpdateFailed(f"Error communicating with Unraid: {err}") from err

async def ping_unraid(self):
"""Ping the Unraid server to check if it's online."""
while True:
try:
await self.api.ping()
if not self._is_online:
_LOGGER.info("Unraid server is back online")
self._is_online = True
await self.async_request_refresh()
except Exception:
if self._is_online:
_LOGGER.warning("Unraid server is offline")
self._is_online = False

await asyncio.sleep(self.ping_interval)

async def start_ping_task(self):
"""Start the ping task."""
if self._ping_task is None:
self._ping_task = self.hass.async_create_task(self.ping_unraid())

async def stop_ping_task(self):
"""Stop the ping task."""
if self._ping_task is not None:
self._ping_task.cancel()
try:
await self._ping_task
except asyncio.CancelledError:
pass
self._ping_task = None
15 changes: 15 additions & 0 deletions custom_components/unraid/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"domain": "unraid",
"name": "UNRAID",
"codeowners": ["@domalab"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/domalab/ha-unraid/wiki",
"homekit": {},
"iot_class": "local_polling",
"issue_tracker": "https://github.com/domalab/ha-unraid/issues",
"requirements": [],
"ssdp": [],
"version": "0.1.0",
"zeroconf": []
}
Loading

0 comments on commit ec7bf17

Please sign in to comment.