-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Custom UNRAID Integration for Home Assistant
- Loading branch information
MrD3y5eL
committed
Oct 15, 2024
1 parent
f6efb08
commit ec7bf17
Showing
13 changed files
with
1,358 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": [] | ||
} |
Oops, something went wrong.