├── .nvmrc ├── .gitattributes ├── custom_components └── unraid │ ├── py.typed │ ├── manifest.json │ ├── exceptions.py │ ├── sensors │ ├── __init__.py │ ├── factory.py │ └── base.py │ ├── diagnostics │ ├── __init__.py │ ├── base.py │ ├── const.py │ └── ups.py │ ├── api │ ├── disk_utils.py │ ├── __init__.py │ ├── error_handling.py │ ├── userscript_operations.py │ └── ups_operations.py │ ├── sensor.py │ ├── entity_naming.py │ ├── types.py │ ├── unraid.py │ ├── binary_sensor.py │ ├── __init__.py │ ├── const.py │ ├── quality_scale.yaml │ └── diagnostics.py ├── requirements.txt ├── .github ├── FUNDING.yml ├── workflows │ ├── hassfest.yaml │ ├── validate.yaml │ ├── stale.yml │ ├── release.yml │ └── docs.yml ├── dependabot.yml └── ISSUE_TEMPLATE │ ├── config.yml │ ├── bug-report.yml │ ├── feature-request.yml │ └── device-support-request.yml ├── scripts ├── lint ├── setup └── develop ├── hacs.json ├── .cfignore ├── config └── configuration.yaml ├── preview_docs.sh ├── .ruff.toml ├── .devcontainer.json ├── .pylintrc ├── docs ├── index.md ├── user-guide │ ├── installation.md │ ├── available-sensors.md │ ├── features.md │ ├── troubleshooting.md │ └── service-commands.md ├── development │ ├── architecture.md │ └── contributing.md └── advanced │ ├── docker-management.md │ ├── user-scripts.md │ └── vm-control.md ├── CONTRIBUTING.md ├── mkdocs.yml └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /custom_components/unraid/py.typed: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorlog==6.10.1 2 | homeassistant>=2025.6.3 3 | pip>=21.3.1 4 | ruff==0.14.5 5 | asyncssh>=2.21.0 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: domalab 4 | buy_me_a_coffee: MrD3y5eL -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | ruff format . 8 | ruff check . --fix 9 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | python3 -m pip install --requirement requirements.txt 8 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UNRAID", 3 | "render_readme": true, 4 | "country": [], 5 | "homeassistant": "2024.9.0", 6 | "hacs": "2.0.1" 7 | } -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | # Python files 2 | *.py 3 | *.pyc 4 | __pycache__/ 5 | .venv/ 6 | requirements.txt 7 | 8 | # Git files 9 | .git/ 10 | .github/ 11 | 12 | # Other files 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /config/configuration.yaml: -------------------------------------------------------------------------------- 1 | # https://www.home-assistant.io/integrations/default_config/ 2 | default_config: 3 | 4 | # https://www.home-assistant.io/integrations/homeassistant/ 5 | homeassistant: 6 | debug: true 7 | 8 | # https://www.home-assistant.io/integrations/logger/ 9 | logger: 10 | default: info 11 | logs: 12 | custom_components.unraid: debug 13 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | pull_request: 9 | schedule: 10 | - cron: '0 0 * * *' 11 | 12 | jobs: 13 | validate: 14 | runs-on: "ubuntu-latest" 15 | steps: 16 | - uses: "actions/checkout@v5" 17 | - uses: "home-assistant/actions/hassfest@master" 18 | -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: HACS Action 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | pull_request: 9 | schedule: 10 | - cron: "0 0 * * *" 11 | 12 | jobs: 13 | hacs: 14 | name: HACS Action 15 | runs-on: "ubuntu-latest" 16 | steps: 17 | - name: HACS Action 18 | uses: "hacs/action@main" 19 | with: 20 | category: "integration" 21 | -------------------------------------------------------------------------------- /custom_components/unraid/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "unraid", 3 | "name": "UNRAID", 4 | "codeowners": ["@domalab"], 5 | "config_flow": true, 6 | "dependencies": [], 7 | "documentation": "https://domalab.github.io/ha-unraid/", 8 | "homekit": {}, 9 | "iot_class": "local_polling", 10 | "issue_tracker": "https://github.com/domalab/ha-unraid/issues", 11 | "quality_scale": "silver", 12 | "requirements": [ 13 | "aiofiles>=23.2.1" 14 | ], 15 | "ssdp": [], 16 | "version": "2025.06.12", 17 | "zeroconf": [] 18 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "devcontainers" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | # Maintain dependencies for GitHub Actions 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | open-pull-requests-limit: 5 13 | 14 | # Maintain dependencies for Python 15 | - package-ecosystem: "pip" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | open-pull-requests-limit: 5 20 | -------------------------------------------------------------------------------- /scripts/develop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | # Create config dir if not present 8 | if [[ ! -d "${PWD}/config" ]]; then 9 | mkdir -p "${PWD}/config" 10 | hass --config "${PWD}/config" --script ensure_config 11 | fi 12 | 13 | # Set the path to custom_components 14 | ## This let's us have the structure we want /custom_components/unraid_management_agent 15 | ## while at the same time have Home Assistant configuration inside /config 16 | ## without resulting to symlinks. 17 | export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" 18 | 19 | # Start Home Assistant 20 | hass --config "${PWD}/config" --debug 21 | -------------------------------------------------------------------------------- /custom_components/unraid/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for the Unraid integration.""" 2 | 3 | 4 | class UnraidError(Exception): 5 | """Base exception for Unraid integration.""" 6 | 7 | 8 | class UnraidConnectionError(UnraidError): 9 | """Exception raised when connection to Unraid server fails.""" 10 | 11 | 12 | class UnraidAuthError(UnraidError): 13 | """Exception raised when authentication to Unraid server fails.""" 14 | 15 | 16 | class UnraidDataError(UnraidError): 17 | """Exception raised when data from Unraid server is invalid.""" 18 | 19 | 20 | class UnraidCommandError(UnraidError): 21 | """Exception raised when a command on Unraid server fails.""" 22 | -------------------------------------------------------------------------------- /preview_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create python virtual environment if it doesn't exist 4 | if [ ! -d "venv" ]; then 5 | echo "Creating virtual environment..." 6 | python3 -m venv venv 7 | fi 8 | 9 | # Activate virtual environment 10 | echo "Activating virtual environment..." 11 | source venv/bin/activate 12 | 13 | # Install required packages 14 | echo "Installing required packages..." 15 | pip install mkdocs==1.6.1 mkdocs-material==9.6.12 16 | 17 | # Start MkDocs development server 18 | echo "Starting MkDocs development server..." 19 | echo "Open http://localhost:8000 in your browser to preview the documentation" 20 | echo "Press Ctrl+C to stop the server" 21 | mkdocs serve -------------------------------------------------------------------------------- /custom_components/unraid/sensors/__init__.py: -------------------------------------------------------------------------------- 1 | """Sensor implementations for Unraid.""" 2 | from .base import UnraidSensorBase 3 | from .system import UnraidSystemSensors 4 | from .storage import UnraidStorageSensors 5 | from .network import UnraidNetworkSensors 6 | from .docker import UnraidDockerSensors 7 | from .ups import UnraidUPSSensors 8 | from .factory import SensorFactory 9 | from .registry import register_all_sensors 10 | 11 | __all__ = [ 12 | "UnraidSensorBase", 13 | "UnraidSystemSensors", 14 | "UnraidStorageSensors", 15 | "UnraidNetworkSensors", 16 | "UnraidDockerSensors", 17 | "UnraidUPSSensors", 18 | "SensorFactory", 19 | "register_all_sensors", 20 | ] 21 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | # The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml 2 | 3 | target-version = "py313" 4 | 5 | [lint] 6 | select = [ 7 | "ALL", 8 | ] 9 | 10 | ignore = [ 11 | "ANN401", # Dynamically typed expressions (typing.Any) are disallowed 12 | "D203", # no-blank-line-before-class (incompatible with formatter) 13 | "D212", # multi-line-summary-first-line (incompatible with formatter) 14 | "COM812", # incompatible with formatter 15 | "ISC001", # incompatible with formatter 16 | ] 17 | 18 | [lint.flake8-pytest-style] 19 | fixture-parentheses = false 20 | 21 | [lint.pyupgrade] 22 | keep-runtime-typing = true 23 | 24 | [lint.mccabe] 25 | max-complexity = 25 26 | -------------------------------------------------------------------------------- /custom_components/unraid/diagnostics/__init__.py: -------------------------------------------------------------------------------- 1 | """Diagnostic sensor implementations for Unraid.""" 2 | from .base import UnraidBinarySensorBase 3 | from .disk import UnraidArrayDiskSensor 4 | from .pool import UnraidPoolDiskSensor 5 | from .parity import UnraidParityDiskSensor, UnraidParityCheckSensor 6 | from .ups import UnraidUPSBinarySensor 7 | from .system_health import SystemHealthDiagnostics 8 | from .const import UnraidBinarySensorEntityDescription, SENSOR_DESCRIPTIONS 9 | 10 | __all__ = [ 11 | "UnraidBinarySensorBase", 12 | "UnraidArrayDiskSensor", 13 | "UnraidPoolDiskSensor", 14 | "UnraidParityDiskSensor", 15 | "UnraidParityCheckSensor", 16 | "UnraidUPSBinarySensor", 17 | "SystemHealthDiagnostics", 18 | "UnraidBinarySensorEntityDescription", 19 | "SENSOR_DESCRIPTIONS", 20 | ] 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 📚 Documentation 4 | url: https://domalab.github.io/ha-unraid/ 5 | about: Read the comprehensive documentation for the Unraid Integration 6 | - name: 💬 Community Forum 7 | url: https://community.home-assistant.io/t/unraid-integration 8 | about: Ask questions and get help from the Home Assistant community 9 | - name: 🔧 Troubleshooting Guide 10 | url: https://domalab.github.io/ha-unraid/user-guide/troubleshooting/ 11 | about: Follow the troubleshooting steps in our documentation 12 | - name: 📋 Examples & Use Cases 13 | url: https://domalab.github.io/ha-unraid/advanced/examples/ 14 | about: See examples of how to use the Unraid Integration 15 | - name: 🤖 Ask DeepWiki 16 | url: https://deepwiki.com/domalab/ha-unraid 17 | about: Get AI-powered help and answers about the integration 18 | -------------------------------------------------------------------------------- /custom_components/unraid/api/disk_utils.py: -------------------------------------------------------------------------------- 1 | """Disk validation logic for Unraid.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | def is_valid_disk_name(disk_name: str) -> bool: 9 | """Validate disk name format.""" 10 | if not disk_name: 11 | return False 12 | 13 | # Array disks (disk1, disk2, etc) 14 | if disk_name.startswith("disk") and disk_name[4:].isdigit(): 15 | return True 16 | 17 | # Cache disks (cache, cache2, cacheNVME, etc) 18 | if disk_name.startswith("cache"): 19 | return True 20 | 21 | # Known system paths to exclude 22 | invalid_names = { 23 | "user", "user0", "rootshare", "addons", 24 | "remotes", "system", "flash", "boot", 25 | "disks" 26 | } 27 | 28 | if disk_name.lower() in invalid_names: 29 | return False 30 | 31 | # Any other mounted disk that isn't in invalid_names is considered valid 32 | # This allows for custom pool names 33 | return True -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close Stale Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' # Runs daily at midnight UTC 6 | workflow_dispatch: # Allows manual triggering of the workflow 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | stale: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Close stale issues and PRs 18 | uses: actions/stale@v10 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | stale-issue-message: 'This issue has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days.' 22 | stale-pr-message: 'This PR has been marked as stale due to inactivity. If no further activity occurs, it will be closed in 7 days.' 23 | days-before-stale: 30 # Number of days before an issue/PR becomes stale 24 | days-before-close: 7 # Number of days to wait after marking as stale before closing 25 | exempt-issue-labels: 'pinned,enhancement' # Do not mark issues with these labels as stale 26 | exempt-pr-labels: 'work-in-progress' # Do not mark PRs with these labels as stale 27 | operations-per-run: 30 # Number of actions to perform per run 28 | -------------------------------------------------------------------------------- /custom_components/unraid/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Unraid API package.""" 2 | from .disk_operations import DiskOperationsMixin 3 | from .docker_operations import DockerOperationsMixin 4 | from .vm_operations import VMOperationsMixin 5 | from .system_operations import SystemOperationsMixin 6 | from .network_operations import NetworkOperationsMixin, NetworkRateSmoothingMixin 7 | from .ups_operations import UPSOperationsMixin 8 | from .userscript_operations import UserScriptOperationsMixin 9 | from .smart_operations import SmartDataManager 10 | from .disk_state import DiskStateManager, DiskState 11 | from .usb_detection import USBFlashDriveDetector, USBDeviceInfo 12 | from .disk_utils import is_valid_disk_name 13 | from .disk_mapping import get_unraid_disk_mapping, get_disk_info 14 | from .connection_manager import ConnectionManager, SSHConnection, ConnectionState, ConnectionMetrics 15 | 16 | __all__ = [ 17 | "DiskOperationsMixin", 18 | "DockerOperationsMixin", 19 | "VMOperationsMixin", 20 | "SystemOperationsMixin", 21 | "NetworkOperationsMixin", 22 | "NetworkRateSmoothingMixin", 23 | "UPSOperationsMixin", 24 | "UserScriptOperationsMixin", 25 | "SmartDataManager", 26 | "DiskStateManager", 27 | "DiskState", 28 | "USBFlashDriveDetector", 29 | "USBDeviceInfo", 30 | "is_valid_disk_name", 31 | "get_unraid_disk_mapping", 32 | "get_disk_info", 33 | "ConnectionManager", 34 | "SSHConnection", 35 | "ConnectionState", 36 | "ConnectionMetrics", 37 | ] 38 | -------------------------------------------------------------------------------- /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domalab/ha-unraid", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.13", 4 | "postCreateCommand": "scripts/setup", 5 | "forwardPorts": [ 6 | 8123 7 | ], 8 | "portsAttributes": { 9 | "8123": { 10 | "label": "Home Assistant", 11 | "onAutoForward": "notify" 12 | } 13 | }, 14 | "customizations": { 15 | "vscode": { 16 | "extensions": [ 17 | "charliermarsh.ruff", 18 | "github.vscode-pull-request-github", 19 | "ms-python.python", 20 | "ms-python.vscode-pylance", 21 | "ryanluker.vscode-coverage-gutters" 22 | ], 23 | "settings": { 24 | "files.eol": "\n", 25 | "editor.tabSize": 4, 26 | "editor.formatOnPaste": true, 27 | "editor.formatOnSave": true, 28 | "editor.formatOnType": false, 29 | "files.trimTrailingWhitespace": true, 30 | "python.analysis.typeCheckingMode": "basic", 31 | "python.analysis.autoImportCompletions": true, 32 | "python.defaultInterpreterPath": "/usr/local/bin/python", 33 | "[python]": { 34 | "editor.defaultFormatter": "charliermarsh.ruff" 35 | } 36 | } 37 | } 38 | }, 39 | "remoteUser": "vscode", 40 | "features": { 41 | "ghcr.io/devcontainers-extra/features/apt-packages:1": { 42 | "packages": [ 43 | "ffmpeg", 44 | "libturbojpeg0", 45 | "libpcap-dev" 46 | ] 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /custom_components/unraid/sensor.py: -------------------------------------------------------------------------------- 1 | """Sensor platform for Unraid.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | from homeassistant.config_entries import ConfigEntry # type: ignore 7 | from homeassistant.core import HomeAssistant # type: ignore 8 | from homeassistant.helpers.entity_platform import AddEntitiesCallback # type: ignore 9 | 10 | from .const import DOMAIN 11 | from .coordinator import UnraidDataUpdateCoordinator 12 | from .sensors.factory import SensorFactory 13 | from .sensors.registry import register_all_sensors 14 | 15 | _LOGGER = logging.getLogger(__name__) 16 | 17 | async def async_setup_entry( 18 | hass: HomeAssistant, 19 | entry: ConfigEntry, 20 | async_add_entities: AddEntitiesCallback, 21 | ) -> None: 22 | """Set up Unraid sensor based on a config entry.""" 23 | coordinator: UnraidDataUpdateCoordinator = entry.runtime_data 24 | 25 | try: 26 | # Register all sensor types 27 | register_all_sensors() 28 | 29 | # Create all sensor entities using the factory 30 | entities = SensorFactory.create_all_sensors(coordinator) 31 | 32 | if entities: 33 | async_add_entities(entities) 34 | _LOGGER.info( 35 | "Successfully added %d sensors for Unraid %s", 36 | len(entities), 37 | coordinator.hostname 38 | ) 39 | else: 40 | _LOGGER.warning( 41 | "No sensors were created for Unraid %s", 42 | coordinator.hostname 43 | ) 44 | 45 | except Exception as err: 46 | _LOGGER.error( 47 | "Error setting up Unraid sensors: %s", 48 | err, 49 | exc_info=True 50 | ) -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version to release (e.g., 2025.04.09)' 8 | required: true 9 | type: string 10 | release_notes: 11 | description: 'Release notes (markdown format)' 12 | required: true 13 | type: string 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | release: 20 | name: Create Release 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v5 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v6 29 | with: 30 | python-version: '3.11' 31 | 32 | - name: Update version in manifest.json 33 | run: | 34 | sed -i 's/"version": "[^"]*"/"version": "${{ github.event.inputs.version }}"/' custom_components/unraid/manifest.json 35 | 36 | - name: Commit version changes 37 | run: | 38 | git config --local user.email "action@github.com" 39 | git config --local user.name "GitHub Action" 40 | git add custom_components/unraid/manifest.json 41 | git commit -m "Bump version to ${{ github.event.inputs.version }}" 42 | git tag -a v${{ github.event.inputs.version }} -m "Release v${{ github.event.inputs.version }}" 43 | git push origin main --tags 44 | 45 | - name: Create Release 46 | uses: softprops/action-gh-release@v2 47 | with: 48 | tag_name: v${{ github.event.inputs.version }} 49 | name: Release v${{ github.event.inputs.version }} 50 | body: | 51 | # Release ${{ github.event.inputs.version }} 52 | 53 | ${{ github.event.inputs.release_notes }} 54 | draft: false 55 | prerelease: false 56 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'docs/**' 9 | - 'mkdocs.yml' 10 | - '.github/workflows/docs.yml' 11 | pull_request: 12 | branches: 13 | - main 14 | paths: 15 | - 'docs/**' 16 | - 'mkdocs.yml' 17 | - '.github/workflows/docs.yml' 18 | workflow_dispatch: # Allow manual triggering 19 | 20 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 21 | permissions: 22 | contents: read 23 | pages: write 24 | id-token: write 25 | 26 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 27 | concurrency: 28 | group: "pages" 29 | cancel-in-progress: false 30 | 31 | jobs: 32 | build: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v5 37 | with: 38 | fetch-depth: 0 39 | 40 | - name: Setup Pages 41 | uses: actions/configure-pages@v5 42 | 43 | - name: Set up Python 44 | uses: actions/setup-python@v6 45 | with: 46 | python-version: '3.10' 47 | 48 | - name: Install dependencies 49 | run: | 50 | python -m pip install --upgrade pip 51 | pip install mkdocs==1.6.1 mkdocs-material==9.6.12 52 | 53 | - name: Build documentation 54 | run: | 55 | mkdocs build 56 | touch site/.nojekyll 57 | 58 | - name: Upload artifact 59 | uses: actions/upload-pages-artifact@v4 60 | with: 61 | path: site 62 | 63 | deploy: 64 | environment: 65 | name: github-pages 66 | url: ${{ steps.deployment.outputs.page_url }} 67 | runs-on: ubuntu-latest 68 | needs: build 69 | if: github.event_name != 'pull_request' 70 | steps: 71 | - name: Deploy to GitHub Pages 72 | id: deployment 73 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | # Ignore Home Assistant modules so that missing dependency errors are not raised 3 | ignored-modules=homeassistant,homeassistant.* 4 | # A comma-separated list of file extensions that should be checked 5 | extension-pkg-whitelist=*.py 6 | # Add files or directories to the blacklist 7 | ignore=CVS,tests 8 | # Use multiple processes to speed up Pylint 9 | jobs=4 10 | 11 | [MESSAGES CONTROL] 12 | # Disable specific messages 13 | disable= 14 | duplicate-code, 15 | missing-module-docstring, 16 | missing-class-docstring, 17 | missing-function-docstring, 18 | too-many-locals, 19 | too-many-branches, 20 | too-many-statements, 21 | too-many-arguments, 22 | too-many-instance-attributes, 23 | too-few-public-methods, 24 | broad-exception-caught, 25 | import-error, 26 | no-name-in-module, 27 | wrong-import-position, 28 | wrong-import-order, 29 | line-too-long, 30 | import-self, 31 | trailing-whitespace, 32 | missing-final-newline, 33 | too-many-nested-blocks, 34 | too-many-return-statements, 35 | too-many-lines, 36 | unused-import, 37 | unused-variable, 38 | unnecessary-pass, 39 | no-else-return, 40 | logging-fstring-interpolation, 41 | consider-using-in, 42 | consider-using-get, 43 | chained-comparison, 44 | arguments-renamed, 45 | overridden-final-method, 46 | too-many-ancestors, 47 | unexpected-keyword-arg, 48 | abstract-method, 49 | consider-using-dict-items, 50 | too-many-positional-arguments, 51 | redefined-outer-name, 52 | reimported, 53 | import-outside-toplevel, 54 | raise-missing-from, 55 | unused-argument, 56 | no-else-raise, 57 | no-else-continue, 58 | bad-except-order, 59 | f-string-without-interpolation, 60 | attribute-defined-outside-init, 61 | superfluous-parens, 62 | unsubscriptable-object, 63 | protected-access 64 | 65 | [FORMAT] 66 | # Maximum number of characters on a single line 67 | max-line-length=120 68 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Unraid Integration for Home Assistant 2 | 3 | This custom integration allows you to monitor and control your Unraid server from Home Assistant. Unraid is a popular NAS (Network Attached Storage) operating system that provides flexible storage, virtualization, and application support. 4 | 5 | ## Features 6 | 7 | - Monitor CPU, RAM, Boot, Cache, Array Disks, and Array usage 8 | - Monitor CPU and Motherboard temperature 9 | - Monitor System Fans 10 | - Monitor UPS Connected 11 | - Control Docker containers 12 | - Control VMs 13 | - Execute shell commands 14 | - Buttons for user scripts 15 | - Manage user scripts 16 | - Automatic repair flows for common issues 17 | - Advanced config flow validation 18 | - Comprehensive diagnostics 19 | 20 | ## Getting Started 21 | 22 | The Unraid Integration is easy to set up and provides immediate value. To get started, visit the [Installation Guide](user-guide/installation.md) for step-by-step instructions. 23 | 24 | ## About 25 | 26 | This integration was developed to bridge the gap between Home Assistant and Unraid, allowing for seamless monitoring and control of your Unraid server within your home automation environment. It uses SSH to securely communicate with your Unraid server, providing reliable access to system information and controls. 27 | 28 | ## Project Status 29 | 30 | The integration is actively maintained and regularly updated with new features and improvements. It is designed to be robust and reliable, with comprehensive error handling and diagnostics capabilities. 31 | 32 | ## Quick Links 33 | 34 | - [Installation Guide](user-guide/installation.md) 35 | - [Feature Overview](user-guide/features.md) 36 | - [Troubleshooting](user-guide/troubleshooting.md) 37 | - [Examples and Use Cases](advanced/examples.md) 38 | - [User Scripts](advanced/user-scripts.md) 39 | - [Docker Management](advanced/docker-management.md) 40 | - [VM Control](advanced/vm-control.md) 41 | - [GitHub Repository](https://github.com/domalab/ha-unraid) 42 | 43 | ## Support 44 | 45 | If you encounter any issues or have questions about the integration, please check the documentation or open an issue on our [GitHub repository](https://github.com/domalab/ha-unraid/issues). -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | Contributing to this project should be as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | 10 | ## Github is used for everything 11 | 12 | Github is used to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | Pull requests are the best way to propose changes to the codebase. 15 | 16 | 1. Fork the repo and create your branch from `main`. 17 | 2. If you've changed something, update the documentation. 18 | 3. Make sure your code lints (using `scripts/lint`). 19 | 4. Test you contribution. 20 | 5. Issue that pull request! 21 | 22 | ## Any contributions you make will be under the MIT Software License 23 | 24 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 25 | 26 | ## Report bugs using Github's [issues](../../issues) 27 | 28 | GitHub issues are used to track public bugs. 29 | Report a bug by [opening a new issue](../../issues/new/choose); it's that easy! 30 | 31 | ## Write bug reports with detail, background, and sample code 32 | 33 | **Great Bug Reports** tend to have: 34 | 35 | - A quick summary and/or background 36 | - Steps to reproduce 37 | - Be specific! 38 | - Give sample code if you can. 39 | - What you expected would happen 40 | - What actually happens 41 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 42 | 43 | People *love* thorough bug reports. I'm not even kidding. 44 | 45 | ## Use a Consistent Coding Style 46 | 47 | Use [black](https://github.com/ambv/black) to make sure the code follows the style. 48 | 49 | ## Test your code modification 50 | 51 | This custom component provides a Home Assistant integration for Unraid servers. 52 | 53 | It comes with development environment in a container, easy to launch 54 | if you use Visual Studio Code. With this container you will have a stand alone 55 | Home Assistant instance running and already configured with the included 56 | [`configuration.yaml`](./config/configuration.yaml) 57 | file. 58 | 59 | ## License 60 | 61 | By contributing, you agree that your contributions will be licensed under its MIT License. 62 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Unraid Integration for Home Assistant 2 | site_url: https://domalab.github.io/ha-unraid/ 3 | site_description: Documentation for the Home Assistant Unraid integration 4 | site_author: domalab 5 | repo_url: https://github.com/domalab/ha-unraid 6 | repo_name: domalab/ha-unraid 7 | edit_uri: edit/main/docs/ 8 | 9 | # Theme settings 10 | theme: 11 | name: material 12 | language: en 13 | palette: 14 | primary: indigo 15 | accent: deep orange 16 | font: 17 | text: Roboto 18 | code: Roboto Mono 19 | features: 20 | - navigation.instant 21 | - navigation.tracking 22 | - navigation.tabs 23 | - navigation.top 24 | - search.highlight 25 | - search.share 26 | - content.code.copy 27 | icon: 28 | repo: fontawesome/brands/github 29 | 30 | # Extensions 31 | markdown_extensions: 32 | - admonition 33 | - attr_list 34 | - codehilite 35 | - footnotes 36 | - md_in_html 37 | - pymdownx.emoji 38 | - pymdownx.highlight: 39 | anchor_linenums: true 40 | - pymdownx.superfences: 41 | custom_fences: 42 | - name: mermaid 43 | class: mermaid 44 | - pymdownx.tabbed: 45 | alternate_style: true 46 | - toc: 47 | permalink: true 48 | 49 | # Navigation 50 | nav: 51 | - Home: index.md 52 | - User Guide: 53 | - Installation: user-guide/installation.md 54 | - Features: user-guide/features.md 55 | - Available Sensors: user-guide/available-sensors.md 56 | - Service Commands: user-guide/service-commands.md 57 | - Troubleshooting: user-guide/troubleshooting.md 58 | - Advanced Usage: 59 | - Examples: advanced/examples.md 60 | - User Scripts: advanced/user-scripts.md 61 | - Docker Management: advanced/docker-management.md 62 | - VM Control: advanced/vm-control.md 63 | - Development: 64 | - Getting Started: development/index.md 65 | - Architecture: development/architecture.md 66 | - API: development/api.md 67 | - Entity Development: development/entities.md 68 | - Contributing: development/contributing.md 69 | 70 | # Plugins 71 | plugins: 72 | - search 73 | 74 | # Extra 75 | extra: 76 | social: 77 | - icon: fontawesome/brands/github 78 | link: https://github.com/domalab/ha-unraid 79 | analytics: 80 | provider: google 81 | property: G-XXXXXXXXXX # Replace with actual Google Analytics ID when deployed 82 | generator: false 83 | 84 | # Copyright 85 | copyright: Copyright © 2024 domalab 86 | 87 | # No extra JavaScript needed for now 88 | -------------------------------------------------------------------------------- /custom_components/unraid/entity_naming.py: -------------------------------------------------------------------------------- 1 | """Entity naming utilities for Unraid integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from dataclasses import dataclass 6 | 7 | _LOGGER = logging.getLogger(__name__) 8 | 9 | @dataclass 10 | class EntityNaming: 11 | """Helper class for entity naming.""" 12 | domain: str 13 | hostname: str 14 | component: str 15 | entity_format: int = 2 # Default to new format (version 2) 16 | 17 | def __init__(self, domain: str, hostname: str, component: str, entity_format: int = 2) -> None: 18 | """Initialize the entity naming helper.""" 19 | self.domain = domain 20 | self.hostname = hostname.lower() # Ensure hostname is lowercase for entity IDs 21 | self.component = component 22 | self.entity_format = entity_format 23 | 24 | def get_entity_name(self, entity_id: str, component_type: str = None) -> str: 25 | """Get the entity name based on the configured format. 26 | 27 | Always uses the format: component_entity_id (without domain or hostname) 28 | This is used for display names within entities, not for entity IDs. 29 | """ 30 | component = component_type or self.component 31 | return f"{component}_{entity_id}" 32 | 33 | def get_entity_id(self, entity_id: str, component_type: str = None) -> str: 34 | """Get the entity ID based on the configured format. 35 | 36 | Uses format: unraid_hostname_component_name 37 | Ensures no duplication of hostname or component in the entity_id 38 | """ 39 | # Clean the entity_id to avoid duplication 40 | clean_entity_id = entity_id 41 | 42 | # Remove hostname from entity_id if it exists 43 | hostname = self.hostname.lower() 44 | entity_id_lower = clean_entity_id.lower() 45 | 46 | # Check if entity_id starts with hostname (case insensitive) 47 | if entity_id_lower.startswith(f"{hostname}_"): 48 | # Get the part after the hostname_ 49 | clean_entity_id = clean_entity_id[len(hostname) + 1:] 50 | 51 | # Remove 'unraid_' prefix if it exists 52 | if clean_entity_id.lower().startswith("unraid_"): 53 | clean_entity_id = clean_entity_id[7:] 54 | 55 | # Format the entity ID - include hostname to avoid conflicts with multiple servers 56 | return f"{self.domain}_{hostname}_{clean_entity_id}" 57 | 58 | def clean_hostname(self) -> str: 59 | """Get a clean version of the hostname for display purposes.""" 60 | return self.hostname.replace('_', ' ').title() 61 | -------------------------------------------------------------------------------- /custom_components/unraid/types.py: -------------------------------------------------------------------------------- 1 | """Type definitions for Unraid integration.""" 2 | from __future__ import annotations 3 | 4 | from typing import Dict, List, Any, Optional, TypedDict, Literal 5 | 6 | 7 | class SystemStatsDict(TypedDict, total=False): 8 | """Type for system stats data.""" 9 | cpu_usage: float 10 | memory_usage: Dict[str, Any] 11 | uptime: int 12 | temperature_data: Dict[str, Any] 13 | array_usage: Dict[str, Any] 14 | cache_usage: Dict[str, Any] 15 | individual_disks: List[Dict[str, Any]] 16 | network_stats: Dict[str, Dict[str, Any]] 17 | ups_info: Dict[str, Any] 18 | load_average: List[float] 19 | cpu_model: str 20 | cpu_cores: int 21 | cpu_frequency: float 22 | 23 | 24 | class DiskInfoDict(TypedDict, total=False): 25 | """Type for disk information.""" 26 | name: str 27 | mount_point: str 28 | total: int 29 | used: int 30 | free: int 31 | percentage: float 32 | state: str 33 | smart_status: str 34 | temperature: Optional[int] 35 | device: str 36 | filesystem: str 37 | 38 | 39 | class NetworkStatsDict(TypedDict, total=False): 40 | """Type for network statistics.""" 41 | rx_bytes: int 42 | tx_bytes: int 43 | rx_rate: float 44 | tx_rate: float 45 | connected: bool 46 | speed: str 47 | duplex: str 48 | 49 | 50 | class DockerContainerDict(TypedDict, total=False): 51 | """Type for Docker container information.""" 52 | id: str 53 | name: str 54 | state: str 55 | status: str 56 | image: str 57 | autostart: bool 58 | 59 | 60 | class VMDict(TypedDict, total=False): 61 | """Type for VM information.""" 62 | name: str 63 | state: str 64 | cpus: int 65 | memory: int 66 | autostart: bool 67 | 68 | 69 | class UserScriptDict(TypedDict, total=False): 70 | """Type for user script information.""" 71 | id: str 72 | name: str 73 | description: str 74 | 75 | 76 | class UPSInfoDict(TypedDict, total=False): 77 | """Type for UPS information.""" 78 | STATUS: str 79 | BCHARGE: str 80 | LOADPCT: str 81 | TIMELEFT: str 82 | NOMPOWER: str 83 | BATTV: str 84 | LINEV: str 85 | MODEL: str 86 | FIRMWARE: str 87 | SERIALNO: str 88 | 89 | 90 | class ParityInfoDict(TypedDict, total=False): 91 | """Type for parity information.""" 92 | status: str 93 | progress: int 94 | speed: str 95 | errors: int 96 | last_check: str 97 | next_check: str 98 | duration: str 99 | last_status: str 100 | last_speed: str 101 | 102 | 103 | class UnraidDataDict(TypedDict, total=False): 104 | """Type for Unraid data.""" 105 | system_stats: SystemStatsDict 106 | docker_containers: List[DockerContainerDict] 107 | vms: List[VMDict] 108 | user_scripts: List[UserScriptDict] 109 | parity_info: ParityInfoDict 110 | smart_data: Dict[str, Dict[str, Any]] 111 | disk_mappings: Dict[str, Any] 112 | 113 | 114 | # Connection types 115 | ConnectionStateType = Literal["connected", "connecting", "disconnected", "error"] 116 | 117 | 118 | # Service action types 119 | ServiceActionType = Literal["start", "stop", "restart", "pause", "unpause"] 120 | 121 | 122 | # Entity state types 123 | EntityStateType = Literal["on", "off", "unavailable", "unknown"] 124 | -------------------------------------------------------------------------------- /custom_components/unraid/diagnostics/base.py: -------------------------------------------------------------------------------- 1 | """Base binary sensor implementations for Unraid.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | from homeassistant.components.binary_sensor import ( # type: ignore 7 | BinarySensorEntity, 8 | ) 9 | from homeassistant.core import callback # type: ignore 10 | from homeassistant.helpers.update_coordinator import CoordinatorEntity # type: ignore 11 | from homeassistant.helpers.entity import DeviceInfo # type: ignore 12 | 13 | from ..const import DOMAIN 14 | from .const import UnraidBinarySensorEntityDescription 15 | from ..entity_naming import EntityNaming 16 | from ..coordinator import UnraidDataUpdateCoordinator 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | class UnraidBinarySensorBase(CoordinatorEntity, BinarySensorEntity): 21 | """Base class for Unraid binary sensors.""" 22 | 23 | entity_description: UnraidBinarySensorEntityDescription 24 | 25 | def __init__( 26 | self, 27 | coordinator: UnraidDataUpdateCoordinator, 28 | description: UnraidBinarySensorEntityDescription, 29 | ) -> None: 30 | """Initialize the binary sensor.""" 31 | super().__init__(coordinator) 32 | self.entity_description = description 33 | self._attr_has_entity_name = True 34 | 35 | # Initialize entity naming 36 | naming = EntityNaming( 37 | domain=DOMAIN, 38 | hostname=coordinator.hostname, 39 | component=description.key.split('_')[0] # Component is first part of key 40 | ) 41 | 42 | self._attr_unique_id = naming.get_entity_id(description.key) 43 | self._attr_name = f"{description.name}" 44 | 45 | _LOGGER.debug( 46 | "Binary Sensor initialized | unique_id: %s | name: %s | description.key: %s", 47 | self._attr_unique_id, 48 | self._attr_name, 49 | description.key 50 | ) 51 | 52 | @property 53 | def device_info(self) -> DeviceInfo: 54 | """Return device information.""" 55 | return DeviceInfo( 56 | identifiers={(DOMAIN, self.coordinator.entry.entry_id)}, 57 | name=f"{self.coordinator.hostname.title()}", 58 | manufacturer="Lime Technology", 59 | model="Unraid Server", 60 | ) 61 | 62 | @property 63 | def is_on(self) -> bool | None: 64 | """Return true if the binary sensor is on.""" 65 | try: 66 | return self.entity_description.value_fn(self.coordinator.data) 67 | except KeyError as err: 68 | _LOGGER.debug( 69 | "Missing key in data for sensor %s: %s", 70 | self.entity_description.key, 71 | err 72 | ) 73 | return None 74 | except TypeError as err: 75 | _LOGGER.debug( 76 | "Type error processing sensor %s: %s", 77 | self.entity_description.key, 78 | err 79 | ) 80 | return None 81 | except AttributeError as err: 82 | _LOGGER.debug( 83 | "Attribute error for sensor %s: %s", 84 | self.entity_description.key, 85 | err 86 | ) 87 | return None 88 | 89 | @property 90 | def available(self) -> bool: 91 | """Return if entity is available.""" 92 | return self.coordinator.last_update_success 93 | 94 | @callback 95 | def _handle_coordinator_update(self) -> None: 96 | """Handle updated data from the coordinator.""" 97 | self.async_write_ha_state() 98 | -------------------------------------------------------------------------------- /docs/user-guide/installation.md: -------------------------------------------------------------------------------- 1 | # Installation Guide 2 | 3 | This page guides you through the installation and initial configuration of the Unraid Integration for Home Assistant. 4 | 5 | ## Prerequisites 6 | 7 | Before you begin, ensure: 8 | 9 | 1. You have Home Assistant installed and running 10 | 2. Your Unraid server is operational and accessible on your network 11 | 3. **SSH is enabled on your Unraid server** (this is disabled by default) 12 | 13 | !!! warning "SSH must be enabled" 14 | SSH is disabled by default in Unraid. You need to enable it in Settings → Management Access before using this integration. 15 | 16 | ## Installation Methods 17 | 18 | There are two ways to install the Unraid Integration: 19 | 20 | ### HACS (Recommended) 21 | 22 | The easiest way to install the integration is through HACS (Home Assistant Community Store): 23 | 24 | 1. Ensure HACS is installed in your Home Assistant instance 25 | 2. Go to HACS → Integrations → + Explore & Add Repositories 26 | 3. Search for "Unraid" 27 | 4. Click on "Unraid Integration" in the search results 28 | 5. Click "Download" 29 | 6. Restart Home Assistant 30 | 31 | Alternatively, you can use this button to add the repository directly: 32 | 33 | [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=domalab&repository=ha-unraid&category=integration) 34 | 35 | ### Manual Installation 36 | 37 | If you prefer to install the integration manually: 38 | 39 | 1. Download the latest release from the [GitHub repository](https://github.com/domalab/ha-unraid) 40 | 2. Extract the `unraid` folder from the archive 41 | 3. Copy the `unraid` folder to your Home Assistant `/config/custom_components/` directory 42 | 4. Restart Home Assistant 43 | 44 | ## Configuration 45 | 46 | Once the integration is installed, you need to add and configure it: 47 | 48 | 1. Go to Home Assistant → Settings → Devices & Services 49 | 2. Click the "+ ADD INTEGRATION" button 50 | 3. Search for "Unraid" and select it 51 | 4. Fill in the configuration form: 52 | - **Host**: The IP address or hostname of your Unraid server 53 | - **Username**: Your Unraid username (usually 'root') 54 | - **Password**: Your Unraid password 55 | - **Port**: SSH port (usually 22) 56 | - **General Update Interval**: How often to update non-disk sensors (1-60 minutes, default: 5) 57 | - **Disk Update Interval**: How often to update disk information (1-24 hours, default: 1) 58 | 5. Click "Submit" 59 | 60 | !!! tip "Update intervals" 61 | Setting lower intervals will provide more up-to-date information but may increase system load. For most users, the default values are a good balance. 62 | 63 | ## Verifying the Installation 64 | 65 | After completing the configuration, you should see the Unraid integration in your Home Assistant instance: 66 | 67 | 1. Go to Settings → Devices & Services 68 | 2. Find the Unraid integration in the list 69 | 3. You should see your Unraid server as a device 70 | 4. Explore the available entities under the device 71 | 72 | ## Troubleshooting 73 | 74 | If you encounter issues during installation or configuration: 75 | 76 | 1. Ensure SSH is enabled on your Unraid server 77 | 2. Verify your username and password are correct 78 | 3. Check that the hostname/IP address is accessible from your Home Assistant instance 79 | 4. Make sure the SSH port (usually 22) is not blocked by a firewall 80 | 5. Check the Home Assistant logs for error messages 81 | 82 | For more troubleshooting tips, see the [Troubleshooting](troubleshooting.md) page. 83 | 84 | ## Next Steps 85 | 86 | Once installation is complete, you can: 87 | 88 | - Explore the [Features](features.md) of the integration 89 | - Set up [Docker Container Management](../advanced/docker-management.md) 90 | - Configure [VM Control](../advanced/vm-control.md) 91 | - Check out [Example Automations](../advanced/examples.md) -------------------------------------------------------------------------------- /custom_components/unraid/diagnostics/const.py: -------------------------------------------------------------------------------- 1 | """Constants for Unraid diagnostic sensors.""" 2 | from __future__ import annotations 3 | 4 | from dataclasses import dataclass, field 5 | from typing import Callable, Any 6 | from enum import Enum 7 | 8 | from homeassistant.components.binary_sensor import ( # type: ignore 9 | BinarySensorDeviceClass, 10 | BinarySensorEntityDescription, 11 | ) 12 | from homeassistant.const import EntityCategory # type: ignore 13 | 14 | # Parity Check Status Constants 15 | PARITY_STATUS_IDLE = "Success" 16 | PARITY_STATUS_UNKNOWN = "Unknown" 17 | PARITY_STATUS_CHECKING = "Running" 18 | 19 | # Parity History Date Formats 20 | PARITY_HISTORY_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" 21 | PARITY_TIME_FORMAT = "%H:%M" 22 | PARITY_FULL_DATE_FORMAT = "%b %d %Y %H:%M" 23 | 24 | # Default Parity Attributes 25 | DEFAULT_PARITY_ATTRIBUTES = { 26 | "status": PARITY_STATUS_IDLE, 27 | "progress": 0, 28 | "speed": "N/A", 29 | "errors": 0, 30 | "last_check": "N/A", 31 | "next_check": "Unknown", 32 | "duration": "N/A", 33 | "last_status": "N/A" 34 | } 35 | 36 | # Speed Units 37 | class SpeedUnit(Enum): 38 | """Speed units with their multipliers.""" 39 | BYTES = (1, "B") 40 | KILOBYTES = (1024, "KB") 41 | MEGABYTES = (1024 * 1024, "MB") 42 | GIGABYTES = (1024 * 1024 * 1024, "GB") 43 | 44 | # Decimal Units 45 | DECIMAL_KILOBYTES = (1000, "kB") 46 | DECIMAL_MEGABYTES = (1_000_000, "MB") 47 | DECIMAL_GIGABYTES = (1_000_000_000, "GB") 48 | 49 | def __init__(self, multiplier: int, symbol: str): 50 | self.multiplier = multiplier 51 | self.symbol = symbol 52 | 53 | @staticmethod 54 | def from_symbol(symbol: str): 55 | """Retrieve SpeedUnit based on symbol.""" 56 | symbol = symbol.upper() 57 | for unit in SpeedUnit: 58 | if unit.symbol == symbol: 59 | return unit 60 | raise ValueError(f"Unknown speed unit: {symbol}") 61 | 62 | @dataclass 63 | class UnraidBinarySensorEntityDescription(BinarySensorEntityDescription): 64 | """Describes Unraid binary sensor entity.""" 65 | 66 | key: str 67 | name: str | None = None 68 | device_class: BinarySensorDeviceClass | None = None 69 | entity_category: EntityCategory | None = None 70 | icon: str | None = None 71 | value_fn: Callable[[dict[str, Any]], bool | None] = field(default=lambda x: None) 72 | has_warning_threshold: bool = False 73 | warning_threshold: float | None = None 74 | 75 | SENSOR_DESCRIPTIONS: tuple[UnraidBinarySensorEntityDescription, ...] = ( 76 | UnraidBinarySensorEntityDescription( 77 | key="ssh_connectivity", 78 | name="Server Connection", 79 | device_class=BinarySensorDeviceClass.CONNECTIVITY, 80 | entity_category=EntityCategory.DIAGNOSTIC, 81 | value_fn=lambda data: data.get("system_stats") is not None, 82 | icon="mdi:server-network", 83 | ), 84 | UnraidBinarySensorEntityDescription( 85 | key="docker_service", 86 | name="Docker Service", 87 | device_class=BinarySensorDeviceClass.RUNNING, 88 | entity_category=EntityCategory.DIAGNOSTIC, 89 | value_fn=lambda data: bool(data.get("docker_containers")), 90 | icon="mdi:docker", 91 | ), 92 | UnraidBinarySensorEntityDescription( 93 | key="vm_service", 94 | name="VM Service", 95 | device_class=BinarySensorDeviceClass.RUNNING, 96 | entity_category=EntityCategory.DIAGNOSTIC, 97 | value_fn=lambda data: bool(data.get("vms")), 98 | icon="mdi:desktop-tower", 99 | ), 100 | ) 101 | 102 | __all__ = [ 103 | "UnraidBinarySensorEntityDescription", 104 | "SENSOR_DESCRIPTIONS", 105 | "SpeedUnit", 106 | "PARITY_STATUS_IDLE", 107 | "PARITY_STATUS_UNKNOWN", 108 | "PARITY_STATUS_CHECKING", 109 | "PARITY_HISTORY_DATE_FORMAT", 110 | "PARITY_TIME_FORMAT", 111 | "PARITY_FULL_DATE_FORMAT", 112 | "DEFAULT_PARITY_ATTRIBUTES", 113 | ] 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | # Distribution / packaging 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | share/python-wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | # PyInstaller 28 | # Usually these files are written by a PyInstaller build script 29 | *.manifest 30 | *.spec 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | # Unit test / coverage reports 35 | htmlcov/ 36 | .tox/ 37 | .nox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | *.cover 44 | *.py,cover 45 | .hypothesis/ 46 | .pytest_cache/ 47 | cover/ 48 | # Translations 49 | *.mo 50 | *.pot 51 | # Django stuff: 52 | *.log 53 | local_settings.py 54 | db.sqlite3 55 | db.sqlite3-journal 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | # Scrapy stuff: 60 | .scrapy 61 | # Sphinx documentation 62 | docs/_build/ 63 | # PyBuilder 64 | .pybuilder/ 65 | target/ 66 | # Jupyter Notebook 67 | .ipynb_checkpoints 68 | # IPython 69 | profile_default/ 70 | ipython_config.py 71 | # pyenv 72 | # For a library or package, you might want to ignore these files since the code is 73 | # intended to run in multiple environments; otherwise, check them in: 74 | .python-version 75 | # pipenv 76 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 77 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 78 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 79 | # install all needed dependencies. 80 | #Pipfile.lock 81 | # poetry 82 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 83 | # This is especially recommended for binary packages to ensure reproducibility, and is more 84 | # commonly ignored for libraries. 85 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 86 | #poetry.lock 87 | # pdm 88 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 89 | #pdm.lock 90 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 91 | # in version control. 92 | # https://pdm.fming.dev/#use-with-ide 93 | .pdm.toml 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 95 | __pypackages__/ 96 | # Celery stuff 97 | celerybeat-schedule 98 | celerybeat.pid 99 | # SageMath parsed files 100 | *.sage.py 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | # Rope project settings 113 | .ropeproject 114 | # mkdocs documentation 115 | /site 116 | # mypy 117 | .mypy_cache/ 118 | .dmypy.json 119 | dmypy.json 120 | # Pyre type checker 121 | .pyre/ 122 | # pytype static type analyzer 123 | .pytype/ 124 | # Cython debug symbols 125 | cython_debug/ 126 | # PyCharm 127 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 128 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 129 | # and can be added to the global gitignore or merged into this file. For a more nuclear 130 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 131 | #.idea/ 132 | # VS Code 133 | .vscode/ 134 | *.code-workspace 135 | # Local History for Visual Studio Code 136 | .history/ 137 | custom_components/.DS_Store 138 | .gitignore 139 | .DS_Store 140 | .json 141 | 142 | # artifacts 143 | __pycache__ 144 | .pytest* 145 | *.egg-info 146 | */build/* 147 | */dist/* 148 | 149 | 150 | # misc 151 | .coverage 152 | .vscode 153 | coverage.xml 154 | .ruff_cache 155 | 156 | 157 | # Home Assistant configuration 158 | config/* 159 | !config/configuration.yaml -------------------------------------------------------------------------------- /custom_components/unraid/unraid.py: -------------------------------------------------------------------------------- 1 | """API client for Unraid.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Optional 6 | 7 | import asyncssh # type: ignore 8 | 9 | from .api.connection_manager import ConnectionManager 10 | from .api.network_operations import NetworkOperationsMixin 11 | from .api.disk_operations import DiskOperationsMixin 12 | from .api.docker_operations import DockerOperationsMixin 13 | from .api.vm_operations import VMOperationsMixin 14 | from .api.system_operations import SystemOperationsMixin 15 | from .api.ups_operations import UPSOperationsMixin 16 | from .api.userscript_operations import UserScriptOperationsMixin 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | class UnraidAPI( 21 | NetworkOperationsMixin, 22 | DiskOperationsMixin, 23 | DockerOperationsMixin, 24 | VMOperationsMixin, 25 | SystemOperationsMixin, 26 | UPSOperationsMixin, 27 | UserScriptOperationsMixin 28 | ): 29 | """API client for interacting with Unraid servers.""" 30 | 31 | def __init__(self, host: str, username: str, password: str, port: int = 22) -> None: 32 | """Initialize the Unraid API client.""" 33 | 34 | # Initialize Network Operations 35 | NetworkOperationsMixin.__init__(self) 36 | 37 | # Initialize other mixins 38 | DiskOperationsMixin.__init__(self) 39 | DockerOperationsMixin.__init__(self) 40 | VMOperationsMixin.__init__(self) 41 | SystemOperationsMixin.__init__(self) 42 | UPSOperationsMixin.__init__(self) 43 | UserScriptOperationsMixin.__init__(self) 44 | 45 | # Set up network ops reference 46 | if isinstance(self, SystemOperationsMixin): 47 | self.set_network_ops(self) 48 | 49 | self.host = host 50 | self.username = username 51 | self.password = password 52 | self.port = port 53 | 54 | # Use ConnectionManager instead of direct connection 55 | self.connection_manager = ConnectionManager() 56 | self.connect_timeout = 30 57 | self.command_timeout = 60 58 | self._in_context = False 59 | self._setup_done = False 60 | 61 | async def ensure_connection(self) -> None: 62 | """Ensure that the connection manager is initialized.""" 63 | if not self._setup_done: 64 | await self.connection_manager.initialize( 65 | self.host, 66 | self.username, 67 | self.password, 68 | self.port 69 | ) 70 | self._setup_done = True 71 | 72 | async def execute_command( 73 | self, 74 | command: str, 75 | timeout: Optional[int] = None 76 | ) -> asyncssh.SSHCompletedProcess: 77 | """Execute a command on the Unraid server using the connection pool.""" 78 | await self.ensure_connection() 79 | 80 | if timeout is None: 81 | timeout = self.command_timeout 82 | 83 | try: 84 | result = await self.connection_manager.execute_command( 85 | command, 86 | timeout=timeout 87 | ) 88 | return result 89 | 90 | except Exception as err: 91 | _LOGGER.error("Command failed: %s", err) 92 | raise 93 | 94 | async def disconnect(self) -> None: 95 | """Disconnect from the Unraid server.""" 96 | if self._setup_done: 97 | await self.connection_manager.shutdown() 98 | self._setup_done = False 99 | 100 | async def ping(self) -> bool: 101 | """Check if the Unraid server is accessible via SSH.""" 102 | try: 103 | await self.ensure_connection() 104 | return await self.connection_manager.health_check() 105 | except Exception as err: 106 | _LOGGER.error("Failed to ping Unraid server at %s: %s", self.host, err) 107 | return False 108 | 109 | async def __aenter__(self) -> 'UnraidAPI': 110 | """Enter async context.""" 111 | self._in_context = True 112 | await self.ensure_connection() 113 | return self 114 | 115 | async def __aexit__(self, *_) -> None: 116 | """Exit async context.""" 117 | self._in_context = False 118 | # We don't disconnect here to maintain the connection pool 119 | -------------------------------------------------------------------------------- /custom_components/unraid/sensors/factory.py: -------------------------------------------------------------------------------- 1 | """Sensor factory for Unraid integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Dict, List, Type, Any, Callable, Optional, Set 6 | 7 | from homeassistant.helpers.entity import Entity 8 | 9 | from ..coordinator import UnraidDataUpdateCoordinator 10 | from .base import UnraidSensorBase 11 | from .const import UnraidSensorEntityDescription 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | class SensorFactory: 17 | """Factory class for creating Unraid sensors.""" 18 | 19 | _sensor_types: Dict[str, Type[UnraidSensorBase]] = {} 20 | _sensor_creators: Dict[str, Callable[[UnraidDataUpdateCoordinator, Any], List[Entity]]] = {} 21 | _sensor_groups: Dict[str, Set[str]] = {} 22 | 23 | @classmethod 24 | def register_sensor_type(cls, sensor_type: str, sensor_class: Type[UnraidSensorBase]) -> None: 25 | """Register a sensor type with the factory.""" 26 | cls._sensor_types[sensor_type] = sensor_class 27 | _LOGGER.debug("Registered sensor type: %s", sensor_type) 28 | 29 | @classmethod 30 | def register_sensor_creator( 31 | cls, 32 | creator_id: str, 33 | creator_fn: Callable[[UnraidDataUpdateCoordinator, Any], List[Entity]], 34 | group: str = "default" 35 | ) -> None: 36 | """Register a sensor creator function with the factory.""" 37 | cls._sensor_creators[creator_id] = creator_fn 38 | 39 | # Add to group 40 | if group not in cls._sensor_groups: 41 | cls._sensor_groups[group] = set() 42 | cls._sensor_groups[group].add(creator_id) 43 | 44 | _LOGGER.debug("Registered sensor creator: %s in group: %s", creator_id, group) 45 | 46 | @classmethod 47 | def create_sensor( 48 | cls, 49 | sensor_type: str, 50 | coordinator: UnraidDataUpdateCoordinator, 51 | description: UnraidSensorEntityDescription, 52 | **kwargs: Any 53 | ) -> Optional[UnraidSensorBase]: 54 | """Create a sensor of the specified type.""" 55 | if sensor_type not in cls._sensor_types: 56 | _LOGGER.error("Unknown sensor type: %s", sensor_type) 57 | return None 58 | 59 | try: 60 | sensor_class = cls._sensor_types[sensor_type] 61 | return sensor_class(coordinator, description, **kwargs) 62 | except Exception as err: 63 | _LOGGER.error("Error creating sensor of type %s: %s", sensor_type, err) 64 | return None 65 | 66 | @classmethod 67 | def create_sensors_by_group( 68 | cls, 69 | coordinator: UnraidDataUpdateCoordinator, 70 | group: str = "default" 71 | ) -> List[Entity]: 72 | """Create all sensors in a specific group.""" 73 | entities = [] 74 | 75 | if group not in cls._sensor_groups: 76 | _LOGGER.warning("Unknown sensor group: %s", group) 77 | return entities 78 | 79 | for creator_id in cls._sensor_groups[group]: 80 | if creator_id not in cls._sensor_creators: 81 | _LOGGER.warning("Missing creator function for: %s", creator_id) 82 | continue 83 | 84 | try: 85 | creator_fn = cls._sensor_creators[creator_id] 86 | new_entities = creator_fn(coordinator, None) 87 | if new_entities: 88 | entities.extend(new_entities) 89 | _LOGGER.debug( 90 | "Created %d sensors with creator: %s", 91 | len(new_entities), 92 | creator_id 93 | ) 94 | except Exception as err: 95 | _LOGGER.error("Error creating sensors with creator %s: %s", creator_id, err) 96 | 97 | return entities 98 | 99 | @classmethod 100 | def create_all_sensors(cls, coordinator: UnraidDataUpdateCoordinator) -> List[Entity]: 101 | """Create all registered sensors.""" 102 | entities = [] 103 | 104 | for creator_id, creator_fn in cls._sensor_creators.items(): 105 | try: 106 | new_entities = creator_fn(coordinator, None) 107 | if new_entities: 108 | entities.extend(new_entities) 109 | _LOGGER.debug( 110 | "Created %d sensors with creator: %s", 111 | len(new_entities), 112 | creator_id 113 | ) 114 | except Exception as err: 115 | _LOGGER.error("Error creating sensors with creator %s: %s", creator_id, err) 116 | 117 | return entities 118 | -------------------------------------------------------------------------------- /docs/user-guide/available-sensors.md: -------------------------------------------------------------------------------- 1 | # Available Sensors 2 | 3 | This page provides detailed information about all the sensors available in the Unraid Integration for Home Assistant. 4 | 5 | ## System Sensors 6 | 7 | ### CPU Sensors 8 | 9 | | Sensor | Entity ID | Description | Unit | 10 | |--------|-----------|-------------|------| 11 | | CPU Usage | `sensor.unraid_cpu_usage` | Current CPU utilization percentage | % | 12 | 13 | ### Memory Sensors 14 | 15 | | Sensor | Entity ID | Description | Unit | 16 | |--------|-----------|-------------|------| 17 | | RAM Usage | `sensor.unraid_ram_usage` | Current RAM usage | GB | 18 | | Memory Usage | `sensor.unraid_memory_usage` | Current RAM usage percentage | % | 19 | 20 | ### System Status Sensors 21 | 22 | | Sensor | Entity ID | Description | Unit | 23 | |--------|-----------|-------------|------| 24 | | Array Status | `sensor.unraid_array_status` | Current status of the array | - | 25 | | Uptime | `sensor.unraid_uptime` | How long the system has been running | Hours | 26 | 27 | ### Temperature Sensors 28 | 29 | | Sensor | Entity ID | Description | Unit | 30 | |--------|-----------|-------------|------| 31 | | CPU Temperature | `sensor.unraid_cpu_temp` | Current CPU temperature | °C | 32 | | Motherboard Temperature | `sensor.unraid_motherboard_temp` | Current motherboard temperature | °C | 33 | 34 | ### Storage Sensors 35 | 36 | | Sensor | Entity ID | Description | Unit | 37 | |--------|-----------|-------------|------| 38 | | Docker VDisk | `sensor.unraid_docker_vdisk` | Docker virtual disk usage | % | 39 | | Log File System | `sensor.unraid_log_filesystem` | Log file system usage | % | 40 | | Boot Usage | `sensor.unraid_boot_usage` | Boot partition usage | % | 41 | 42 | ### Fan Sensors 43 | 44 | Fan sensors are dynamically created based on the available hardware: 45 | 46 | | Sensor | Entity ID | Description | Unit | 47 | |--------|-----------|-------------|------| 48 | | Fan Speed | `sensor.unraid_fan_[id]` | Fan speed for the fan with id | RPM | 49 | 50 | ## Storage Sensors 51 | 52 | ### Array Sensor 53 | 54 | | Sensor | Entity ID | Description | Unit | 55 | |--------|-----------|-------------|------| 56 | | Array | `sensor.unraid_array` | Array usage and information | % | 57 | 58 | ### Individual Disk Sensors 59 | 60 | For each disk in your Unraid array (only spinning drives), the following sensor will be available: 61 | 62 | | Sensor | Entity ID | Description | Unit | 63 | |--------|-----------|-------------|------| 64 | | Disk | `sensor.unraid_disk_[name]` | Usage and information for the disk | % | 65 | 66 | ### Pool Sensors 67 | 68 | For each cache pool in your Unraid system: 69 | 70 | | Sensor | Entity ID | Description | Unit | 71 | |--------|-----------|-------------|------| 72 | | Pool | `sensor.unraid_pool_[name]` | Usage and information for the pool | % | 73 | 74 | ## Network Sensors 75 | 76 | For each network interface that's connected and meets criteria: 77 | 78 | | Sensor | Entity ID | Description | Unit | 79 | |--------|-----------|-------------|------| 80 | | Network Inbound | `sensor.unraid_network_[interface]_inbound` | Inbound traffic rate for the interface | MB/s | 81 | | Network Outbound | `sensor.unraid_network_[interface]_outbound` | Outbound traffic rate for the interface | MB/s | 82 | 83 | ## UPS Sensors 84 | 85 | If you have a UPS connected to your Unraid server and NOMPOWER attribute is available: 86 | 87 | | Sensor | Entity ID | Description | Unit | 88 | |--------|-----------|-------------|------| 89 | | UPS Server Power | `sensor.unraid_ups_server_power` | Current power consumption | W | 90 | 91 | ## Binary Sensors 92 | 93 | The integration provides several binary sensors: 94 | 95 | | Binary Sensor | Entity ID | Description | 96 | |---------------|-----------|-------------| 97 | | Server Connection | `binary_sensor.unraid_server_connection` | Whether the server is reachable | 98 | | Docker Service | `binary_sensor.unraid_docker_service` | Whether the Docker service is running | 99 | | VM Service | `binary_sensor.unraid_vm_service` | Whether the VM service is running | 100 | | UPS | `binary_sensor.unraid_ups` | Whether the UPS is online (if configured) | 101 | | Parity Check | `binary_sensor.unraid_parity_check` | Whether a parity check is running | 102 | | Parity Disk | `binary_sensor.unraid_parity_disk` | Health status of the parity disk | 103 | 104 | ### Individual Disk Health Sensors 105 | 106 | For each disk in the array and pool: 107 | 108 | | Binary Sensor | Entity ID | Description | 109 | |---------------|-----------|-------------| 110 | | Array Disk Health | `binary_sensor.unraid_disk_[name]` | Health status of the array disk | 111 | | Pool Disk Health | `binary_sensor.unraid_pool_[name]` | Health status of the pool disk | 112 | 113 | ## Using Sensors in Automations 114 | 115 | These sensors can be used in automations to trigger actions based on system conditions. For example: 116 | 117 | ```yaml 118 | automation: 119 | - alias: "Low Disk Space Warning" 120 | trigger: 121 | - platform: numeric_state 122 | entity_id: sensor.unraid_array 123 | above: 85 124 | action: 125 | - service: notify.mobile_app 126 | data: 127 | title: "Unraid Disk Space Warning" 128 | message: "Your Unraid array is over 85% full. Consider freeing up some space." 129 | ``` 130 | 131 | See the [Examples](../advanced/examples.md) page for more automation ideas using these sensors. -------------------------------------------------------------------------------- /docs/user-guide/features.md: -------------------------------------------------------------------------------- 1 | # Features Overview 2 | 3 | The Unraid Integration for Home Assistant provides comprehensive monitoring and control capabilities for your Unraid server. This page outlines the main features and functionality of the integration. 4 | 5 | ## System Monitoring 6 | 7 | ### CPU and Memory Sensors 8 | 9 | The integration provides detailed information about your Unraid server's CPU and memory usage: 10 | 11 | - **CPU Usage**: Shows the current CPU utilization percentage 12 | - **Memory Usage**: Displays the current RAM usage percentage 13 | - **CPU Load**: Shows 1, 5, and 15-minute load averages 14 | 15 | ### Temperature Sensors 16 | 17 | Monitor various temperature readings from your Unraid server: 18 | 19 | - **CPU Temperature**: Shows the temperature of your processor 20 | - **Motherboard Temperature**: Displays the temperature of your motherboard 21 | - **Component-specific Temperatures**: Where available, shows temperatures for other components 22 | 23 | ### System Fans 24 | 25 | Monitor the speed of system fans: 26 | 27 | - **Fan RPM**: Shows the rotation speed of various fans in your system 28 | - **Fan Status**: Indicates whether fans are operational 29 | 30 | ### Storage Monitoring 31 | 32 | Comprehensive monitoring of your Unraid storage arrays and disks: 33 | 34 | - **Array Usage**: Shows the overall array usage percentage 35 | - **Cache Usage**: Displays the usage of cache drives 36 | - **Individual Disk Usage**: Provides usage information for each disk in the array 37 | - **Disk Health**: Displays disk health information (where available) 38 | 39 | ### Uptime and System Info 40 | 41 | Basic system information and status indicators: 42 | 43 | - **Uptime**: Shows how long the Unraid server has been running 44 | - **Version**: Displays the Unraid OS version 45 | - **Array Status**: Indicates whether the array is started or stopped 46 | 47 | ### UPS Monitoring 48 | 49 | If you have a UPS connected to your Unraid server: 50 | 51 | - **UPS Status**: Online, on battery, or other status 52 | - **Battery Level**: Current battery charge percentage 53 | - **Estimated Runtime**: Time remaining on battery power 54 | - **Input Voltage**: Current input voltage 55 | - **Load Percentage**: UPS load percentage 56 | - **Power Consumption**: Current power consumption (where supported) 57 | 58 | ## Control Features 59 | 60 | ### Docker Container Management 61 | 62 | Comprehensive Docker container control capabilities: 63 | 64 | - **Container Status**: Monitor whether containers are running or stopped 65 | - **Container Switches**: Start and stop containers directly from Home Assistant 66 | - **Advanced Controls**: Pause, resume, and restart containers 67 | - **Command Execution**: Run commands inside containers 68 | 69 | ### Virtual Machine Control 70 | 71 | Complete VM management capabilities: 72 | 73 | - **VM Status**: Monitor whether VMs are running, stopped, or paused 74 | - **VM Switches**: Start and stop VMs directly from Home Assistant 75 | - **Advanced Controls**: Pause, resume, hibernate, restart, and force stop VMs 76 | 77 | ### Command Execution 78 | 79 | Execute commands directly on your Unraid server: 80 | 81 | - **Shell Commands**: Run any terminal command on the server 82 | - **User Scripts**: Execute user-created scripts 83 | - **Background Execution**: Run commands in the background 84 | 85 | ## System Control 86 | 87 | Control your Unraid system directly from Home Assistant: 88 | 89 | - **System Reboot**: Safely reboot your Unraid server 90 | - **System Shutdown**: Safely shut down your Unraid server 91 | - **Array Stop**: Safely stop the Unraid array 92 | 93 | ## Automation Capabilities 94 | 95 | Create powerful automations using the Unraid integration: 96 | 97 | - **Event-based Actions**: Trigger actions based on Unraid system events 98 | - **Scheduled Tasks**: Schedule regular tasks on your Unraid server 99 | - **Conditional Logic**: Create complex automations based on server state 100 | 101 | ## Diagnostics 102 | 103 | Comprehensive diagnostic information for troubleshooting: 104 | 105 | - **SSH Connectivity**: Validate SSH connection status 106 | - **Disk Health**: Check for potential disk issues 107 | - **Service Status**: Monitor status of Docker and VM services 108 | - **UPS Diagnostics**: Detailed UPS information 109 | - **Parity Check Status**: Monitor parity check operations 110 | 111 | ## Repair Flows 112 | 113 | Automatic detection and guidance for common issues: 114 | 115 | - **Connection Issues**: Help resolving connectivity problems 116 | - **Authentication Problems**: Guidance for fixing authentication issues 117 | - **Disk Health Issues**: Alerts for potential disk failures 118 | - **Array Problems**: Notifications about array issues 119 | - **Parity Check Failures**: Alerts about parity check failures 120 | 121 | ## Available Services 122 | 123 | The integration provides several services you can call from automations: 124 | 125 | - **unraid.execute_command**: Run a shell command on the Unraid server 126 | - **unraid.execute_in_container**: Run a command inside a Docker container 127 | - **unraid.execute_user_script**: Execute a user script 128 | - **unraid.stop_user_script**: Stop a running user script 129 | - **unraid.system_reboot**: Reboot the Unraid server 130 | - **unraid.system_shutdown**: Shut down the Unraid server 131 | - **unraid.array_stop**: Safely stop the Unraid array 132 | - **unraid.docker_pause**: Pause a Docker container 133 | - **unraid.docker_resume**: Resume a paused Docker container 134 | - **unraid.docker_restart**: Restart a Docker container 135 | - **unraid.vm_pause**: Pause a virtual machine 136 | - **unraid.vm_resume**: Resume a paused virtual machine 137 | - **unraid.vm_restart**: Restart a virtual machine 138 | - **unraid.vm_hibernate**: Hibernate a virtual machine 139 | - **unraid.vm_force_stop**: Force stop a virtual machine -------------------------------------------------------------------------------- /docs/development/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture Overview 2 | 3 | This document provides a detailed overview of the Unraid integration's architecture, explaining how the various components work together. 4 | 5 | ## High-Level Architecture 6 | 7 | The Unraid integration follows a layered architecture pattern: 8 | 9 | *[Diagram: High-level architecture showing the layered components of the Unraid integration, including Home Assistant layer, API layer, and Unraid server layer]* 10 | 11 | **Note:** A detailed architecture diagram will be added in a future update. 12 | 13 | ## Key Components 14 | 15 | ### Home Assistant Integration Layer 16 | 17 | This is the top layer that integrates with Home Assistant's framework: 18 | 19 | 1. **Config Flow** (`config_flow.py`): 20 | - Handles setup and configuration of the integration 21 | - Validates connection settings 22 | - Manages integration options 23 | 24 | 2. **Data Update Coordinator** (`coordinator.py`): 25 | - Central component managing all data updates 26 | - Implements intelligent update scheduling 27 | - Provides data to all entities 28 | - Manages caching and state preservation 29 | 30 | 3. **Entity Platforms**: 31 | - **Sensors** (`sensor.py`, `sensors/`): Read-only data points 32 | - **Binary Sensors** (`binary_sensor.py`, `diagnostics/`): Boolean state sensors 33 | - **Switches** (`switch.py`): Toggleable controls 34 | - **Buttons** (`button.py`): Action triggers 35 | 36 | 4. **Services** (`services.py`): 37 | - Provides Home Assistant services for performing actions 38 | - Handles command execution and parameter validation 39 | 40 | ### API Layer 41 | 42 | The middle layer that handles communication with the Unraid server: 43 | 44 | 1. **API Client** (`unraid.py`): 45 | - Main client class combining functionality from mixins 46 | - Provides consistent interface for all operations 47 | 48 | 2. **Connection Manager** (`api/connection_manager.py`): 49 | - Manages SSH connections with connection pooling 50 | - Implements circuit breaking and retry logic 51 | - Provides fault tolerance and health monitoring 52 | 53 | 3. **Cache Manager** (`api/cache_manager.py`): 54 | - Optimizes performance by caching data 55 | - Implements TTL-based cache invalidation 56 | - Prioritizes data based on importance 57 | 58 | 4. **API Modules**: 59 | - **System Operations** (`api/system_operations.py`): System information 60 | - **Disk Operations** (`api/disk_operations.py`): Array and disk management 61 | - **Docker Operations** (`api/docker_operations.py`): Container control 62 | - **VM Operations** (`api/vm_operations.py`): Virtual machine management 63 | - **UPS Operations** (`api/ups_operations.py`): UPS monitoring 64 | - **User Script Operations** (`api/userscript_operations.py`): User script execution 65 | - **Network Operations** (`api/network_operations.py`): Network statistics 66 | 67 | ### Unraid Layer 68 | 69 | The actual Unraid server that the integration communicates with: 70 | 71 | 1. **SSH Connection**: 72 | - Primary communication channel 73 | - Used for executing commands and retrieving data 74 | 75 | 2. **Unraid Components**: 76 | - **System**: OS and hardware information 77 | - **Docker**: Container management system 78 | - **VMs**: Virtual machines 79 | - **Array & Disks**: Storage system 80 | - **UPS**: Uninterruptible power supply 81 | - **User Scripts**: Custom scripts defined in Unraid 82 | 83 | ## Data Flow 84 | 85 | 1. **Configuration and Initialization**: 86 | - User configures the integration through the UI 87 | - Home Assistant creates a ConfigEntry 88 | - Integration sets up the coordinator and API client 89 | - Platforms register entities with Home Assistant 90 | 91 | 2. **Data Update Cycle**: 92 | - Coordinator schedules regular updates 93 | - API client requests data from the Unraid server 94 | - Data is processed, normalized, and cached 95 | - Entities receive updated data through the coordinator 96 | 97 | 3. **User Actions**: 98 | - User interacts with an entity (e.g., switch, button) 99 | - Entity calls appropriate API method 100 | - API executes command on the Unraid server 101 | - Coordinator refreshes data to reflect changes 102 | 103 | ## Factory Pattern for Entity Creation 104 | 105 | The integration uses a factory pattern for creating entities: 106 | 107 | *[Diagram: Factory pattern class diagram showing the relationship between SensorFactory, SensorRegistry, UnraidSensorBase, and related classes]* 108 | 109 | **Note:** A detailed class diagram will be added in a future update. 110 | 111 | This pattern allows for flexible entity creation: 112 | 113 | - Sensor types are registered once 114 | - Creator functions determine what entities to create 115 | - Factory orchestrates the creation process 116 | 117 | ## Caching and Performance Optimization 118 | 119 | The integration implements sophisticated caching to minimize SSH connections: 120 | 121 | *[Diagram: Sequence diagram showing the caching flow between Coordinator, SensorPriorityManager, CacheManager, API Client, and Unraid Server]* 122 | 123 | **Note:** A detailed sequence diagram will be added in a future update. 124 | 125 | Key aspects of the caching system: 126 | 127 | - Different TTLs for different data types 128 | - Memory-efficient storage with size limits 129 | - Priority-based invalidation 130 | - Performance monitoring 131 | 132 | ## Error Handling and Recovery 133 | 134 | The integration includes robust error handling: 135 | 136 | *[Diagram: Flowchart showing the error handling process with retry logic, circuit breaking, and fallback mechanisms]* 137 | 138 | **Note:** A detailed flowchart will be added in a future update. 139 | 140 | Key error handling features: 141 | 142 | - Exception hierarchy for different error types 143 | - Automatic retries with exponential backoff 144 | - Circuit breaking to prevent cascading failures 145 | - Detailed logging for troubleshooting 146 | -------------------------------------------------------------------------------- /docs/development/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Project 2 | 3 | Thank you for your interest in contributing to the Unraid Integration for Home Assistant! This guide will help you get started with contributing to the project. 4 | 5 | ## Ways to Contribute 6 | 7 | There are many ways to contribute to the project: 8 | 9 | - **Bug Reports**: Reporting issues you encounter 10 | - **Feature Requests**: Suggesting new features or improvements 11 | - **Documentation**: Improving or expanding documentation 12 | - **Code Contributions**: Fixing bugs or implementing new features 13 | - **Testing**: Testing the integration in different environments 14 | - **Helping Others**: Helping users in GitHub discussions or Home Assistant community 15 | 16 | ## Development Setup 17 | 18 | ### Prerequisites 19 | 20 | To develop for this integration, you'll need: 21 | 22 | 1. A working Home Assistant development environment 23 | 2. An Unraid server for testing 24 | 3. Basic knowledge of Python 25 | 4. Familiarity with Home Assistant custom component development 26 | 27 | ### Setting Up a Development Environment 28 | 29 | 1. **Fork the Repository**: Fork [domalab/ha-unraid](https://github.com/domalab/ha-unraid) on GitHub 30 | 2. **Clone Your Fork**: 31 | 32 | ```bash 33 | git clone https://github.com/your-username/ha-unraid.git 34 | cd ha-unraid 35 | ``` 36 | 37 | 3. **Set Up a Development Home Assistant Instance**: 38 | - Use [the Home Assistant development container](https://developers.home-assistant.io/docs/development_environment) 39 | - Or set up a dedicated test instance 40 | 4. **Install Development Dependencies**: 41 | 42 | ```bash 43 | python -m pip install -r requirements_dev.txt 44 | ``` 45 | 46 | ### Development Workflow 47 | 48 | 1. **Create a Branch**: Create a branch for your feature or bug fix 49 | 50 | ```bash 51 | git checkout -b your-feature-name 52 | ``` 53 | 54 | 2. **Make Changes**: Implement your changes following the code style and conventions 55 | 3. **Write Tests**: Add tests for your changes to ensure they work correctly 56 | 4. **Run Tests Locally**: Ensure all tests pass before submitting 57 | 58 | ```bash 59 | pytest 60 | ``` 61 | 62 | 5. **Lint Your Code**: Ensure your code passes all linting checks 63 | 64 | ```bash 65 | pylint custom_components/unraid 66 | ``` 67 | 68 | 6. **Commit Your Changes**: Write clear, concise commit messages 69 | 7. **Push to Your Fork**: 70 | 71 | ```bash 72 | git push origin your-feature-name 73 | ``` 74 | 75 | 8. **Create a Pull Request**: Submit a pull request from your fork to the main repository 76 | 77 | ## Code Style and Conventions 78 | 79 | The project follows the [Home Assistant Development Guidelines](https://developers.home-assistant.io/docs/development_guidelines). Key points include: 80 | 81 | - Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) for Python code style 82 | - Use [typing](https://docs.python.org/3/library/typing.html) for type hints 83 | - Write [docstrings](https://www.python.org/dev/peps/pep-0257/) for functions and classes 84 | - Format strings using [f-strings](https://www.python.org/dev/peps/pep-0498/) where appropriate 85 | - Follow existing patterns and conventions in the codebase 86 | 87 | ## Testing 88 | 89 | ### Unit Tests 90 | 91 | - Write unit tests for all new code 92 | - Make sure existing tests still pass with your changes 93 | - Test both expected behavior and edge cases 94 | - Use appropriate mocking for external dependencies 95 | 96 | ### Integration Tests 97 | 98 | - Test the integration with a real Unraid server when possible 99 | - Test with different Unraid versions if available 100 | - Verify that your changes work in different environments 101 | 102 | ## Pull Request Process 103 | 104 | 1. **Describe Your Changes**: Provide a clear description of what your PR does 105 | 2. **Link Related Issues**: Reference any related issues using the `#issue-number` syntax 106 | 3. **Pass CI Checks**: Ensure your PR passes all CI checks 107 | 4. **Review Process**: Respond to any feedback or suggestions during review 108 | 5. **Merge**: Once approved, your PR will be merged into the main codebase 109 | 110 | ## Bug Reports and Feature Requests 111 | 112 | ### Reporting Bugs 113 | 114 | When reporting bugs, please include: 115 | 116 | 1. Steps to reproduce the issue 117 | 2. Expected behavior 118 | 3. Actual behavior 119 | 4. Home Assistant version 120 | 5. Unraid version 121 | 6. Integration version 122 | 7. Relevant logs or error messages 123 | 8. Any other context that might be helpful 124 | 125 | ### Requesting Features 126 | 127 | When requesting a feature, please include: 128 | 129 | 1. A clear description of the feature 130 | 2. The problem it solves or benefit it provides 131 | 3. Any implementation ideas you have 132 | 4. Whether you're willing to help implement it 133 | 134 | ## Documentation Contributions 135 | 136 | Documentation improvements are always welcome! To contribute to documentation: 137 | 138 | 1. Follow the same pull request process as for code changes 139 | 2. Preview documentation changes locally using [MkDocs](https://www.mkdocs.org/) 140 | 141 | ```bash 142 | mkdocs serve 143 | ``` 144 | 145 | 3. Ensure your changes are accurate and well-written 146 | 147 | ## Getting Help 148 | 149 | If you need help with contributing: 150 | 151 | - Check the [project's GitHub issues](https://github.com/domalab/ha-unraid/issues) for similar questions 152 | - Ask in the [Home Assistant Community](https://community.home-assistant.io/) 153 | - Open a [GitHub discussion](https://github.com/domalab/ha-unraid/discussions) with your question 154 | 155 | ## Code of Conduct 156 | 157 | Please follow these guidelines when contributing: 158 | 159 | - Be respectful and inclusive toward other contributors 160 | - Provide constructive feedback on others' contributions 161 | - Focus on the issues, not the person 162 | - Accept feedback graciously on your own contributions 163 | - Help create a positive community for everyone 164 | 165 | ## License 166 | 167 | By contributing to this project, you agree that your contributions will be licensed under the project's [Apache 2.0 License](https://github.com/domalab/ha-unraid/blob/main/LICENSE). 168 | -------------------------------------------------------------------------------- /custom_components/unraid/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Binary sensors for Unraid.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Optional, Dict, Any 6 | 7 | from homeassistant.config_entries import ConfigEntry # type: ignore 8 | from homeassistant.core import HomeAssistant # type: ignore 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback # type: ignore 10 | 11 | from .const import DOMAIN 12 | from .diagnostics.base import UnraidBinarySensorBase 13 | from .diagnostics.disk import UnraidArrayDiskSensor 14 | from .diagnostics.pool import UnraidPoolDiskSensor 15 | from .diagnostics.parity import UnraidParityDiskSensor, UnraidParityCheckSensor 16 | from .diagnostics.ups import UnraidUPSBinarySensor 17 | from .diagnostics.array import UnraidArrayStatusBinarySensor, UnraidArrayHealthSensor 18 | from .diagnostics.const import SENSOR_DESCRIPTIONS 19 | from .coordinator import UnraidDataUpdateCoordinator 20 | 21 | _LOGGER = logging.getLogger(__name__) 22 | 23 | async def _get_parity_info(coordinator: UnraidDataUpdateCoordinator) -> Optional[Dict[str, Any]]: 24 | """Get parity disk information from mdcmd status.""" 25 | try: 26 | result = await coordinator.api.execute_command("mdcmd status") 27 | if result.exit_status != 0: 28 | return None 29 | 30 | parity_info = {} 31 | for line in result.stdout.splitlines(): 32 | if "=" not in line: 33 | continue 34 | key, value = line.split("=", 1) 35 | key = key.strip() 36 | value = value.strip() 37 | if key in [ 38 | "diskNumber.0", "diskName.0", "diskSize.0", "diskState.0", 39 | "diskId.0", "rdevNumber.0", "rdevStatus.0", "rdevName.0", 40 | "rdevOffset.0", "rdevSize.0", "rdevId.0" 41 | ]: 42 | parity_info[key] = value 43 | 44 | # Only return if we found valid parity info 45 | if "rdevName.0" in parity_info and "diskState.0" in parity_info: 46 | return parity_info 47 | 48 | return None 49 | 50 | except Exception as err: 51 | _LOGGER.error("Error getting parity disk info: %s", err) 52 | return None 53 | 54 | async def async_setup_entry( 55 | hass: HomeAssistant, 56 | entry: ConfigEntry, 57 | async_add_entities: AddEntitiesCallback, 58 | ) -> None: 59 | """Set up Unraid binary sensors.""" 60 | coordinator: UnraidDataUpdateCoordinator = entry.runtime_data 61 | entities: list[UnraidBinarySensorBase] = [] 62 | processed_disks = set() # Track processed disks 63 | 64 | # Add base sensors first 65 | for description in SENSOR_DESCRIPTIONS: 66 | entities.append(UnraidBinarySensorBase(coordinator, description)) 67 | _LOGGER.debug( 68 | "Added binary sensor | description_key: %s | name: %s", 69 | description.key, 70 | description.name, 71 | ) 72 | 73 | # Add Array Status binary sensor 74 | entities.append(UnraidArrayStatusBinarySensor(coordinator)) 75 | _LOGGER.debug("Added Array Status binary sensor") 76 | 77 | # Add Array Health binary sensor 78 | entities.append(UnraidArrayHealthSensor(coordinator)) 79 | _LOGGER.debug("Added Array Health binary sensor") 80 | 81 | # Add UPS sensor if UPS info is available 82 | if coordinator.data.get("system_stats", {}).get("ups_info"): 83 | entities.append(UnraidUPSBinarySensor(coordinator)) 84 | _LOGGER.debug("Added UPS binary sensor") 85 | 86 | # Check for and add parity-related sensors 87 | parity_info = await _get_parity_info(coordinator) 88 | if parity_info: 89 | # Store parity info in coordinator data 90 | coordinator.data["parity_info"] = parity_info 91 | 92 | # Add parity disk sensor 93 | entities.append(UnraidParityDiskSensor(coordinator, parity_info)) 94 | _LOGGER.debug( 95 | "Added parity disk sensor | device: %s", 96 | parity_info.get("rdevName.0") 97 | ) 98 | 99 | # Add parity check sensor 100 | entities.append(UnraidParityCheckSensor(coordinator)) 101 | _LOGGER.debug( 102 | "Added parity check sensor for %s", 103 | coordinator.hostname 104 | ) 105 | 106 | # Filter out tmpfs and special mounts 107 | ignored_mounts = { 108 | "disks", "remotes", "addons", "rootshare", 109 | "user/0", "dev/shm" 110 | } 111 | 112 | # Process disk health sensors 113 | disk_data = coordinator.data.get("system_stats", {}).get("individual_disks", []) 114 | valid_disks = [ 115 | disk for disk in disk_data 116 | if ( 117 | disk.get("name") 118 | and not any(mount in disk.get("mount_point", "") for mount in ignored_mounts) 119 | and disk.get("filesystem") != "tmpfs" 120 | ) 121 | ] 122 | 123 | # First process array disks 124 | for disk in valid_disks: 125 | disk_name = disk.get("name") 126 | if not disk_name or disk_name in processed_disks: 127 | continue 128 | 129 | if disk_name.startswith("disk"): 130 | try: 131 | entities.append( 132 | UnraidArrayDiskSensor( 133 | coordinator=coordinator, 134 | disk_name=disk_name 135 | ) 136 | ) 137 | processed_disks.add(disk_name) 138 | _LOGGER.info( 139 | "Added array disk sensor: %s", 140 | disk_name 141 | ) 142 | except ValueError as err: 143 | _LOGGER.warning("Skipping invalid array disk %s: %s", disk_name, err) 144 | continue 145 | 146 | # Then process pool disks 147 | for disk in valid_disks: 148 | disk_name = disk.get("name") 149 | if not disk_name or disk_name in processed_disks: 150 | continue 151 | 152 | if not disk_name.startswith("disk"): 153 | try: 154 | entities.append( 155 | UnraidPoolDiskSensor( 156 | coordinator=coordinator, 157 | disk_name=disk_name 158 | ) 159 | ) 160 | processed_disks.add(disk_name) 161 | _LOGGER.info( 162 | "Added pool disk sensor: %s", 163 | disk_name 164 | ) 165 | except ValueError as err: 166 | _LOGGER.warning("Skipping invalid pool disk %s: %s", disk_name, err) 167 | continue 168 | 169 | async_add_entities(entities) 170 | -------------------------------------------------------------------------------- /custom_components/unraid/__init__.py: -------------------------------------------------------------------------------- 1 | """The Unraid integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.const import Platform 8 | from homeassistant.core import HomeAssistant 9 | 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | # Import only what's needed for setup 13 | DOMAIN = "unraid" 14 | 15 | # Define platforms to load 16 | PLATFORMS = [ 17 | Platform.BINARY_SENSOR, 18 | Platform.SENSOR, 19 | Platform.SWITCH, 20 | Platform.BUTTON, 21 | ] 22 | 23 | async def async_setup(hass: HomeAssistant, config: dict) -> bool: 24 | """Set up the Unraid integration.""" 25 | # This function is called when the integration is loaded via configuration.yaml 26 | # We don't support configuration.yaml setup, so just return True 27 | return True 28 | 29 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 30 | """Set up Unraid from a config entry.""" 31 | # Import dependencies lazily to avoid blocking the event loop 32 | from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME 33 | from homeassistant.exceptions import ConfigEntryNotReady 34 | 35 | # Import local modules lazily 36 | from .coordinator import UnraidDataUpdateCoordinator 37 | from .unraid import UnraidAPI 38 | from .api.logging_helper import LogManager 39 | from . import services 40 | 41 | # Create a log manager instance here to avoid module-level instantiation 42 | log_manager = LogManager() 43 | log_manager.configure() 44 | 45 | # Using modern runtime_data approach - no need for hass.data setup 46 | 47 | try: 48 | # Extract configuration 49 | host = entry.data[CONF_HOST] 50 | username = entry.data[CONF_USERNAME] 51 | password = entry.data[CONF_PASSWORD] 52 | port = entry.data.get(CONF_PORT, 22) 53 | 54 | # Create API client 55 | api = UnraidAPI(host, username, password, port) 56 | 57 | # Create coordinator 58 | coordinator = UnraidDataUpdateCoordinator(hass, api, entry) 59 | 60 | # Clean up any duplicate entities before setting up new ones 61 | from .migrations import async_cleanup_duplicate_entities 62 | try: 63 | duplicates_removed = await async_cleanup_duplicate_entities(hass, entry) 64 | if duplicates_removed > 0: 65 | _LOGGER.info("Cleaned up %d duplicate entities during setup", duplicates_removed) 66 | except Exception as cleanup_err: 67 | _LOGGER.warning("Failed to clean up duplicate entities: %s", cleanup_err) 68 | 69 | # Get initial data 70 | await coordinator.async_config_entry_first_refresh() 71 | 72 | # Store coordinator using modern runtime_data approach 73 | entry.runtime_data = coordinator 74 | 75 | # Set up platforms 76 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 77 | 78 | # Set up services 79 | await services.async_setup_services(hass) 80 | 81 | # Register update listener for options 82 | entry.async_on_unload(entry.add_update_listener(update_listener)) 83 | 84 | return True 85 | 86 | except Exception as err: 87 | _LOGGER.error("Failed to set up Unraid integration: %s", err) 88 | raise ConfigEntryNotReady from err 89 | 90 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 91 | """Unload a config entry.""" 92 | # Unload platforms 93 | unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 94 | 95 | # Clean up coordinator using modern runtime_data approach 96 | if unload_ok and hasattr(entry, 'runtime_data') and entry.runtime_data: 97 | coordinator = entry.runtime_data 98 | await coordinator.async_stop() 99 | entry.runtime_data = None 100 | 101 | return unload_ok 102 | 103 | async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: 104 | """Handle options update.""" 105 | # Reload the integration 106 | await hass.config_entries.async_reload(entry.entry_id) 107 | 108 | async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 109 | """Migrate old entry to new version. 110 | 111 | This function handles migration of config entries from older versions to the current version. 112 | It supports: 113 | - Migration from version 1 to version 2 114 | - Migration from no version (None) to version 2 115 | - Migration from any lower version to the current version 116 | 117 | Returns True if migration is successful, False otherwise. 118 | """ 119 | from .const import MIGRATION_VERSION 120 | from .migrations import async_migrate_with_rollback 121 | 122 | _LOGGER.debug("Migrating from version %s.%s to %s.%s", 123 | entry.version, getattr(entry, 'minor_version', 0), 124 | MIGRATION_VERSION, 0) 125 | 126 | # If the user has downgraded from a future version, we can't migrate 127 | if entry.version is not None and entry.version > MIGRATION_VERSION: 128 | _LOGGER.error( 129 | "Cannot migrate from version %s to %s (downgrade not supported)", 130 | entry.version, MIGRATION_VERSION 131 | ) 132 | return False 133 | 134 | # Handle migration from version 1 to version 2 135 | if entry.version == 1: 136 | try: 137 | if await async_migrate_with_rollback(hass, entry): 138 | # Update entry version after successful migration 139 | hass.config_entries.async_update_entry( 140 | entry, 141 | version=MIGRATION_VERSION 142 | ) 143 | _LOGGER.info("Successfully migrated entry %s from version 1 to %s", 144 | entry.entry_id, MIGRATION_VERSION) 145 | return True 146 | except Exception as err: 147 | _LOGGER.error("Migration failed: %s", err) 148 | return False 149 | 150 | # Handle any entry with no version (None) or other versions 151 | elif entry.version is None or entry.version < MIGRATION_VERSION: 152 | _LOGGER.info("Migrating entry %s from version %s to %s", 153 | entry.entry_id, entry.version, MIGRATION_VERSION) 154 | 155 | # Simply update the version without any data changes 156 | # This is safe because we're not changing the data structure 157 | hass.config_entries.async_update_entry( 158 | entry, 159 | version=MIGRATION_VERSION 160 | ) 161 | 162 | _LOGGER.info("Migration to version %s successful", MIGRATION_VERSION) 163 | return True 164 | 165 | # If we get here, the entry is already at the current version 166 | return True 167 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Report a bug or issue with the Unraid Integration 3 | title: "[BUG] " 4 | labels: ["bug"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | # 🐛 Bug Report 12 | 13 | Thank you for reporting a bug! This template is for issues with the **Unraid Integration for Home Assistant** that are not working correctly. 14 | 15 | ## 📋 When to use this template: 16 | - ✅ The Unraid integration was working before but now has issues 17 | - ✅ The integration loads but some features don't work as expected 18 | - ✅ You're getting error messages in Home Assistant logs related to Unraid 19 | - ✅ Sensors, switches, or other entities are not updating correctly 20 | 21 | ## ❌ When NOT to use this template: 22 | - 🚫 **Feature Requests**: Want new features for the integration → Use "Feature Request" template 23 | - 🚫 **Setup Help**: Need help with initial configuration → Check [documentation](https://domalab.github.io/ha-unraid/) first 24 | 25 | - type: input 26 | id: unraid_version 27 | attributes: 28 | label: 🖥️ Unraid Version 29 | description: "Your Unraid server version" 30 | placeholder: "6.12.6" 31 | validations: 32 | required: true 33 | 34 | - type: dropdown 35 | id: bug_category 36 | attributes: 37 | label: 🏷️ Bug Category 38 | description: "What type of issue are you experiencing?" 39 | options: 40 | - "Integration fails to load/setup" 41 | - "Connection issues (authentication, network)" 42 | - "Sensors not updating or showing incorrect data" 43 | - "Docker container controls not working" 44 | - "VM controls not working" 45 | - "User script execution issues" 46 | - "Disk/array monitoring problems" 47 | - "UPS monitoring issues" 48 | - "Network monitoring problems" 49 | - "Entity duplication or missing entities" 50 | - "Home Assistant crashes/errors" 51 | - "Other" 52 | validations: 53 | required: true 54 | 55 | - type: textarea 56 | id: bug_description 57 | attributes: 58 | label: 📝 Bug Description 59 | description: "Clear description of what's wrong and what you expected to happen" 60 | placeholder: | 61 | **What's happening:** 62 | Docker container switches don't work - when I try to start/stop containers in HA, nothing happens on Unraid. 63 | 64 | **What should happen:** 65 | Containers should start/stop when I toggle the switches in Home Assistant. 66 | 67 | **When did this start:** 68 | After updating to version 2025.06.05 69 | 70 | **Additional context:** 71 | Include any error messages, affected entities, or specific scenarios. 72 | validations: 73 | required: true 74 | 75 | - type: textarea 76 | id: reproduction_steps 77 | attributes: 78 | label: 🔄 Steps to Reproduce 79 | description: "Step-by-step instructions to reproduce the issue" 80 | placeholder: | 81 | 1. Open Home Assistant 82 | 2. Go to the Unraid integration entities 83 | 3. Try to toggle a Docker container switch 84 | 4. Check Unraid dashboard - container state doesn't change 85 | 5. Check HA logs for any error messages 86 | render: markdown 87 | validations: 88 | required: true 89 | 90 | - type: input 91 | id: integration_version 92 | attributes: 93 | label: 📦 Integration Version 94 | description: "Version of the Unraid Integration (check in HACS or manifest.json)" 95 | placeholder: "2025.06.05" 96 | validations: 97 | required: true 98 | 99 | - type: input 100 | id: ha_version 101 | attributes: 102 | label: 🏠 Home Assistant Version 103 | description: "Your Home Assistant version" 104 | placeholder: "2025.6.0" 105 | validations: 106 | required: true 107 | 108 | - type: dropdown 109 | id: ha_installation_type 110 | attributes: 111 | label: 🏗️ Home Assistant Installation Type 112 | description: "How is Home Assistant installed?" 113 | options: 114 | - "Home Assistant OS (HAOS)" 115 | - "Home Assistant Container (Docker)" 116 | - "Home Assistant Supervised" 117 | - "Home Assistant Core (Python venv)" 118 | - "Other" 119 | validations: 120 | required: true 121 | 122 | - type: textarea 123 | id: logs 124 | attributes: 125 | label: 📋 Relevant Logs 126 | description: | 127 | Home Assistant logs related to the issue. Enable debug logging first: 128 | 129 | ```yaml 130 | logger: 131 | logs: 132 | custom_components.unraid: debug 133 | ``` 134 | 135 | Then reload the integration and reproduce the issue. 136 | placeholder: | 137 | ``` 138 | 2025-06-11 10:30:00 ERROR (MainThread) [custom_components.unraid] ... 139 | 2025-06-11 10:30:01 DEBUG (MainThread) [custom_components.unraid.api] ... 140 | ``` 141 | render: shell 142 | validations: 143 | required: false 144 | 145 | - type: textarea 146 | id: network_setup 147 | attributes: 148 | label: 🌐 Network Setup 149 | description: "Information about your network configuration" 150 | placeholder: | 151 | - Unraid server IP: 192.168.1.100 152 | - Home Assistant IP: 192.168.1.50 153 | - Same subnet: Yes/No 154 | - VLANs or firewalls: None/Details 155 | - SSH port (if not 22): 22 156 | render: markdown 157 | validations: 158 | required: false 159 | 160 | - type: textarea 161 | id: additional_context 162 | attributes: 163 | label: 📝 Additional Context 164 | description: "Any other relevant information" 165 | placeholder: | 166 | - Recent changes to your Unraid or HA setup 167 | - Workarounds you've tried 168 | - Screenshots of error messages 169 | - Specific entities affected 170 | - Docker containers or VMs involved 171 | render: markdown 172 | validations: 173 | required: false 174 | 175 | - type: checkboxes 176 | id: checklist 177 | attributes: 178 | label: ✅ Checklist 179 | description: "Please confirm you have done the following:" 180 | options: 181 | - label: "I have searched existing [open](https://github.com/domalab/ha-unraid/issues) and [closed](https://github.com/domalab/ha-unraid/issues?q=is%3Aissue+is%3Aclosed) issues to ensure this isn't a duplicate" 182 | required: true 183 | - label: "I have reproduced the issue on the latest version to confirm it still exists" 184 | required: true 185 | - label: "I have tried restarting Home Assistant" 186 | required: false 187 | - label: "I have tried reloading the Unraid integration" 188 | required: false 189 | - label: "I have enabled debug logging and included relevant logs" 190 | required: false 191 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature Request 2 | description: Suggest a new feature or enhancement for the Unraid Integration 3 | title: "[Enhancement] " 4 | labels: ["enhancement"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | # ✨ Feature Request 12 | 13 | Thank you for suggesting a new feature! This template is for requesting **new features or enhancements** for the Unraid Integration for Home Assistant. 14 | 15 | ## 📋 When to use this template: 16 | - ✅ You want to add new functionality to the Unraid integration 17 | - ✅ You have ideas for better automation or control options 18 | - ✅ You want to suggest monitoring improvements or new sensors 19 | - ✅ You want to suggest UI/UX improvements 20 | - ✅ You have ideas for new services or entity types 21 | 22 | ## ❌ When NOT to use this template: 23 | - 🚫 **Bug Reports**: Existing features are broken → Use "Bug Report" template 24 | - 🚫 **Setup Help**: Need help with configuration → Check [documentation](https://domalab.github.io/ha-unraid/) first 25 | 26 | - type: dropdown 27 | id: feature_category 28 | attributes: 29 | label: 🏷️ Feature Category 30 | description: "What type of feature are you requesting?" 31 | options: 32 | - "New sensor type (monitoring, status, etc.)" 33 | - "New switch/control entity" 34 | - "Enhanced Docker container management" 35 | - "Enhanced VM management" 36 | - "New user script capabilities" 37 | - "Improved disk/array monitoring" 38 | - "Network monitoring enhancements" 39 | - "UPS monitoring improvements" 40 | - "New custom service" 41 | - "Configuration options" 42 | - "Performance optimization" 43 | - "UI/UX improvements" 44 | - "Documentation improvement" 45 | - "Other" 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: feature_description 51 | attributes: 52 | label: 📝 Feature Description 53 | description: "Clear description of the feature you'd like to see" 54 | placeholder: | 55 | **What feature would you like:** 56 | Add a sensor that shows the current parity check progress as a percentage instead of just showing "running" or "stopped". 57 | 58 | **Why would this be useful:** 59 | It would help users monitor long-running parity checks and set up automations based on progress. 60 | validations: 61 | required: true 62 | 63 | - type: textarea 64 | id: use_case 65 | attributes: 66 | label: 🎯 Use Case / Problem Solved 67 | description: "What problem does this feature solve? How would you use it?" 68 | placeholder: | 69 | **Problem:** 70 | Currently I can only see if a parity check is running, but not how much progress has been made. 71 | 72 | **Use Case:** 73 | I want to create an automation that sends notifications when parity check reaches 50% and 90% completion. 74 | 75 | **Benefit:** 76 | This would help users better monitor and plan around long-running maintenance operations. 77 | render: markdown 78 | validations: 79 | required: true 80 | 81 | - type: textarea 82 | id: proposed_solution 83 | attributes: 84 | label: 💡 Proposed Solution 85 | description: "How do you think this feature should work?" 86 | placeholder: | 87 | **Implementation idea:** 88 | - Add a new sensor entity: `sensor.unraid_parity_check_progress` 89 | - Parse parity check status from Unraid logs or API 90 | - Update every few minutes during active parity checks 91 | 92 | **UI/UX:** 93 | - Show progress percentage in entity state 94 | - Include estimated time remaining as an attribute 95 | - Add to device diagnostics 96 | render: markdown 97 | validations: 98 | required: false 99 | 100 | - type: textarea 101 | id: alternatives 102 | attributes: 103 | label: 🔄 Alternatives Considered 104 | description: "Have you considered any alternative solutions or workarounds?" 105 | placeholder: | 106 | **Current workarounds:** 107 | - Manually checking Unraid web interface 108 | - Using template sensors with SSH commands 109 | - Setting up external monitoring scripts 110 | 111 | **Why they're not ideal:** 112 | - Requires manual checking or complex setup 113 | - Not integrated with Home Assistant automations 114 | - May not be reliable or efficient 115 | render: markdown 116 | validations: 117 | required: false 118 | 119 | - type: textarea 120 | id: unraid_context 121 | attributes: 122 | label: 🖥️ Unraid Context 123 | description: "Relevant information about your Unraid setup" 124 | placeholder: | 125 | - Unraid version: 6.12.6 126 | - Array size: 8 drives 127 | - Specific hardware or configuration that might be relevant 128 | - Current monitoring setup you're using 129 | render: markdown 130 | validations: 131 | required: false 132 | 133 | - type: dropdown 134 | id: priority 135 | attributes: 136 | label: 📊 Priority Level 137 | description: "How important is this feature to you?" 138 | options: 139 | - "Low - Nice to have" 140 | - "Medium - Would improve my experience" 141 | - "High - Significantly impacts usability" 142 | - "Critical - Blocking important use cases" 143 | validations: 144 | required: true 145 | 146 | - type: checkboxes 147 | id: contribution 148 | attributes: 149 | label: 🤝 Contribution 150 | description: "Would you be willing to help implement this feature?" 151 | options: 152 | - label: "I'm willing to test development versions" 153 | - label: "I can help with documentation" 154 | - label: "I have programming skills and could contribute code" 155 | - label: "I can provide detailed requirements and feedback" 156 | 157 | - type: textarea 158 | id: additional_context 159 | attributes: 160 | label: 📝 Additional Context 161 | description: "Any other relevant information, mockups, or examples" 162 | placeholder: | 163 | - Screenshots or mockups of desired UI 164 | - Examples from other integrations 165 | - Technical considerations 166 | - Related feature requests 167 | render: markdown 168 | validations: 169 | required: false 170 | 171 | - type: checkboxes 172 | id: checklist 173 | attributes: 174 | label: ✅ Checklist 175 | description: "Please confirm you have done the following:" 176 | options: 177 | - label: "I have searched existing [open](https://github.com/domalab/ha-unraid/issues) and [closed](https://github.com/domalab/ha-unraid/issues?q=is%3Aissue+is%3Aclosed) issues to ensure this isn't a duplicate" 178 | required: true 179 | - label: "I have clearly described the problem this feature would solve" 180 | required: true 181 | - label: "I understand that feature requests might take time or not be implemented if they are not within project scope" 182 | required: true 183 | -------------------------------------------------------------------------------- /custom_components/unraid/api/error_handling.py: -------------------------------------------------------------------------------- 1 | """Error handling utilities for Unraid integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | import functools 6 | import asyncio 7 | from typing import Any, Callable, TypeVar, cast, Optional 8 | 9 | from .connection_manager import CommandTimeoutError, CommandError, UnraidConnectionError 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | # Type variables for function signatures 14 | T = TypeVar('T') 15 | F = TypeVar('F', bound=Callable[..., Any]) 16 | 17 | 18 | class UnraidAPIError(Exception): 19 | """Base class for Unraid API errors.""" 20 | pass 21 | 22 | 23 | class UnraidDataError(UnraidAPIError): 24 | """Raised when there's an error parsing or processing data.""" 25 | pass 26 | 27 | 28 | class UnraidTimeoutError(UnraidAPIError): 29 | """Raised when an operation times out.""" 30 | pass 31 | 32 | 33 | class UnraidOperationError(UnraidAPIError): 34 | """Raised when an operation fails.""" 35 | def __init__(self, message: str, exit_code: Optional[int] = None): 36 | super().__init__(message) 37 | self.exit_code = exit_code 38 | 39 | 40 | def with_error_handling( 41 | fallback_return: Optional[Any] = None, 42 | max_retries: int = 2, 43 | retry_delay: float = 1.0 44 | ) -> Callable[[F], F]: 45 | """Decorator to add error handling to API operations. 46 | 47 | Args: 48 | fallback_return: Value to return if the operation fails 49 | max_retries: Maximum number of retries for the operation 50 | retry_delay: Delay between retries in seconds 51 | 52 | Returns: 53 | Decorated function with error handling 54 | """ 55 | def decorator(func: F) -> F: 56 | @functools.wraps(func) 57 | async def wrapper(*args: Any, **kwargs: Any) -> Any: 58 | retries = 0 59 | 60 | while retries <= max_retries: 61 | try: 62 | return await func(*args, **kwargs) 63 | 64 | except CommandTimeoutError as err: 65 | # Error handling 66 | retries += 1 67 | if retries > max_retries: 68 | _LOGGER.error( 69 | "Operation timed out after %d retries: %s", 70 | max_retries, 71 | func.__name__ 72 | ) 73 | if fallback_return is not None: 74 | return fallback_return 75 | raise UnraidTimeoutError(f"Operation timed out: {err}") from err 76 | 77 | _LOGGER.warning( 78 | "Operation timed out, retrying (%d/%d): %s", 79 | retries, 80 | max_retries + 1, 81 | func.__name__ 82 | ) 83 | await asyncio.sleep(retry_delay * retries) 84 | 85 | except CommandError as err: 86 | # Error handling 87 | # Don't retry if the command itself failed with a non-zero exit code 88 | if err.exit_code is not None and err.exit_code != 0: 89 | _LOGGER.error( 90 | "Command failed with exit code %d: %s", 91 | err.exit_code, 92 | func.__name__ 93 | ) 94 | if fallback_return is not None: 95 | return fallback_return 96 | raise UnraidOperationError( 97 | f"Command failed: {err}", 98 | exit_code=err.exit_code 99 | ) from err 100 | 101 | # For other command errors, retry 102 | retries += 1 103 | if retries > max_retries: 104 | _LOGGER.error( 105 | "Operation failed after %d retries: %s", 106 | max_retries, 107 | func.__name__ 108 | ) 109 | if fallback_return is not None: 110 | return fallback_return 111 | raise UnraidOperationError(f"Operation failed: {err}") from err 112 | 113 | _LOGGER.warning( 114 | "Operation failed, retrying (%d/%d): %s", 115 | retries, 116 | max_retries + 1, 117 | func.__name__ 118 | ) 119 | await asyncio.sleep(retry_delay * retries) 120 | 121 | except (ConnectionError, UnraidConnectionError) as err: 122 | # Error handling 123 | retries += 1 124 | if retries > max_retries: 125 | _LOGGER.error( 126 | "Connection error after %d retries: %s", 127 | max_retries, 128 | func.__name__ 129 | ) 130 | if fallback_return is not None: 131 | return fallback_return 132 | raise UnraidConnectionError(f"Connection error: {err}") from err 133 | 134 | _LOGGER.warning( 135 | "Connection error, retrying (%d/%d): %s", 136 | retries, 137 | max_retries + 1, 138 | func.__name__ 139 | ) 140 | await asyncio.sleep(retry_delay * retries) 141 | 142 | except Exception as err: 143 | # last_error = err 144 | _LOGGER.error( 145 | "Unexpected error in %s: %s", 146 | func.__name__, 147 | err, 148 | exc_info=True 149 | ) 150 | if fallback_return is not None: 151 | return fallback_return 152 | raise UnraidAPIError(f"Unexpected error: {err}") from err 153 | 154 | # This should never happen, but just in case 155 | if fallback_return is not None: 156 | return fallback_return 157 | raise UnraidAPIError(f"Operation failed after {max_retries} retries") 158 | 159 | return cast(F, wrapper) 160 | return decorator 161 | 162 | 163 | def safe_parse( 164 | parser_func: Callable[[Any], T], 165 | data: Any, 166 | default: Optional[T] = None, 167 | error_msg: str = "Error parsing data" 168 | ) -> T: 169 | """Safely parse data with error handling. 170 | 171 | Args: 172 | parser_func: Function to parse the data 173 | data: Data to parse 174 | default: Default value to return if parsing fails 175 | error_msg: Error message to log if parsing fails 176 | 177 | Returns: 178 | Parsed data or default value if parsing fails 179 | """ 180 | try: 181 | return parser_func(data) 182 | except Exception as err: 183 | _LOGGER.error("%s: %s", error_msg, err) 184 | if default is not None: 185 | return default 186 | raise UnraidDataError(f"{error_msg}: {err}") from err 187 | -------------------------------------------------------------------------------- /custom_components/unraid/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Unraid integration.""" 2 | from enum import Enum, IntEnum 3 | from typing import Final, Dict 4 | from homeassistant.const import ( # type: ignore 5 | Platform, 6 | PERCENTAGE, 7 | UnitOfPower, 8 | UnitOfElectricPotential, 9 | UnitOfTime, 10 | UnitOfEnergy, 11 | ) 12 | 13 | # Unraid Server 14 | DOMAIN = "unraid" 15 | DEFAULT_PORT = 22 16 | 17 | # Migration version 18 | MIGRATION_VERSION = 2 19 | 20 | # Unraid Server Hostname 21 | CONF_HOSTNAME = "hostname" 22 | MAX_HOSTNAME_LENGTH = 32 23 | DEFAULT_NAME = "unraid" # Fallback name if no hostname 24 | 25 | # Update intervals 26 | MIN_UPDATE_INTERVAL = 1 # minutes 27 | MAX_GENERAL_INTERVAL = 60 # minutes 28 | 29 | # Disk update intervals 30 | MIN_DISK_INTERVAL_MINUTES = 5 # minutes 31 | MAX_DISK_INTERVAL_HOURS = 24 # hours 32 | DEFAULT_GENERAL_INTERVAL = 5 # minutes 33 | DEFAULT_DISK_INTERVAL = 60 # minutes (1 hour) 34 | 35 | # General update interval options in minutes 36 | GENERAL_INTERVAL_OPTIONS = [ 37 | 1, # 1 minute 38 | 2, # 2 minutes 39 | 3, # 3 minutes 40 | 5, # 5 minutes 41 | 10, # 10 minutes 42 | 15, # 15 minutes 43 | 30, # 30 minutes 44 | 60 # 60 minutes (1 hour) 45 | ] 46 | 47 | # Disk update interval options in minutes 48 | DISK_INTERVAL_OPTIONS = [ 49 | 5, # 5 minutes 50 | 10, # 10 minutes 51 | 15, # 15 minutes 52 | 30, # 30 minutes 53 | 45, # 45 minutes 54 | 60, # 1 hour 55 | 120, # 2 hours 56 | 180, # 3 hours 57 | 240, # 4 hours 58 | 300, # 5 hours 59 | 360, # 6 hours 60 | 480, # 8 hours 61 | 720, # 12 hours 62 | 1440 # 24 hours 63 | ] 64 | 65 | UPDATE_FAILED_RETRY_DELAY: Final = 30 # seconds 66 | MAX_FAILED_UPDATE_COUNT: Final = 3 67 | MAX_UPDATE_METRICS_HISTORY: Final = 10 68 | 69 | # Configuration and options 70 | CONF_GENERAL_INTERVAL = "general_interval" 71 | CONF_DISK_INTERVAL = "disk_interval" 72 | CONF_HAS_UPS = "has_ups" 73 | CONF_HOST = "host" 74 | CONF_USERNAME = "username" 75 | CONF_PASSWORD = "password" 76 | 77 | 78 | # Platforms 79 | PLATFORMS = [ 80 | Platform.BINARY_SENSOR, 81 | Platform.SENSOR, 82 | Platform.SWITCH, 83 | Platform.BUTTON, 84 | ] 85 | 86 | # Signals 87 | SIGNAL_UPDATE_UNRAID = f"{DOMAIN}_update" 88 | 89 | # Services 90 | SERVICE_FORCE_UPDATE = "force_update" 91 | 92 | # Config Entry Attributes 93 | ATTR_CONFIG_ENTRY_ID = "config_entry_id" 94 | 95 | # Units 96 | UNIT_PERCENTAGE = "%" 97 | 98 | # CPU Temperature monitoring thresholds (Celsius) 99 | TEMP_WARN_THRESHOLD: Final = 80 # Temperature above which warning state is triggered 100 | TEMP_CRIT_THRESHOLD: Final = 90 # Temperature above which critical state is triggered 101 | 102 | # UPS metric validation ranges 103 | UPS_METRICS: Final[Dict[str, dict]] = { 104 | "NOMPOWER": {"min": 0, "max": 10000, "unit": UnitOfPower.WATT}, 105 | "LOADPCT": {"min": 0, "max": 100, "unit": PERCENTAGE}, 106 | "BCHARGE": {"min": 0, "max": 100, "unit": PERCENTAGE}, 107 | "LINEV": {"min": 0, "max": 500, "unit": UnitOfElectricPotential.VOLT}, 108 | "BATTV": {"min": 0, "max": 60, "unit": UnitOfElectricPotential.VOLT}, 109 | "TIMELEFT": {"min": 0, "max": 1440, "unit": UnitOfTime.MINUTES}, 110 | "ITEMP": {"min": 0, "max": 60, "unit": "°C"}, 111 | "CUMONKWHOURS": {"min": 0, "max": 1000000, "unit": UnitOfEnergy.KILO_WATT_HOUR}, 112 | } 113 | 114 | # UPS model patterns for power calculation 115 | UPS_MODEL_PATTERNS: Final[Dict[str, float]] = { 116 | r'smart-ups.*?(\d{3,4})': 1.0, # Smart-UPS models use direct VA rating 117 | r'back-ups.*?(\d{3,4})': 0.9, # Back-UPS models typically 90% of VA 118 | r'back-ups pro.*?(\d{3,4})': 0.95, # Back-UPS Pro models ~95% of VA 119 | r'smart-ups\s*x.*?(\d{3,4})': 1.0, # Smart-UPS X series 120 | r'smart-ups\s*xl.*?(\d{3,4})': 1.0, # Smart-UPS XL series 121 | r'smart-ups\s*rt.*?(\d{3,4})': 1.0, # Smart-UPS RT series 122 | r'symmetra.*?(\d{3,4})': 1.0, # Symmetra models 123 | r'sua\d{3,4}': 1.0, # Smart-UPS alternative model format 124 | r'smx\d{3,4}': 1.0, # Smart-UPS SMX model format 125 | r'smt\d{3,4}': 1.0, # Smart-UPS SMT model format 126 | } 127 | 128 | # UPS default values and thresholds 129 | UPS_DEFAULT_POWER_FACTOR: Final = 0.9 130 | UPS_TEMP_WARN_THRESHOLD: Final = 45 # °C 131 | UPS_TEMP_CRIT_THRESHOLD: Final = 60 # °C 132 | UPS_BATTERY_LOW_THRESHOLD: Final = 50 # % 133 | UPS_BATTERY_CRITICAL_THRESHOLD: Final = 20 # % 134 | UPS_LOAD_HIGH_THRESHOLD: Final = 80 # % 135 | UPS_LOAD_CRITICAL_THRESHOLD: Final = 95 # % 136 | 137 | # SpinDownDelay class 138 | class SpinDownDelay(IntEnum): 139 | """Unraid disk spin down delay settings.""" 140 | NEVER = 0 # Default in Unraid 141 | MINUTES_15 = 15 142 | MINUTES_30 = 30 143 | MINUTES_45 = 45 144 | HOUR_1 = 1 145 | HOURS_2 = 2 146 | HOURS_3 = 3 147 | HOURS_4 = 4 148 | HOURS_5 = 5 149 | HOURS_6 = 6 150 | HOURS_7 = 7 151 | HOURS_8 = 8 152 | HOURS_9 = 9 153 | 154 | @classmethod 155 | def _missing_(cls, value: object) -> "SpinDownDelay": 156 | """Handle invalid values by mapping to closest valid option.""" 157 | try: 158 | # Convert value to int for comparison 159 | val = int(str(value)) 160 | valid_values = sorted([m.value for m in cls]) 161 | # Find closest valid value 162 | closest = min(valid_values, key=lambda x: abs(x - val)) 163 | return cls(closest) 164 | except (ValueError, TypeError): 165 | return cls.NEVER 166 | 167 | def to_human_readable(self) -> str: 168 | """Convert spin down delay to human readable format.""" 169 | try: 170 | if self == self.NEVER: 171 | return "Never" 172 | if self.value < 60: 173 | return f"{self.value} minutes" 174 | return f"{self.value // 60} hours" 175 | except ValueError: 176 | return f"Unknown ({self.value})" 177 | 178 | def to_seconds(self) -> int: 179 | """Convert delay to seconds for calculations.""" 180 | if self == self.NEVER: 181 | return 0 182 | return self.value * 60 # Convert minutes to seconds 183 | 184 | # DiskStatus class 185 | class DiskStatus(str, Enum): 186 | """Disk status enum.""" 187 | ACTIVE = "active" 188 | STANDBY = "standby" 189 | UNKNOWN = "unknown" 190 | 191 | # DiskHealth class 192 | class DiskHealth(str, Enum): 193 | """Disk health status enum.""" 194 | PASSED = "PASSED" 195 | FAILED = "FAILED" 196 | UNKNOWN = "Unknown" 197 | 198 | # Device identifier patterns 199 | DEVICE_ID_SERVER = "{}_server_{}" # DOMAIN, entry_id 200 | DEVICE_ID_DOCKER = "{}_docker_{}_{}" # DOMAIN, container_name, entry_id 201 | DEVICE_ID_VM = "{}_vm_{}_{}" # DOMAIN, vm_name, entry_id 202 | DEVICE_ID_DISK = "{}_disk_{}_{}" # DOMAIN, disk_name, entry_id 203 | DEVICE_ID_UPS = "{}_ups_{}" # DOMAIN, entry_id 204 | 205 | # Device info defaults 206 | DEVICE_INFO_SERVER = { 207 | "manufacturer": "Lime Technology", 208 | "model": "Unraid Server", 209 | } 210 | 211 | DEVICE_INFO_DOCKER = { 212 | "manufacturer": "Docker", 213 | "model": "Container Engine", 214 | } 215 | 216 | DEVICE_INFO_VM = { 217 | "manufacturer": "Unraid", 218 | "model": "Virtual Machine", 219 | } -------------------------------------------------------------------------------- /docs/advanced/docker-management.md: -------------------------------------------------------------------------------- 1 | # Docker Container Management 2 | 3 | The Unraid Integration provides powerful capabilities for monitoring and controlling Docker containers running on your Unraid server. This guide explains how to effectively use these features. 4 | 5 | ## Available Features 6 | 7 | The Docker management features include: 8 | 9 | - **Status Monitoring**: Real-time status of Docker containers (running, stopped, paused) 10 | - **Basic Controls**: Start and stop containers through switches 11 | - **Advanced Controls**: Pause, resume, and restart containers through services 12 | - **Command Execution**: Run commands inside containers 13 | - **Health Monitoring**: Track container health status (where available) 14 | 15 | ## Container Switches 16 | 17 | Each Docker container on your Unraid server will appear as a switch entity in Home Assistant. The entity ID will be in the format `switch.unraid_docker_[container_name]`, where `[container_name]` is the name of your Docker container with special characters replaced. 18 | 19 | ### Using Container Switches 20 | 21 | Container switches provide basic on/off functionality: 22 | 23 | - **Turn On**: Starts the container if it's stopped 24 | - **Turn Off**: Stops the container if it's running 25 | 26 | You can use these switches in the Home Assistant UI, automations, scripts, and scenes like any other switch entity. 27 | 28 | ```yaml 29 | # Example: Turn on a Docker container 30 | service: switch.turn_on 31 | target: 32 | entity_id: switch.unraid_docker_plex 33 | ``` 34 | 35 | ```yaml 36 | # Example: Turn off a Docker container 37 | service: switch.turn_off 38 | target: 39 | entity_id: switch.unraid_docker_sonarr 40 | ``` 41 | 42 | ## Advanced Container Controls 43 | 44 | For more advanced control, the integration provides several services: 45 | 46 | ### Pause a Container 47 | 48 | Pauses a running container (freezes the container's processes): 49 | 50 | ```yaml 51 | service: unraid.docker_pause 52 | data: 53 | entry_id: your_entry_id 54 | container: container_name 55 | ``` 56 | 57 | ### Resume a Container 58 | 59 | Resumes a paused container: 60 | 61 | ```yaml 62 | service: unraid.docker_resume 63 | data: 64 | entry_id: your_entry_id 65 | container: container_name 66 | ``` 67 | 68 | ### Restart a Container 69 | 70 | Restarts a container (graceful stop and start): 71 | 72 | ```yaml 73 | service: unraid.docker_restart 74 | data: 75 | entry_id: your_entry_id 76 | container: container_name 77 | ``` 78 | 79 | !!! note "Container Names" 80 | The `container` parameter should match the exact container name as shown in the Unraid Docker UI, not the Home Assistant entity name. 81 | 82 | ## Execute Commands in Containers 83 | 84 | You can execute commands inside Docker containers using the `unraid.execute_in_container` service: 85 | 86 | ```yaml 87 | service: unraid.execute_in_container 88 | data: 89 | entry_id: your_entry_id 90 | container: container_name 91 | command: your_command_here 92 | ``` 93 | 94 | ### Example: Run a Database Backup in a Container 95 | 96 | ```yaml 97 | service: unraid.execute_in_container 98 | data: 99 | entry_id: your_entry_id 100 | container: mariadb 101 | command: "mysqldump -u root -ppassword --all-databases > /backup/db_backup.sql" 102 | ``` 103 | 104 | ### Example: Update Package Lists in a Container 105 | 106 | ```yaml 107 | service: unraid.execute_in_container 108 | data: 109 | entry_id: your_entry_id 110 | container: ubuntu-container 111 | command: "apt-get update" 112 | ``` 113 | 114 | ## Best Practices 115 | 116 | ### Container Management 117 | 118 | 1. **Graceful Shutdown**: Always use the proper stop methods rather than force-stopping containers to prevent data corruption 119 | 2. **Status Verification**: Check container status before sending commands 120 | 3. **Health Monitoring**: Monitor container health and set up automation to restart unhealthy containers 121 | 4. **Resource Awareness**: Be mindful of starting resource-intensive containers simultaneously 122 | 123 | ### Container Naming 124 | 125 | For best compatibility with Home Assistant: 126 | 127 | 1. Use simple, consistent naming for containers in Unraid 128 | 2. Avoid special characters in container names 129 | 3. Be aware that entity IDs in Home Assistant will convert spaces and special characters to underscores 130 | 131 | ## Automation Ideas 132 | 133 | ### Monitor and Restart Unhealthy Containers 134 | 135 | ```yaml 136 | automation: 137 | - alias: "Restart Unhealthy Container" 138 | trigger: 139 | - platform: state 140 | entity_id: sensor.unraid_docker_nginx_health 141 | to: "unhealthy" 142 | for: 143 | minutes: 2 144 | action: 145 | - service: unraid.docker_restart 146 | data: 147 | entry_id: your_entry_id 148 | container: "nginx" 149 | - service: notify.mobile_app 150 | data: 151 | title: "Container Restarted" 152 | message: "NGINX container was unhealthy and has been restarted." 153 | ``` 154 | 155 | ### Schedule Container Maintenance 156 | 157 | ```yaml 158 | automation: 159 | - alias: "Weekly Container Maintenance" 160 | trigger: 161 | - platform: time 162 | at: "03:00:00" 163 | condition: 164 | - condition: time 165 | weekday: 166 | - mon 167 | action: 168 | - service: unraid.docker_restart 169 | data: 170 | entry_id: your_entry_id 171 | container: "database" 172 | - service: unraid.execute_in_container 173 | data: 174 | entry_id: your_entry_id 175 | container: "database" 176 | command: "/usr/local/bin/db_optimize.sh" 177 | ``` 178 | 179 | ### Load Balancing 180 | 181 | ```yaml 182 | automation: 183 | - alias: "Smart Container Load Balancing" 184 | trigger: 185 | - platform: numeric_state 186 | entity_id: sensor.unraid_cpu_usage 187 | above: 80 188 | for: 189 | minutes: 5 190 | action: 191 | - service: unraid.docker_pause 192 | data: 193 | entry_id: your_entry_id 194 | container: "non_critical_service" 195 | - service: notify.mobile_app 196 | data: 197 | title: "Load Balancing" 198 | message: "Paused non-critical services due to high CPU usage." 199 | ``` 200 | 201 | ## Troubleshooting 202 | 203 | ### Container Controls Not Working 204 | 205 | If you're having issues controlling Docker containers: 206 | 207 | 1. **Check Docker Service**: Ensure the Docker service is running on your Unraid server 208 | 2. **Verify Names**: Container names in service calls must match exactly with Unraid 209 | 3. **Check Permissions**: Make sure your Unraid user has permissions to control Docker 210 | 4. **Container State**: The container might be in a transitional state, check Unraid UI 211 | 5. **SSH Connection**: Verify the SSH connection between Home Assistant and Unraid is working 212 | 213 | ### Delayed Status Updates 214 | 215 | If container status doesn't update promptly: 216 | 217 | 1. **Update Interval**: The status may not update until the next polling interval 218 | 2. **UI Refresh**: Try refreshing the Home Assistant UI 219 | 3. **Restart Integration**: In extreme cases, try reloading the integration 220 | 221 | ## Advanced Configuration 222 | 223 | For advanced users, consider creating custom scripts on your Unraid server that can manage multiple containers or perform complex operations, then call these scripts from Home Assistant using the `unraid.execute_command` service. -------------------------------------------------------------------------------- /custom_components/unraid/quality_scale.yaml: -------------------------------------------------------------------------------- 1 | # Integration Quality Scale Assessment for Unraid Integration 2 | # Based on Home Assistant Integration Quality Scale requirements 3 | # https://developers.home-assistant.io/docs/core/integration-quality-scale/ 4 | 5 | rules: 6 | # 🥉 BRONZE TIER REQUIREMENTS 7 | action-setup: 8 | status: done 9 | comment: Services are properly registered in async_setup via services.py module 10 | 11 | appropriate-polling: 12 | status: done 13 | comment: Configurable polling intervals (1-60 min general, 5min-24h disk) with intelligent caching 14 | 15 | brands: 16 | status: exempt 17 | comment: No specific branding assets required for Unraid server integration 18 | 19 | common-modules: 20 | status: done 21 | comment: Well-organized API modules, diagnostics, sensors, and utilities in separate packages 22 | 23 | config-flow-test-coverage: 24 | status: not_done 25 | comment: Config flow exists but lacks comprehensive automated test coverage 26 | 27 | config-flow: 28 | status: done 29 | comment: Full UI-based setup with validation, options flow, and reauth flow implemented 30 | 31 | dependency-transparency: 32 | status: done 33 | comment: Clear requirements in manifest.json (aiofiles>=23.2.1, asyncssh) 34 | 35 | docs-actions: 36 | status: not_done 37 | comment: Documentation site not accessible, service descriptions exist in strings.json 38 | 39 | docs-high-level-description: 40 | status: not_done 41 | comment: Documentation site (https://domalab.github.io/ha-unraid/) not accessible 42 | 43 | docs-installation-instructions: 44 | status: not_done 45 | comment: Documentation site not accessible for step-by-step instructions 46 | 47 | docs-removal-instructions: 48 | status: not_done 49 | comment: Documentation site not accessible for removal instructions 50 | 51 | entity-event-setup: 52 | status: done 53 | comment: Entities properly subscribe to coordinator updates in lifecycle methods 54 | 55 | entity-unique-id: 56 | status: done 57 | comment: All entities implement unique IDs using domain_device_entity pattern 58 | 59 | has-entity-name: 60 | status: done 61 | comment: Entities use has_entity_name = True with proper naming conventions 62 | 63 | runtime-data: 64 | status: not_done 65 | comment: Uses hass.data[DOMAIN] instead of ConfigEntry.runtime_data 66 | 67 | test-before-configure: 68 | status: done 69 | comment: Config flow validates connection with UnraidAPI.ping() before setup 70 | 71 | test-before-setup: 72 | status: done 73 | comment: Integration tests connection during async_setup_entry with proper error handling 74 | 75 | unique-config-entry: 76 | status: done 77 | comment: Uses hostname as unique_id to prevent duplicate entries 78 | 79 | # 🥈 SILVER TIER REQUIREMENTS 80 | action-exceptions: 81 | status: done 82 | comment: Services raise appropriate exceptions with error handling decorators 83 | 84 | config-entry-unloading: 85 | status: done 86 | comment: Proper unloading implemented in async_unload_entry with coordinator cleanup 87 | 88 | docs-configuration-parameters: 89 | status: not_done 90 | comment: Documentation site not accessible for configuration parameter descriptions 91 | 92 | docs-installation-parameters: 93 | status: not_done 94 | comment: Documentation site not accessible for installation parameter descriptions 95 | 96 | entity-unavailable: 97 | status: done 98 | comment: Entities marked unavailable when coordinator data is None or connection fails 99 | 100 | integration-owner: 101 | status: done 102 | comment: Active codeowner @domalab listed in manifest.json 103 | 104 | log-when-unavailable: 105 | status: done 106 | comment: Proper logging for connection issues with rate limiting to avoid spam 107 | 108 | parallel-updates: 109 | status: done 110 | comment: Coordinator implements proper update coordination with configurable intervals 111 | 112 | reauthentication-flow: 113 | status: done 114 | comment: Reauth flow implemented in config_flow.py for credential updates 115 | 116 | test-coverage: 117 | status: not_done 118 | comment: No automated test files found, lacks 95% test coverage requirement 119 | 120 | # 🥇 GOLD TIER REQUIREMENTS 121 | devices: 122 | status: done 123 | comment: Creates device entries for Unraid server with proper device info 124 | 125 | diagnostics: 126 | status: done 127 | comment: Comprehensive diagnostics implementation with system health checks 128 | 129 | discovery-update-info: 130 | status: exempt 131 | comment: Local polling integration, discovery not applicable for SSH-based connections 132 | 133 | discovery: 134 | status: exempt 135 | comment: Unraid servers require manual configuration (SSH credentials) 136 | 137 | docs-data-update: 138 | status: not_done 139 | comment: Documentation site not accessible for data update descriptions 140 | 141 | docs-examples: 142 | status: not_done 143 | comment: Documentation site not accessible for automation examples 144 | 145 | docs-known-limitations: 146 | status: not_done 147 | comment: Documentation site not accessible for known limitations 148 | 149 | docs-supported-devices: 150 | status: not_done 151 | comment: Documentation site not accessible for supported device information 152 | 153 | docs-supported-functions: 154 | status: not_done 155 | comment: Documentation site not accessible for supported functionality descriptions 156 | 157 | docs-troubleshooting: 158 | status: not_done 159 | comment: Documentation site not accessible for troubleshooting information 160 | 161 | docs-use-cases: 162 | status: not_done 163 | comment: Documentation site not accessible for use case descriptions 164 | 165 | dynamic-devices: 166 | status: done 167 | comment: Supports dynamic Docker containers, VMs, and disk detection 168 | 169 | entity-category: 170 | status: done 171 | comment: Entities properly categorized (diagnostic, config) where appropriate 172 | 173 | entity-device-class: 174 | status: done 175 | comment: Sensors use appropriate device classes (temperature, data_size, power, etc.) 176 | 177 | entity-disabled-by-default: 178 | status: done 179 | comment: Less common entities disabled by default with entity registry 180 | 181 | entity-translations: 182 | status: partial 183 | comment: Basic translations in strings.json, but not comprehensive 184 | 185 | exception-translations: 186 | status: not_done 187 | comment: Exception messages not fully translatable 188 | 189 | icon-translations: 190 | status: not_done 191 | comment: Icons not translatable 192 | 193 | reconfiguration-flow: 194 | status: done 195 | comment: Options flow allows reconfiguration of polling intervals and UPS settings 196 | 197 | repair-issues: 198 | status: done 199 | comment: Comprehensive repair flows for connection, disk health, and array issues 200 | 201 | stale-devices: 202 | status: done 203 | comment: Migration system cleans up duplicate entities and stale devices 204 | 205 | # 🏆 PLATINUM TIER REQUIREMENTS 206 | async-dependency: 207 | status: done 208 | comment: Uses asyncssh for async SSH connections 209 | 210 | inject-websession: 211 | status: exempt 212 | comment: SSH-based integration doesn't use HTTP websessions 213 | 214 | strict-typing: 215 | status: partial 216 | comment: Type hints present but not comprehensive, py.typed file exists 217 | -------------------------------------------------------------------------------- /custom_components/unraid/api/userscript_operations.py: -------------------------------------------------------------------------------- 1 | """User script operations for Unraid.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Dict, List, Any 6 | import asyncio 7 | import asyncssh # type: ignore 8 | 9 | _LOGGER = logging.getLogger(__name__) 10 | 11 | class UserScriptOperationsMixin: 12 | """Mixin for user script related operations.""" 13 | 14 | async def get_user_scripts(self) -> List[Dict[str, Any]]: 15 | """Fetch information about user scripts using a batched command.""" 16 | try: 17 | _LOGGER.debug("Fetching user scripts with batched command") 18 | # Use a single command to check if plugin is installed and get script information 19 | cmd = ( 20 | "if [ -d /boot/config/plugins/user.scripts/scripts ]; then " 21 | " for script in $(ls -1 /boot/config/plugins/user.scripts/scripts 2>/dev/null); do " 22 | " name=$(cat /boot/config/plugins/user.scripts/scripts/$script/name 2>/dev/null || echo $script); " 23 | " desc=$(cat /boot/config/plugins/user.scripts/scripts/$script/description 2>/dev/null || echo ''); " 24 | " echo \"$script|$name|$desc\"; " 25 | " done; " 26 | "else " 27 | " echo 'not_installed'; " 28 | "fi" 29 | ) 30 | 31 | result = await self.execute_command(cmd) 32 | 33 | if result.exit_status != 0 or result.stdout.strip() == 'not_installed': 34 | _LOGGER.debug("User scripts plugin not installed") 35 | return [] 36 | 37 | scripts = [] 38 | for line in result.stdout.splitlines(): 39 | if not line.strip() or '|' not in line: 40 | continue 41 | 42 | try: 43 | parts = line.split('|') 44 | if len(parts) >= 3: 45 | script_id = parts[0].strip() 46 | name = parts[1].strip() 47 | description = parts[2].strip() 48 | 49 | scripts.append({ 50 | "id": script_id, 51 | "name": name, 52 | "description": description 53 | }) 54 | except Exception as err: 55 | _LOGGER.warning("Error parsing user script line '%s': %s", line, err) 56 | 57 | return scripts 58 | 59 | except Exception as err: 60 | _LOGGER.debug("Error getting user scripts (plugin might not be installed): %s", str(err)) 61 | # Fallback to original implementation 62 | try: 63 | return await self._get_user_scripts_original() 64 | except Exception as fallback_err: 65 | _LOGGER.error("Fallback user scripts method also failed: %s", fallback_err) 66 | return [] 67 | 68 | async def _get_user_scripts_original(self) -> List[Dict[str, Any]]: 69 | """Original implementation of user scripts collection as fallback.""" 70 | try: 71 | # Check if user scripts plugin is installed first 72 | check_result = await self.execute_command( 73 | "[ -d /boot/config/plugins/user.scripts/scripts ] && echo 'exists'" 74 | ) 75 | 76 | if check_result.exit_status == 0 and "exists" in check_result.stdout: 77 | result = await self.execute_command( 78 | "ls -1 /boot/config/plugins/user.scripts/scripts 2>/dev/null" 79 | ) 80 | if result.exit_status == 0: 81 | return [{"name": script.strip()} for script in result.stdout.splitlines()] 82 | 83 | # If not installed or no scripts, return empty list without error 84 | return [] 85 | except Exception as err: 86 | _LOGGER.debug("Error in original user scripts method: %s", str(err)) 87 | return [] 88 | 89 | async def execute_user_script(self, script_name: str, background: bool = False) -> str: 90 | """Execute a user script.""" 91 | try: 92 | _LOGGER.debug("Executing user script: %s", script_name) 93 | 94 | # Build proper script paths 95 | script_dir = f"/boot/config/plugins/user.scripts/scripts/{script_name}" 96 | script_path = f"{script_dir}/script" 97 | 98 | # Check if script exists first 99 | check_result = await self.execute_command(f'test -f "{script_path}" && echo "exists"') 100 | if check_result.exit_status != 0 or "exists" not in check_result.stdout: 101 | _LOGGER.error("Script %s not found at %s", script_name, script_path) 102 | return "" 103 | 104 | # First try direct execution 105 | try: 106 | _LOGGER.debug("Attempting direct script execution: %s", script_path) 107 | result = await self.execute_command(f'bash "{script_path}"') 108 | if result.exit_status == 0: 109 | return result.stdout 110 | except Exception as err: 111 | _LOGGER.debug("Direct execution failed, trying PHP conversion: %s", err) 112 | 113 | # If direct execution fails, try PHP conversion 114 | php_cmd = ( 115 | f'php -r \'$_POST["action"]="convertScript"; ' 116 | f'$_POST["path"]="{script_path}"; ' 117 | f'include("/usr/local/emhttp/plugins/user.scripts/exec.php");\'' 118 | ) 119 | 120 | _LOGGER.debug("Running PHP convert command: %s", php_cmd) 121 | convert_result = await self.execute_command(php_cmd) 122 | 123 | if convert_result.exit_status != 0: 124 | _LOGGER.error( 125 | "Failed to convert script %s: %s", 126 | script_name, 127 | convert_result.stderr or convert_result.stdout 128 | ) 129 | return "" 130 | 131 | _LOGGER.debug("Script conversion output: %s", convert_result.stdout) 132 | 133 | # Execute the script 134 | if background: 135 | result = await self.execute_command(f'nohup "{script_path}" > /dev/null 2>&1 &') 136 | else: 137 | result = await self.execute_command(f'bash "{script_path}"') 138 | 139 | if result.exit_status != 0: 140 | _LOGGER.error( 141 | "Script %s failed with exit status %d: %s", 142 | script_name, 143 | result.exit_status, 144 | result.stderr or result.stdout 145 | ) 146 | return "" 147 | 148 | return result.stdout 149 | 150 | except Exception as e: 151 | _LOGGER.error("Error executing user script %s: %s", script_name, str(e)) 152 | return "" 153 | 154 | async def stop_user_script(self, script_name: str) -> str: 155 | """Stop a user script.""" 156 | try: 157 | _LOGGER.debug("Stopping user script: %s", script_name) 158 | result = await self.execute_command(f"pkill -f '{script_name}'") 159 | if result.exit_status != 0: 160 | _LOGGER.error( 161 | "Stopping user script %s failed with exit status %d", 162 | script_name, 163 | result.exit_status 164 | ) 165 | return "" 166 | return result.stdout 167 | except (asyncssh.Error, asyncio.TimeoutError, OSError, ValueError) as e: 168 | _LOGGER.error("Error stopping user script %s: %s", script_name, str(e)) 169 | return "" -------------------------------------------------------------------------------- /docs/advanced/user-scripts.md: -------------------------------------------------------------------------------- 1 | # User Scripts 2 | 3 | The Unraid Integration allows you to execute and manage user scripts on your Unraid server directly from Home Assistant. This guide explains how to effectively use these features. 4 | 5 | ## Understanding User Scripts 6 | 7 | Unraid user scripts are custom scripts created on your Unraid server, typically stored in the `/boot/config/plugins/user.scripts/scripts/` directory. These scripts can perform various tasks, from system maintenance to automated backups and more. 8 | 9 | ## Available Features 10 | 11 | The user script management features include: 12 | 13 | - **Script Execution**: Run user scripts remotely from Home Assistant 14 | - **Script Termination**: Stop running scripts 15 | - **Execution Status**: Monitor script execution status 16 | - **Button Controls**: Optional buttons to trigger scripts directly from the UI 17 | 18 | ## Accessing User Scripts 19 | 20 | The integration accesses user scripts that are set up in the Unraid "User Scripts" plugin. If you don't already have this plugin installed on your Unraid server, you'll need to: 21 | 22 | 1. Open your Unraid Web UI 23 | 2. Go to "Apps" (Community Applications) 24 | 3. Search for "User Scripts" 25 | 4. Install the User Scripts plugin 26 | 5. Create your scripts using the plugin interface 27 | 28 | ## Executing User Scripts 29 | 30 | You can execute a user script using the `unraid.execute_user_script` service: 31 | 32 | ```yaml 33 | service: unraid.execute_user_script 34 | data: 35 | entry_id: your_entry_id 36 | script_name: "script_name.sh" 37 | ``` 38 | 39 | Where: 40 | - `your_entry_id` is your Unraid integration entry ID 41 | - `script_name.sh` is the exact name of the script as shown in the User Scripts plugin 42 | 43 | ### Example: Run a Backup Script 44 | 45 | ```yaml 46 | service: unraid.execute_user_script 47 | data: 48 | entry_id: your_entry_id 49 | script_name: "backup_appdata.sh" 50 | ``` 51 | 52 | ## Stopping User Scripts 53 | 54 | If you need to stop a running script, you can use the `unraid.stop_user_script` service: 55 | 56 | ```yaml 57 | service: unraid.stop_user_script 58 | data: 59 | entry_id: your_entry_id 60 | script_name: "script_name.sh" 61 | ``` 62 | 63 | !!! warning "Script Termination" 64 | Forcefully stopping a script may leave background processes running or resources allocated. Use this feature with caution. 65 | 66 | ## Button Controls 67 | 68 | The integration can optionally create button entities for your user scripts, making them easily accessible from the Home Assistant UI. This feature is disabled by default. 69 | 70 | To enable buttons for user scripts: 71 | 72 | 1. Go to Configuration → Integrations 73 | 2. Find the Unraid integration 74 | 3. Click "Options" 75 | 4. Enable the "Create buttons for user scripts" option 76 | 5. Save your changes 77 | 78 | Once enabled, each user script will appear as a button entity in Home Assistant. The entity ID will follow the format `button.unraid_script_[script_name]`. 79 | 80 | ## Best Practices 81 | 82 | ### Script Creation 83 | 84 | 1. **Error Handling**: Include proper error handling in your scripts 85 | 2. **Logging**: Add logging to help with troubleshooting 86 | 3. **Idempotence**: Scripts should be idempotent (can be run multiple times without side effects) 87 | 4. **Status Indication**: Return proper exit codes to indicate success or failure 88 | 89 | ### Example Script Structure 90 | 91 | Here's a recommended structure for Unraid user scripts: 92 | 93 | ```bash 94 | #!/bin/bash 95 | # Description: My Useful Script 96 | # Author: Your Name 97 | # Date: YYYY-MM-DD 98 | 99 | # Setup error handling 100 | set -e 101 | trap 'echo "Error occurred at line $LINENO"; exit 1' ERR 102 | 103 | # Log start 104 | echo "Starting script $(basename "$0") at $(date)" 105 | 106 | # Main script content 107 | # ... 108 | 109 | # Log completion 110 | echo "Script completed successfully at $(date)" 111 | exit 0 112 | ``` 113 | 114 | ## Automation Examples 115 | 116 | ### Scheduled Backup with Notification 117 | 118 | ```yaml 119 | automation: 120 | - alias: "Weekly Appdata Backup" 121 | trigger: 122 | - platform: time 123 | at: "03:00:00" 124 | condition: 125 | - condition: time 126 | weekday: 127 | - sun 128 | action: 129 | - service: unraid.execute_user_script 130 | data: 131 | entry_id: your_entry_id 132 | script_name: "backup_appdata.sh" 133 | - service: notify.mobile_app 134 | data: 135 | title: "Backup Started" 136 | message: "Weekly appdata backup has started." 137 | ``` 138 | 139 | ### User Script with Parameter Passing 140 | 141 | While the integration doesn't directly support passing parameters to user scripts, you can work around this by: 142 | 143 | 1. Creating multiple scripts for different parameter sets 144 | 2. Using environment variables in your scripts 145 | 3. Creating a wrapper script that accepts parameters via environment variables 146 | 147 | Example wrapper script: 148 | 149 | ```bash 150 | #!/bin/bash 151 | # File: backup_with_params.sh 152 | 153 | # Default values 154 | BACKUP_DESTINATION="/mnt/user/backups" 155 | COMPRESSION_LEVEL="9" 156 | 157 | # Override with environment variables if they exist 158 | [ -n "$CUSTOM_DESTINATION" ] && BACKUP_DESTINATION="$CUSTOM_DESTINATION" 159 | [ -n "$CUSTOM_COMPRESSION" ] && COMPRESSION_LEVEL="$CUSTOM_COMPRESSION" 160 | 161 | # Execute the actual backup with parameters 162 | /mnt/user/scripts/actual_backup.sh "$BACKUP_DESTINATION" "$COMPRESSION_LEVEL" 163 | ``` 164 | 165 | Then in Home Assistant: 166 | 167 | ```yaml 168 | service: unraid.execute_command 169 | data: 170 | entry_id: your_entry_id 171 | command: "CUSTOM_DESTINATION='/mnt/user/special' CUSTOM_COMPRESSION='5' /boot/config/plugins/user.scripts/scripts/backup_with_params.sh" 172 | ``` 173 | 174 | ## Troubleshooting 175 | 176 | ### Script Not Found 177 | 178 | If you get a "Script not found" error: 179 | 180 | 1. **Check Name**: Verify the exact script name (case-sensitive) 181 | 2. **Plugin Installed**: Make sure the User Scripts plugin is installed on Unraid 182 | 3. **Script Location**: Verify the script is properly registered in the User Scripts plugin 183 | 184 | ### Script Execution Fails 185 | 186 | If the script runs but fails: 187 | 188 | 1. **Script Permissions**: Ensure the script has execute permissions (`chmod +x script.sh`) 189 | 2. **Error Handling**: Check if your script has proper error handling 190 | 3. **Dependencies**: Verify all required dependencies are installed 191 | 4. **Unraid UI**: Try running the script directly from the Unraid UI to see specific errors 192 | 193 | ### Finding Your Entry ID 194 | 195 | Not sure what your entry_id is? Here's how to find it: 196 | 197 | 1. Go to Configuration → Integrations 198 | 2. Find your Unraid integration 199 | 3. Click on your device entity 200 | 4. Click on UNRAID under Device Info 201 | 5. The long string in the URL is your entry_id 202 | * Example URL: `/config/integrations/integration/unraid#config_entry/1234abcd5678efgh` 203 | * Your entry_id would be: `1234abcd5678efgh` 204 | 205 | ## Advanced Usage 206 | 207 | ### Creating Complex Scripts 208 | 209 | For more complex operations, consider: 210 | 211 | 1. **Bash Functions**: Organize your code into functions 212 | 2. **External Dependencies**: Use tools and scripts already available on Unraid 213 | 3. **Status Files**: Use status files to track long-running operations 214 | 4. **Locking**: Implement file locks to prevent concurrent execution 215 | 216 | ### Monitoring Script Execution 217 | 218 | For long-running scripts, you can: 219 | 220 | 1. **Create Status Sensors**: Have scripts update a file that Home Assistant monitors 221 | 2. **Log Parsing**: Parse script logs for status updates 222 | 3. **Completion Notification**: Have scripts trigger a Home Assistant webhook upon completion -------------------------------------------------------------------------------- /custom_components/unraid/sensors/base.py: -------------------------------------------------------------------------------- 1 | """Base sensor implementations for Unraid.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Any, Protocol 6 | from datetime import datetime, timezone 7 | 8 | from homeassistant.components.sensor import ( # type: ignore 9 | SensorEntity, 10 | ) 11 | from homeassistant.const import EntityCategory # type: ignore 12 | from homeassistant.core import callback # type: ignore 13 | from homeassistant.helpers.update_coordinator import ( # type: ignore 14 | CoordinatorEntity, 15 | ) 16 | from homeassistant.helpers.typing import StateType # type: ignore 17 | from homeassistant.helpers.entity import DeviceInfo # type: ignore 18 | 19 | from ..const import DOMAIN 20 | from .const import UnraidSensorEntityDescription 21 | from ..entity_naming import EntityNaming 22 | 23 | _LOGGER = logging.getLogger(__name__) 24 | 25 | class UnraidDataProtocol(Protocol): 26 | """Protocol for accessing coordinator data.""" 27 | 28 | @property 29 | def data(self) -> dict[str, Any]: 30 | """Return coordinator data.""" 31 | 32 | @property 33 | def hostname(self) -> str: 34 | """Return server hostname.""" 35 | 36 | @property 37 | def last_update_success(self) -> bool: 38 | """Return if last update was successful.""" 39 | 40 | class SensorUpdateMixin: 41 | """Mixin for sensor update handling.""" 42 | 43 | def __init__(self) -> None: 44 | """Initialize the mixin.""" 45 | self._last_update: datetime | None = None 46 | self._error_count: int = 0 47 | self._last_value: StateType = None 48 | 49 | def _handle_update_error(self, err: Exception) -> None: 50 | """Handle update errors with exponential backoff.""" 51 | self._error_count += 1 52 | if self._error_count <= 3: # Log first 3 errors 53 | _LOGGER.error( 54 | "Error updating sensor %s: %s", 55 | self.entity_description.key, 56 | err 57 | ) 58 | 59 | def _reset_error_count(self) -> None: 60 | """Reset error count on successful update.""" 61 | if self._error_count > 0: 62 | self._error_count = 0 63 | _LOGGER.debug( 64 | "Reset error count for sensor %s after successful update", 65 | self.entity_description.key 66 | ) 67 | 68 | class ValueValidationMixin: 69 | """Mixin for sensor value validation.""" 70 | 71 | def _validate_value( 72 | self, 73 | value: StateType, 74 | min_value: float | None = None, 75 | max_value: float | None = None 76 | ) -> StateType | None: 77 | """Validate sensor value against bounds.""" 78 | if value is None: 79 | return None 80 | 81 | try: 82 | if isinstance(value, (int, float)): 83 | if min_value is not None and value < min_value: 84 | _LOGGER.warning( 85 | "Value %s below minimum %s for sensor %s", 86 | value, 87 | min_value, 88 | self.entity_description.key 89 | ) 90 | return None 91 | if max_value is not None and value > max_value: 92 | _LOGGER.warning( 93 | "Value %s above maximum %s for sensor %s", 94 | value, 95 | max_value, 96 | self.entity_description.key 97 | ) 98 | return None 99 | return value 100 | except (TypeError, ValueError) as err: 101 | _LOGGER.debug( 102 | "Value validation error for sensor %s: %s", 103 | self.entity_description.key, 104 | err 105 | ) 106 | return None 107 | 108 | class UnraidSensorBase(CoordinatorEntity, SensorEntity, SensorUpdateMixin, ValueValidationMixin): 109 | """Base class for Unraid sensors.""" 110 | 111 | entity_description: UnraidSensorEntityDescription 112 | 113 | def __init__( 114 | self, 115 | coordinator: UnraidDataProtocol, 116 | description: UnraidSensorEntityDescription, 117 | ) -> None: 118 | """Initialize the sensor.""" 119 | super().__init__(coordinator) 120 | SensorUpdateMixin.__init__(self) 121 | 122 | self.entity_description = description 123 | self._attr_has_entity_name = True 124 | 125 | # Initialize entity naming 126 | naming = EntityNaming( 127 | domain=DOMAIN, 128 | hostname=coordinator.hostname, 129 | component=description.key.split('_')[0] # Get first part of key as component 130 | ) 131 | 132 | # Set consistent entity ID 133 | self._attr_unique_id = naming.get_entity_id(description.key) 134 | 135 | # Set name with cleaner UI format 136 | self._attr_name = f"{description.name}" 137 | 138 | _LOGGER.debug("Base Entity initialized | unique_id: %s | name: %s | description.key: %s", 139 | self._attr_unique_id, self._attr_name, description.key) 140 | 141 | # Optional display settings from description 142 | if description.suggested_unit_of_measurement: 143 | self._attr_suggested_unit_of_measurement = description.suggested_unit_of_measurement 144 | 145 | if description.suggested_display_precision is not None: 146 | self._attr_suggested_display_precision = description.suggested_display_precision 147 | 148 | @property 149 | def device_info(self) -> DeviceInfo: 150 | """Return device information.""" 151 | return DeviceInfo( 152 | identifiers={(DOMAIN, self.coordinator.entry.entry_id)}, 153 | name=f"{self.coordinator.hostname.title()}", 154 | manufacturer="Lime Technology", 155 | model="Unraid Server", 156 | ) 157 | 158 | @property 159 | def native_value(self) -> StateType: 160 | """Return the state of the sensor.""" 161 | try: 162 | if not self.available: 163 | return None 164 | 165 | value = self.entity_description.value_fn(self.coordinator.data) 166 | self._last_value = value 167 | self._reset_error_count() 168 | return value 169 | 170 | except (KeyError, AttributeError, TypeError, ValueError) as err: 171 | self._handle_update_error(err) 172 | return self._last_value 173 | 174 | @property 175 | def available(self) -> bool: 176 | """Return if entity is available.""" 177 | if not self.coordinator.last_update_success: 178 | return False 179 | 180 | try: 181 | return self.entity_description.available_fn(self.coordinator.data) 182 | except (KeyError, AttributeError, TypeError, ValueError) as err: 183 | _LOGGER.debug( 184 | "Error checking availability for %s: %s", 185 | self.entity_description.key, 186 | err 187 | ) 188 | return False 189 | 190 | @callback 191 | def _handle_coordinator_update(self) -> None: 192 | """Handle updated data from the coordinator.""" 193 | self._last_update = datetime.now(timezone.utc) 194 | self.async_write_ha_state() 195 | 196 | class UnraidDiagnosticMixin: 197 | """Mixin for diagnostic sensors.""" 198 | 199 | def __init__(self) -> None: 200 | """Initialize diagnostic mixin.""" 201 | self._attr_entity_category = EntityCategory.DIAGNOSTIC 202 | 203 | class UnraidConfigMixin: 204 | """Mixin for configuration sensors.""" 205 | 206 | def __init__(self) -> None: 207 | """Initialize configuration mixin.""" 208 | self._attr_entity_category = EntityCategory.CONFIG 209 | -------------------------------------------------------------------------------- /docs/user-guide/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | This page provides solutions to common issues you might encounter when using the Unraid Integration for Home Assistant. 4 | 5 | ## Installation Issues 6 | 7 | ### Integration Not Found 8 | 9 | **Problem**: The Unraid integration doesn't appear in the integration list. 10 | 11 | **Solutions**: 12 | 1. Make sure you've installed the integration correctly 13 | 2. If installed via HACS, verify it shows as installed in HACS 14 | 3. Restart Home Assistant 15 | 4. Clear browser cache and reload the page 16 | 17 | ### Installation Fails 18 | 19 | **Problem**: The installation process fails or throws errors. 20 | 21 | **Solutions**: 22 | 1. Check Home Assistant logs for specific error messages 23 | 2. Verify you have the latest version of Home Assistant 24 | 3. Try the manual installation method if HACS installation fails 25 | 4. Make sure your custom_components directory has the correct permissions 26 | 27 | ## Connection Issues 28 | 29 | ### Can't Connect to Unraid Server 30 | 31 | **Problem**: Home Assistant can't establish a connection to your Unraid server. 32 | 33 | **Solutions**: 34 | 1. **Check SSH Status**: Ensure SSH is enabled on your Unraid server (Settings → Management Access) 35 | 2. **Verify Network Connectivity**: Make sure your Unraid server is online and accessible from Home Assistant 36 | 3. **Firewall Settings**: Check if any firewall is blocking SSH connections (port 22 by default) 37 | 4. **Try Different Address**: If using hostname, try using IP address instead, or vice versa 38 | 5. **Check Logs**: Review Home Assistant logs for specific error messages 39 | 40 | ### Authentication Failed 41 | 42 | **Problem**: Connection fails due to authentication issues. 43 | 44 | **Solutions**: 45 | 1. **Verify Credentials**: Double-check your username and password 46 | 2. **Special Characters**: If your password contains special characters, make sure they're properly escaped 47 | 3. **Change Password**: Try changing your Unraid password temporarily to something simpler (no special characters) 48 | 4. **User Permissions**: Ensure the user has sufficient permissions (usually 'root' is required) 49 | 50 | ### Connection Timeouts 51 | 52 | **Problem**: Connection attempts timeout. 53 | 54 | **Solutions**: 55 | 1. **Network Speed**: Check your network performance 56 | 2. **Server Load**: Verify your Unraid server isn't under heavy load 57 | 3. **Custom SSH Port**: If using a non-standard SSH port, make sure it's correctly specified 58 | 4. **MTU Settings**: Check MTU settings on your network devices 59 | 60 | ## Sensor Issues 61 | 62 | ### Missing Sensors 63 | 64 | **Problem**: Some expected sensors don't appear. 65 | 66 | **Solutions**: 67 | 1. **Wait for Update**: Some sensors might take time to appear after initial setup 68 | 2. **Reload Integration**: Try removing and re-adding the integration 69 | 3. **Feature Availability**: Certain sensors only appear if the feature is available on your server (e.g., UPS sensors) 70 | 4. **Check Logs**: Look for error messages related to sensor creation 71 | 72 | ### Incorrect Sensor Values 73 | 74 | **Problem**: Sensors show incorrect or unexpected values. 75 | 76 | **Solutions**: 77 | 1. **Update Interval**: Adjust the update interval in the integration configuration 78 | 2. **Server Load**: High server load can cause inaccurate readings 79 | 3. **Restart Integration**: Remove and re-add the integration 80 | 4. **Check Unraid Web UI**: Compare values with what's shown in the Unraid web interface 81 | 82 | ### No Updates to Sensors 83 | 84 | **Problem**: Sensor values don't update. 85 | 86 | **Solutions**: 87 | 1. **Check Update Interval**: Make sure the update interval isn't set too high 88 | 2. **Verify Connection**: Ensure the integration still has a connection to the server 89 | 3. **Restart Home Assistant**: Sometimes a full restart is needed 90 | 4. **Check Logs**: Look for error messages during update attempts 91 | 92 | ## Docker and VM Control Issues 93 | 94 | ### Can't Control Docker Containers 95 | 96 | **Problem**: Docker containers can't be started, stopped, or controlled. 97 | 98 | **Solutions**: 99 | 1. **Docker Service**: Verify Docker service is running on the Unraid server 100 | 2. **User Permissions**: Ensure the user has permissions to control Docker 101 | 3. **Container State**: The container might be in an inconsistent state; try managing it from the Unraid UI first 102 | 4. **Name Mismatch**: Container names in Home Assistant must match exactly with Unraid 103 | 104 | ### VM Controls Not Working 105 | 106 | **Problem**: Unable to control VMs from Home Assistant. 107 | 108 | **Solutions**: 109 | 1. **VM Service**: Make sure VM service is running on Unraid 110 | 2. **Libvirt Status**: Check if libvirt service is active 111 | 3. **VM State**: The VM might be in a transitional state 112 | 4. **Response Time**: VM operations can take time, be patient for status updates 113 | 114 | ### Delayed Responses 115 | 116 | **Problem**: Actions take a long time to reflect in the UI. 117 | 118 | **Solutions**: 119 | 1. **Update Interval**: Your update interval might be too long 120 | 2. **Server Load**: High server load can cause delays 121 | 3. **Network Latency**: Check your network connection 122 | 4. **Command Queue**: Multiple commands might be processing sequentially 123 | 124 | ## Service Call Issues 125 | 126 | ### Service Calls Failing 127 | 128 | **Problem**: Service calls return errors or don't execute. 129 | 130 | **Solutions**: 131 | 1. **Entry ID**: Make sure you're using the correct entry_id 132 | 2. **Service Parameters**: Verify all required parameters are provided 133 | 3. **Syntax**: Check for syntax errors in your service calls 134 | 4. **User Permissions**: Ensure the user has permissions to execute the commands 135 | 5. **Check Logs**: Look for specific error messages in the logs 136 | 137 | ### Finding Your Entry ID 138 | 139 | **Problem**: You don't know your entry_id for service calls. 140 | 141 | **Solution**: 142 | 1. Go to Configuration → Integrations 143 | 2. Find your Unraid integration 144 | 3. Click on your device entity 145 | 4. Click on UNRAID under Device Info 146 | 5. The long string in the URL is your entry_id 147 | * Example URL: `/config/integrations/integration/unraid#config_entry/1234abcd5678efgh` 148 | * Your entry_id would be: `1234abcd5678efgh` 149 | 150 | ## Performance Issues 151 | 152 | ### High CPU Usage 153 | 154 | **Problem**: The integration causes high CPU usage on Home Assistant. 155 | 156 | **Solutions**: 157 | 1. **Increase Intervals**: Set longer update intervals 158 | 2. **Reduce Entities**: Remove unnecessary entities if possible 159 | 3. **Check Logs**: Look for repeated errors or warnings 160 | 4. **Upgrade Hardware**: Consider upgrading your Home Assistant hardware 161 | 162 | ### Slow UI Response 163 | 164 | **Problem**: Home Assistant UI becomes slow after adding the integration. 165 | 166 | **Solutions**: 167 | 1. **Browser Performance**: Check browser resource usage 168 | 2. **HA Resource Usage**: Monitor Home Assistant resource usage 169 | 3. **Database Size**: Large databases can slow down performance 170 | 4. **Entity Count**: Having too many entities can impact performance 171 | 172 | ## Logs and Diagnostics 173 | 174 | If you're still experiencing issues, check the Home Assistant logs: 175 | 176 | 1. Go to Configuration → System → Logs 177 | 2. Filter for "unraid" to see integration-specific logs 178 | 3. Look for error messages or warnings 179 | 180 | For more detailed diagnostics: 181 | 182 | 1. Go to Configuration → Integrations 183 | 2. Find the Unraid integration 184 | 3. Click the "..." menu 185 | 4. Select "Download diagnostics" 186 | 187 | ## Still Having Issues? 188 | 189 | If you're still experiencing problems after trying these solutions: 190 | 191 | 1. Check the [GitHub Issues](https://github.com/domalab/ha-unraid/issues) to see if others have reported similar problems 192 | 2. Open a new issue on GitHub with: 193 | * Detailed description of the problem 194 | * Steps to reproduce 195 | * Home Assistant logs 196 | * Diagnostic information 197 | * Your environment details (HA version, Unraid version, etc.) -------------------------------------------------------------------------------- /custom_components/unraid/diagnostics/ups.py: -------------------------------------------------------------------------------- 1 | """UPS monitoring for Unraid.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Any 6 | 7 | from homeassistant.components.binary_sensor import BinarySensorDeviceClass # type: ignore 8 | from homeassistant.const import EntityCategory # type: ignore 9 | 10 | from .base import UnraidBinarySensorBase 11 | from .const import UnraidBinarySensorEntityDescription 12 | from ..coordinator import UnraidDataUpdateCoordinator 13 | # from ..const import DOMAIN 14 | # from ..helpers import EntityNaming 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | class UnraidUPSBinarySensor(UnraidBinarySensorBase): 19 | """Binary sensor for UPS monitoring.""" 20 | 21 | def __init__(self, coordinator: UnraidDataUpdateCoordinator) -> None: 22 | """Initialize UPS binary sensor.""" 23 | # Entity naming not used in this class 24 | # EntityNaming( 25 | # domain=DOMAIN, 26 | # hostname=coordinator.hostname, 27 | # component="ups" 28 | # ) 29 | 30 | description = UnraidBinarySensorEntityDescription( 31 | key="ups_status", 32 | name="UPS Status", 33 | device_class=BinarySensorDeviceClass.POWER, 34 | entity_category=EntityCategory.DIAGNOSTIC, 35 | icon="mdi:battery-medium", 36 | ) 37 | 38 | super().__init__(coordinator, description) 39 | 40 | _LOGGER.debug( 41 | "Initialized UPS binary sensor | name: %s", 42 | self._attr_name 43 | ) 44 | 45 | @property 46 | def available(self) -> bool: 47 | """Return True if entity is available.""" 48 | try: 49 | ups_info = self.coordinator.data.get("system_stats", {}).get("ups_info") 50 | has_ups = bool(ups_info) 51 | 52 | if not has_ups: 53 | _LOGGER.debug("No UPS info available in coordinator data") 54 | 55 | return self.coordinator.last_update_success and has_ups 56 | 57 | except Exception as err: 58 | _LOGGER.error("Error checking UPS availability: %s", err) 59 | return False 60 | 61 | @property 62 | def is_on(self) -> bool | None: 63 | """Return true if the UPS is online.""" 64 | try: 65 | status = self.coordinator.data["system_stats"].get("ups_info", {}).get("STATUS") 66 | if status is None: 67 | _LOGGER.debug("No UPS status available") 68 | return None 69 | 70 | is_online = status.upper() in ["ONLINE", "ON LINE"] 71 | _LOGGER.debug("UPS status: %s (online: %s)", status, is_online) 72 | return is_online 73 | 74 | except (KeyError, AttributeError, TypeError) as err: 75 | _LOGGER.debug("Error getting UPS status: %s", err) 76 | return None 77 | 78 | @property 79 | def extra_state_attributes(self) -> dict[str, Any]: 80 | """Return additional state attributes.""" 81 | try: 82 | ups_info = self.coordinator.data["system_stats"].get("ups_info", {}) 83 | 84 | # Base attributes 85 | attrs = { 86 | "model": ups_info.get("MODEL", "Unknown"), 87 | "status": ups_info.get("STATUS", "Unknown"), 88 | } 89 | 90 | # Store binary sensor data in coordinator for other sensors to use 91 | if "binary_sensors" not in self.coordinator.data: 92 | self.coordinator.data["binary_sensors"] = {} 93 | 94 | # Use entity_id as the key 95 | self.coordinator.data["binary_sensors"][self.entity_id] = { 96 | "state": self.state, 97 | "attributes": {}, # Will be populated below 98 | } 99 | 100 | # Add percentage values with validation 101 | for key, attr_name in [ 102 | ("BCHARGE", "battery_charge"), 103 | ("LOADPCT", "load_percentage") 104 | ]: 105 | if value := ups_info.get(key): 106 | try: 107 | # Ensure value is numeric and within range 108 | numeric_value = float(value) 109 | if 0 <= numeric_value <= 100: 110 | attrs[attr_name] = f"{numeric_value}%" 111 | else: 112 | _LOGGER.warning( 113 | "Invalid %s value: %s (expected 0-100)", 114 | key, 115 | value 116 | ) 117 | except (ValueError, TypeError) as err: 118 | _LOGGER.debug( 119 | "Error processing %s value: %s", 120 | key, 121 | err 122 | ) 123 | 124 | # Add time values 125 | if runtime := ups_info.get("TIMELEFT"): 126 | try: 127 | # Ensure runtime is numeric and positive 128 | runtime_value = float(runtime) 129 | if runtime_value >= 0: 130 | attrs["runtime_left"] = f"{runtime_value} minutes" 131 | else: 132 | _LOGGER.warning( 133 | "Invalid runtime value: %s (expected >= 0)", 134 | runtime 135 | ) 136 | except (ValueError, TypeError) as err: 137 | _LOGGER.debug( 138 | "Error processing runtime value: %s", 139 | err 140 | ) 141 | 142 | # Add power/voltage values with validation 143 | for key, attr_name, unit in [ 144 | ("NOMPOWER", "nominal_power", "W"), 145 | ("LINEV", "line_voltage", "V"), 146 | ("BATTV", "battery_voltage", "V") 147 | ]: 148 | if value := ups_info.get(key): 149 | try: 150 | # Ensure value is numeric and positive 151 | numeric_value = float(value) 152 | if numeric_value >= 0: 153 | attrs[attr_name] = f"{numeric_value}{unit}" 154 | else: 155 | _LOGGER.warning( 156 | "Invalid %s value: %s (expected >= 0)", 157 | key, 158 | value 159 | ) 160 | except (ValueError, TypeError) as err: 161 | _LOGGER.debug( 162 | "Error processing %s value: %s", 163 | key, 164 | err 165 | ) 166 | 167 | # Additional UPS details if available 168 | if firmware := ups_info.get("FIRMWARE"): 169 | attrs["firmware"] = firmware 170 | if serial := ups_info.get("SERIALNO"): 171 | attrs["serial_number"] = serial 172 | if manufacture_date := ups_info.get("MANDATE"): 173 | attrs["manufacture_date"] = manufacture_date 174 | 175 | _LOGGER.debug("UPS attributes: %s", attrs) 176 | 177 | # Store attributes in coordinator for other sensors to use 178 | if "binary_sensors" in self.coordinator.data and self.entity_id in self.coordinator.data["binary_sensors"]: 179 | self.coordinator.data["binary_sensors"][self.entity_id]["attributes"] = attrs.copy() 180 | 181 | return attrs 182 | 183 | except (KeyError, AttributeError, TypeError) as err: 184 | _LOGGER.debug("Error getting UPS attributes: %s", err) 185 | return {} 186 | 187 | @property 188 | def state(self) -> str: 189 | """Return the state of the sensor.""" 190 | if self.is_on is None: 191 | return "Unknown" 192 | return "Online" if self.is_on else "Offline" 193 | -------------------------------------------------------------------------------- /custom_components/unraid/api/ups_operations.py: -------------------------------------------------------------------------------- 1 | """UPS operations for Unraid.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Dict, Any 6 | 7 | import asyncio 8 | import asyncssh # type: ignore 9 | 10 | from ..const import ( 11 | UPS_METRICS, 12 | UPS_DEFAULT_POWER_FACTOR, 13 | ) 14 | 15 | _LOGGER = logging.getLogger(__name__) 16 | 17 | class UPSOperationsMixin: 18 | """Mixin for UPS-related operations.""" 19 | 20 | async def detect_ups(self) -> bool: 21 | """Attempt to detect if a UPS is connected.""" 22 | try: 23 | result = await self.execute_command("which apcaccess") 24 | if result.exit_status == 0: 25 | # apcaccess is installed, now check if it can communicate with a UPS 26 | result = await self.execute_command("apcaccess status") 27 | return result.exit_status == 0 28 | return False 29 | except (asyncssh.Error, OSError) as err: 30 | _LOGGER.debug( 31 | "Error during UPS detection: %s", 32 | err 33 | ) 34 | return False 35 | 36 | async def get_ups_info(self) -> Dict[str, Any]: 37 | """Fetch UPS information from the Unraid system.""" 38 | try: 39 | _LOGGER.debug("Fetching UPS info") 40 | # Check if apcupsd is installed and running first 41 | check_result = await self.execute_command( 42 | "command -v apcaccess >/dev/null 2>&1 && " 43 | "pgrep apcupsd >/dev/null 2>&1 && " 44 | "echo 'running'" 45 | ) 46 | 47 | if check_result.exit_status == 0 and "running" in check_result.stdout: 48 | result = await self.execute_command("apcaccess -u 2>/dev/null") 49 | if result.exit_status == 0: 50 | ups_data = {} 51 | for line in result.stdout.splitlines(): 52 | if ':' in line: 53 | key, value = line.split(':', 1) 54 | ups_data[key.strip()] = value.strip() 55 | 56 | # Add power factor info if not present 57 | if "POWERFACTOR" not in ups_data: 58 | ups_data["POWERFACTOR"] = str(UPS_DEFAULT_POWER_FACTOR) 59 | 60 | return ups_data 61 | 62 | # If not installed or not running, return empty dict without error 63 | return {} 64 | except (asyncssh.Error, OSError) as error: 65 | _LOGGER.debug( 66 | "Error getting UPS info (apcupsd might not be installed): %s", 67 | str(error) 68 | ) 69 | return {} 70 | 71 | def _validate_ups_metric(self, metric: str, value: str) -> Any: 72 | """Validate and process UPS metric values. 73 | 74 | Args: 75 | metric: Metric name 76 | value: Raw value string 77 | 78 | Returns: 79 | Processed value or None if validation fails 80 | """ 81 | if metric not in UPS_METRICS: 82 | return None 83 | 84 | try: 85 | # Clean up value string and handle special cases 86 | if isinstance(value, str): 87 | # Remove any non-numeric characters except decimals and negatives 88 | numeric_value = float(''.join( 89 | c for c in value if c.isdigit() or c in '.-' 90 | )) 91 | else: 92 | numeric_value = float(value) 93 | 94 | # Check range 95 | metric_info = UPS_METRICS[metric] 96 | if ( 97 | numeric_value < metric_info["min"] or 98 | numeric_value > metric_info["max"] 99 | ): 100 | _LOGGER.warning( 101 | "UPS metric %s value %f outside valid range [%f, %f]", 102 | metric, 103 | numeric_value, 104 | metric_info["min"], 105 | metric_info["max"] 106 | ) 107 | return None 108 | 109 | # Convert to integer for specific metrics 110 | if metric in ["NOMPOWER", "TIMELEFT"]: 111 | return int(numeric_value) 112 | 113 | return numeric_value 114 | 115 | except (ValueError, TypeError) as err: 116 | _LOGGER.debug( 117 | "Error processing UPS metric %s value '%s': %s", 118 | metric, 119 | value, 120 | err 121 | ) 122 | return None 123 | 124 | async def _validate_ups_connection(self) -> bool: 125 | """Validate UPS connection and communication. 126 | 127 | Returns: 128 | bool: True if UPS is properly connected and responding 129 | """ 130 | try: 131 | # First check if apcupsd is installed and running 132 | service_check = await self.execute_command( 133 | "systemctl is-active apcupsd" 134 | ) 135 | if service_check.exit_status != 0: 136 | _LOGGER.debug("apcupsd service not running") 137 | return False 138 | 139 | # Then verify we can actually communicate with a UPS 140 | result = await self.execute_command( 141 | "timeout 5 apcaccess status", 142 | timeout=10 # Allow some extra time for timeout command 143 | ) 144 | 145 | if result.exit_status != 0: 146 | _LOGGER.debug("Cannot communicate with UPS") 147 | return False 148 | 149 | # Verify we got valid data 150 | for line in result.stdout.splitlines(): 151 | if "STATUS" in line and "ONLINE" in line: 152 | return True 153 | 154 | _LOGGER.debug("UPS status check failed") 155 | return False 156 | 157 | except (asyncssh.Error, asyncio.TimeoutError, OSError) as err: 158 | _LOGGER.error("Error validating UPS connection: %s", err) 159 | return False 160 | 161 | async def get_ups_model(self) -> str: 162 | """Get UPS model information. 163 | 164 | Returns: 165 | str: UPS model name or 'Unknown' if not available 166 | """ 167 | try: 168 | result = await self.execute_command( 169 | "apcaccess -u | grep '^MODEL'" 170 | ) 171 | if result.exit_status == 0 and ':' in result.stdout: 172 | return result.stdout.split(':', 1)[1].strip() 173 | return "Unknown" 174 | except (asyncssh.Error, asyncio.TimeoutError, OSError) as err: 175 | _LOGGER.debug("Error getting UPS model: %s", err) 176 | return "Unknown" 177 | 178 | async def get_ups_status_summary(self) -> Dict[str, Any]: 179 | """Get a summary of critical UPS status information. 180 | 181 | Returns: 182 | Dict containing key UPS metrics and status 183 | """ 184 | try: 185 | info = await self.get_ups_info() 186 | 187 | # Process each metric through validation 188 | validated_metrics = { 189 | metric: self._validate_ups_metric(metric, info.get(metric, "0")) 190 | for metric in ["BCHARGE", "TIMELEFT", "LOADPCT", "NOMPOWER", "LINEV", "BATTV"] 191 | } 192 | 193 | return { 194 | "status": info.get("STATUS", "Unknown"), 195 | "battery_charge": validated_metrics["BCHARGE"], 196 | "runtime_left": validated_metrics["TIMELEFT"], 197 | "load_percent": validated_metrics["LOADPCT"], 198 | "nominal_power": validated_metrics["NOMPOWER"], 199 | "line_voltage": validated_metrics["LINEV"], 200 | "battery_voltage": validated_metrics["BATTV"] 201 | } 202 | except (asyncssh.Error, asyncio.TimeoutError, OSError) as err: 203 | _LOGGER.error("Error getting UPS status summary: %s", err) 204 | return { 205 | "status": "Error", 206 | "error": str(err) 207 | } 208 | -------------------------------------------------------------------------------- /custom_components/unraid/diagnostics.py: -------------------------------------------------------------------------------- 1 | """Diagnostics support for Unraid.""" 2 | from __future__ import annotations 3 | 4 | import json 5 | import logging 6 | from typing import Any 7 | 8 | from homeassistant.components.diagnostics import async_redact_data # type: ignore 9 | from homeassistant.config_entries import ConfigEntry # type: ignore 10 | from homeassistant.const import ( # type: ignore 11 | CONF_HOST, 12 | CONF_PASSWORD, 13 | CONF_PORT, 14 | CONF_USERNAME, 15 | ) 16 | from homeassistant.core import HomeAssistant # type: ignore 17 | 18 | from .const import DOMAIN 19 | from .diagnostics.system_health import SystemHealthDiagnostics 20 | 21 | _LOGGER = logging.getLogger(__name__) 22 | 23 | TO_REDACT = { 24 | CONF_HOST, 25 | CONF_PASSWORD, 26 | CONF_USERNAME, 27 | CONF_PORT, 28 | "UPSNAME", # UPS identifier 29 | "SERIALNO", # UPS serial number 30 | "HOSTNAME", # Server hostname 31 | "identifiers", # Device identifiers 32 | "serial", # Serial numbers 33 | "id", # IDs that might contain sensitive information 34 | } 35 | 36 | def format_bytes(bytes_value: int) -> str: 37 | """Format bytes into human readable sizes.""" 38 | for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB']: 39 | if bytes_value < 1024.0: 40 | return f"{bytes_value:.2f} {unit}" 41 | bytes_value /= 1024.0 42 | return f"{bytes_value:.2f} PB" 43 | 44 | async def async_get_config_entry_diagnostics( 45 | hass: HomeAssistant, 46 | entry: ConfigEntry, 47 | ) -> dict[str, Any]: 48 | """Return diagnostics for a config entry.""" 49 | coordinator = hass.data[DOMAIN][entry.entry_id] 50 | 51 | # Get system stats data 52 | system_stats = coordinator.data.get("system_stats", {}) 53 | 54 | # Run system health diagnostics 55 | try: 56 | system_health = SystemHealthDiagnostics(coordinator) 57 | health_data = await system_health.check_system_health() 58 | _LOGGER.debug("System health diagnostics completed with %d issues found", 59 | len(health_data.get("issues", []))) 60 | except Exception as err: 61 | _LOGGER.error("Error running system health diagnostics: %s", err) 62 | health_data = {"error": str(err)} 63 | 64 | # Process disk data with formatted sizes 65 | processed_disks = [] 66 | for disk in system_stats.get("individual_disks", []): 67 | if not isinstance(disk, dict): 68 | continue 69 | processed_disks.append({ 70 | "name": disk.get("name", "unknown"), 71 | "percentage": f"{disk.get('percentage', 0):.1f}%", 72 | "total_size": format_bytes(disk.get("total", 0)), 73 | "used_space": format_bytes(disk.get("used", 0)), 74 | "free_space": format_bytes(disk.get("free", 0)), 75 | "mount_point": disk.get("mount_point", "unknown"), 76 | "filesystem": disk.get("filesystem", "unknown"), 77 | "device": disk.get("device", "unknown"), 78 | }) 79 | 80 | # Process temperature data 81 | temp_data = system_stats.get("temperature_data", {}).get("sensors", {}) 82 | processed_temps = {} 83 | for sensor, data in temp_data.items(): 84 | processed_temps[sensor] = {k: v for k, v in data.items() if isinstance(v, (str, int, float))} 85 | 86 | # Create diagnostics data structure 87 | diagnostics_data = { 88 | "entry": { 89 | "entry_id": entry.entry_id, 90 | "version": entry.version, 91 | "domain": entry.domain, 92 | "title": entry.title, 93 | "options": async_redact_data(dict(entry.options), TO_REDACT), 94 | "data": async_redact_data(dict(entry.data), TO_REDACT), 95 | "integration_type": entry.domain, 96 | }, 97 | "system_health": health_data, 98 | "system_status": { 99 | "array_usage": { 100 | "percentage": f"{system_stats.get('array_usage', {}).get('percentage', 0):.1f}%", 101 | "total_size": format_bytes(system_stats.get('array_usage', {}).get('total', 0)), 102 | "used_space": format_bytes(system_stats.get('array_usage', {}).get('used', 0)), 103 | "free_space": format_bytes(system_stats.get('array_usage', {}).get('free', 0)), 104 | "status": system_stats.get('array_usage', {}).get('status', 'unknown'), 105 | }, 106 | "memory_usage": { 107 | "percentage": f"{system_stats.get('memory_usage', {}).get('percentage', 0):.1f}%", 108 | "total": format_bytes(system_stats.get('memory_usage', {}).get('total', 0)), 109 | "used": format_bytes(system_stats.get('memory_usage', {}).get('used', 0)), 110 | "free": format_bytes(system_stats.get('memory_usage', {}).get('free', 0)), 111 | }, 112 | "individual_disks": processed_disks, 113 | "temperatures": processed_temps, 114 | "docker_info": { 115 | "container_count": len(coordinator.data.get("docker_containers", [])), 116 | "running_count": sum(1 for c in coordinator.data.get("docker_containers", []) 117 | if c.get("status") == "running"), 118 | "containers": [ 119 | { 120 | "name": container.get("name"), 121 | "status": container.get("status"), 122 | "state": container.get("state", "unknown"), 123 | "autostart": container.get("autostart", False), 124 | } 125 | for container in coordinator.data.get("docker_containers", []) 126 | ], 127 | }, 128 | "vm_info": { 129 | "vm_count": len(coordinator.data.get("vms", [])), 130 | "running_count": sum(1 for vm in coordinator.data.get("vms", []) 131 | if vm.get("status") == "running"), 132 | "vms": [ 133 | { 134 | "name": vm.get("name"), 135 | "status": vm.get("status"), 136 | "autostart": vm.get("autostart", False), 137 | } 138 | for vm in coordinator.data.get("vms", []) 139 | ], 140 | }, 141 | }, 142 | } 143 | 144 | # Add UPS info if available 145 | if coordinator.has_ups and "ups_info" in system_stats: 146 | ups_info = system_stats["ups_info"] 147 | diagnostics_data["system_status"]["ups_info"] = async_redact_data({ 148 | "status": ups_info.get("STATUS"), 149 | "battery_charge": ups_info.get("BCHARGE"), 150 | "load_percentage": ups_info.get("LOADPCT"), 151 | "runtime_left": ups_info.get("TIMELEFT"), 152 | "nominal_power": ups_info.get("NOMPOWER"), 153 | "battery_voltage": ups_info.get("BATTV"), 154 | "line_voltage": ups_info.get("LINEV"), 155 | "model": ups_info.get("MODEL"), 156 | "firmware": ups_info.get("FIRMWARE"), 157 | }, TO_REDACT) 158 | 159 | # Add cache info if available 160 | cache_usage = system_stats.get("cache_usage") 161 | if cache_usage: 162 | diagnostics_data["system_status"]["cache_usage"] = { 163 | "percentage": f"{cache_usage.get('percentage', 0):.1f}%", 164 | "total_size": format_bytes(cache_usage.get('total', 0)), 165 | "used_space": format_bytes(cache_usage.get('used', 0)), 166 | "free_space": format_bytes(cache_usage.get('free', 0)), 167 | "filesystem": cache_usage.get("filesystem", "unknown"), 168 | } 169 | 170 | # Add parity info if available 171 | parity_info = coordinator.data.get("parity_info", {}) 172 | if parity_info: 173 | diagnostics_data["system_status"]["parity_info"] = { 174 | "status": parity_info.get("status", "unknown"), 175 | "last_check": parity_info.get("last_check", "unknown"), 176 | "next_check": parity_info.get("next_check", "unknown"), 177 | "duration": parity_info.get("duration", "unknown"), 178 | "speed": parity_info.get("last_speed", "unknown"), 179 | "errors": parity_info.get("errors", 0), 180 | } 181 | 182 | # Ensure all values are JSON serializable 183 | return json.loads(json.dumps(diagnostics_data)) 184 | -------------------------------------------------------------------------------- /docs/advanced/vm-control.md: -------------------------------------------------------------------------------- 1 | # VM Control 2 | 3 | The Unraid Integration provides comprehensive capabilities for monitoring and controlling virtual machines (VMs) running on your Unraid server. This guide explains how to effectively use these features. 4 | 5 | ## Available Features 6 | 7 | The VM management features include: 8 | 9 | - **Status Monitoring**: Real-time status of VMs (running, stopped, paused) 10 | - **Basic Controls**: Start and stop VMs through switches 11 | - **Advanced Controls**: Pause, resume, hibernate, restart, and force stop VMs through services 12 | - **VM Resource Monitoring**: Track CPU and memory usage (where available) 13 | 14 | ## VM Switches 15 | 16 | Each virtual machine on your Unraid server will appear as a switch entity in Home Assistant. The entity ID will be in the format `switch.unraid_vm_[vm_name]`, where `[vm_name]` is the name of your VM with special characters replaced. 17 | 18 | ### Using VM Switches 19 | 20 | VM switches provide basic on/off functionality: 21 | 22 | - **Turn On**: Starts the VM if it's stopped 23 | - **Turn Off**: Gracefully stops the VM if it's running 24 | 25 | You can use these switches in the Home Assistant UI, automations, scripts, and scenes like any other switch entity. 26 | 27 | ```yaml 28 | # Example: Turn on a VM 29 | service: switch.turn_on 30 | target: 31 | entity_id: switch.unraid_vm_windows_10 32 | ``` 33 | 34 | ```yaml 35 | # Example: Turn off a VM 36 | service: switch.turn_off 37 | target: 38 | entity_id: switch.unraid_vm_ubuntu_server 39 | ``` 40 | 41 | ## Advanced VM Controls 42 | 43 | For more advanced control, the integration provides several services: 44 | 45 | ### Pause a VM 46 | 47 | Pauses a running VM (freezes the VM's state in memory): 48 | 49 | ```yaml 50 | service: unraid.vm_pause 51 | data: 52 | entry_id: your_entry_id 53 | vm_name: vm_name 54 | ``` 55 | 56 | ### Resume a VM 57 | 58 | Resumes a paused VM: 59 | 60 | ```yaml 61 | service: unraid.vm_resume 62 | data: 63 | entry_id: your_entry_id 64 | vm_name: vm_name 65 | ``` 66 | 67 | ### Restart a VM 68 | 69 | Gracefully restarts a running VM: 70 | 71 | ```yaml 72 | service: unraid.vm_restart 73 | data: 74 | entry_id: your_entry_id 75 | vm_name: vm_name 76 | ``` 77 | 78 | ### Hibernate a VM 79 | 80 | Hibernates a running VM (suspends to disk): 81 | 82 | ```yaml 83 | service: unraid.vm_hibernate 84 | data: 85 | entry_id: your_entry_id 86 | vm_name: vm_name 87 | ``` 88 | 89 | ### Force Stop a VM 90 | 91 | Forcefully stops a VM (equivalent to pulling the power plug): 92 | 93 | ```yaml 94 | service: unraid.vm_force_stop 95 | data: 96 | entry_id: your_entry_id 97 | vm_name: vm_name 98 | ``` 99 | 100 | !!! warning "Force Stop" 101 | Using force stop should be a last resort as it may lead to data corruption or file system issues. Only use it when a VM is unresponsive to normal shutdown methods. 102 | 103 | !!! note "VM Names" 104 | The `vm_name` parameter should match the exact VM name as shown in the Unraid VM Manager, not the Home Assistant entity name. 105 | 106 | ## Best Practices 107 | 108 | ### VM Management 109 | 110 | 1. **Graceful Shutdown**: Always use proper shutdown methods when possible to prevent data corruption 111 | 2. **Status Verification**: Check VM status before sending commands 112 | 3. **Resource Awareness**: Be mindful of starting resource-intensive VMs simultaneously 113 | 4. **State Transitions**: Allow VMs sufficient time to complete start/stop operations before sending additional commands 114 | 115 | ### VM Naming 116 | 117 | For best compatibility with Home Assistant: 118 | 119 | 1. Use simple, consistent naming for VMs in Unraid 120 | 2. Avoid special characters in VM names 121 | 3. Be aware that entity IDs in Home Assistant will convert spaces and special characters to underscores 122 | 123 | ## Automation Ideas 124 | 125 | ### Scheduled VM Power Management 126 | 127 | ```yaml 128 | automation: 129 | - alias: "Start VM for Backup" 130 | trigger: 131 | - platform: time 132 | at: "01:00:00" 133 | condition: 134 | - condition: time 135 | weekday: 136 | - mon 137 | - wed 138 | - fri 139 | action: 140 | - service: switch.turn_on 141 | target: 142 | entity_id: switch.unraid_vm_backup_server 143 | - delay: "00:10:00" # Allow 10 minutes for VM to boot fully 144 | - service: unraid.execute_command 145 | data: 146 | entry_id: your_entry_id 147 | command: "ssh backup@backup-vm '/usr/local/bin/start_backup.sh'" 148 | - delay: "02:00:00" # Allow 2 hours for backup to complete 149 | - service: switch.turn_off 150 | target: 151 | entity_id: switch.unraid_vm_backup_server 152 | ``` 153 | 154 | ### Power Management Based on Presence 155 | 156 | ```yaml 157 | automation: 158 | - alias: "Start Gaming VM When Home" 159 | trigger: 160 | - platform: state 161 | entity_id: person.gamer 162 | from: "not_home" 163 | to: "home" 164 | condition: 165 | - condition: time 166 | after: "17:00:00" 167 | before: "23:00:00" 168 | - condition: state 169 | entity_id: switch.unraid_vm_gaming 170 | state: "off" 171 | action: 172 | - service: switch.turn_on 173 | target: 174 | entity_id: switch.unraid_vm_gaming 175 | - service: notify.mobile_app 176 | data: 177 | title: "Gaming VM Started" 178 | message: "Your gaming VM is starting up and will be ready in a few minutes." 179 | ``` 180 | 181 | ### Resource-Based VM Management 182 | 183 | ```yaml 184 | automation: 185 | - alias: "Hibernate VMs on High Server Load" 186 | trigger: 187 | - platform: numeric_state 188 | entity_id: sensor.unraid_cpu_usage 189 | above: 85 190 | for: 191 | minutes: 10 192 | action: 193 | - service: unraid.vm_hibernate 194 | data: 195 | entry_id: your_entry_id 196 | vm_name: "non_critical_vm" 197 | - service: notify.mobile_app 198 | data: 199 | title: "VM Hibernated" 200 | message: "Non-critical VM has been hibernated due to high server load." 201 | ``` 202 | 203 | ### Recovery Automations 204 | 205 | ```yaml 206 | automation: 207 | - alias: "Restart Frozen VM" 208 | trigger: 209 | - platform: state 210 | entity_id: binary_sensor.unraid_vm_frozen 211 | to: "on" 212 | for: 213 | minutes: 5 214 | action: 215 | - service: unraid.vm_force_stop 216 | data: 217 | entry_id: your_entry_id 218 | vm_name: "problematic_vm" 219 | - delay: "00:00:30" 220 | - service: switch.turn_on 221 | target: 222 | entity_id: switch.unraid_vm_problematic_vm 223 | - service: notify.mobile_app 224 | data: 225 | title: "VM Restarted" 226 | message: "A frozen VM has been forcefully restarted." 227 | ``` 228 | 229 | ## Troubleshooting 230 | 231 | ### VM Controls Not Working 232 | 233 | If you're having issues controlling VMs: 234 | 235 | 1. **Check VM Service**: Ensure VM service is running on your Unraid server 236 | 2. **Libvirt Status**: Verify libvirt service is active 237 | 3. **Check Permissions**: Make sure your Unraid user has permissions to control VMs 238 | 4. **VM State**: The VM might be in a transitional state, check Unraid UI 239 | 5. **SSH Connection**: Verify the SSH connection between Home Assistant and Unraid is working 240 | 241 | ### Delayed Status Updates 242 | 243 | If VM status doesn't update promptly: 244 | 245 | 1. **Update Interval**: The status may not update until the next polling interval 246 | 2. **UI Refresh**: Try refreshing the Home Assistant UI 247 | 3. **Restart Integration**: In extreme cases, try reloading the integration 248 | 249 | ### Status Showing Incorrect State 250 | 251 | If VM status seems incorrect: 252 | 253 | 1. **Check Unraid UI**: Verify the actual VM state in Unraid VM Manager 254 | 2. **State Transitions**: During state transitions, status might temporarily report incorrectly 255 | 3. **Reload Integration**: Try reloading the Unraid integration 256 | 257 | ## Advanced Configuration 258 | 259 | For advanced users, consider: 260 | 261 | 1. **Custom Scripts**: Create VM management scripts on your Unraid server 262 | 2. **Conditional Automations**: Create complex VM management based on multiple conditions 263 | 3. **VM Templates**: Use template VMs in Unraid that can be cloned and started through Home Assistant -------------------------------------------------------------------------------- /docs/user-guide/service-commands.md: -------------------------------------------------------------------------------- 1 | # How to Use Unraid Service Commands 2 | 3 | This page provides detailed information about the service commands available in the Unraid Integration for Home Assistant and how to use them effectively. 4 | 5 | ## Understanding Service Commands 6 | 7 | The Unraid integration provides several service commands that allow you to control your Unraid server and its features from Home Assistant. These commands can be used in automations, scripts, or triggered manually from the Developer Tools. 8 | 9 | ## Finding Your Entry ID 10 | 11 | Before using any of the service commands, you'll need to know your Unraid integration's `entry_id`. This is a unique identifier for your Unraid server instance in Home Assistant. 12 | 13 | To find your entry_id: 14 | 15 | 1. Go to Configuration → Integrations 16 | 2. Find your Unraid integration 17 | 3. Click on your device entity 18 | 4. Click on UNRAID under Device Info 19 | 5. The long string in the URL is your entry_id 20 | * Example URL: `/config/integrations/integration/unraid#config_entry/1234abcd5678efgh` 21 | * Your entry_id would be: `1234abcd5678efgh` 22 | 23 | ## Available Service Commands 24 | 25 | ### Basic Command Execution 26 | 27 | #### Execute Command 28 | 29 | ```yaml 30 | service: unraid.execute_command 31 | data: 32 | entry_id: your_entry_id 33 | command: "your_command_here" 34 | ``` 35 | 36 | This service lets you run any shell command on your Unraid server. For example: 37 | 38 | ```yaml 39 | service: unraid.execute_command 40 | data: 41 | entry_id: your_entry_id 42 | command: "ls -la /mnt/user/data" 43 | ``` 44 | 45 | #### Execute Command in Background 46 | 47 | ```yaml 48 | service: unraid.execute_command 49 | data: 50 | entry_id: your_entry_id 51 | command: "your_command_here" 52 | background: true 53 | ``` 54 | 55 | This runs a command in the background, allowing it to continue running after the service call completes. Useful for long-running operations. 56 | 57 | ### User Scripts 58 | 59 | #### Execute User Script 60 | 61 | ```yaml 62 | service: unraid.execute_user_script 63 | data: 64 | entry_id: your_entry_id 65 | script_name: "script_name.sh" 66 | ``` 67 | 68 | This service runs a user script that has been created in the Unraid User Scripts plugin. The `script_name` must match exactly as it appears in the User Scripts plugin. 69 | 70 | #### Stop User Script 71 | 72 | ```yaml 73 | service: unraid.stop_user_script 74 | data: 75 | entry_id: your_entry_id 76 | script_name: "script_name.sh" 77 | ``` 78 | 79 | This service stops a running user script. 80 | 81 | ### System Commands 82 | 83 | #### System Reboot 84 | 85 | ```yaml 86 | service: unraid.system_reboot 87 | data: 88 | entry_id: your_entry_id 89 | ``` 90 | 91 | This service safely reboots your Unraid server. 92 | 93 | #### System Shutdown 94 | 95 | ```yaml 96 | service: unraid.system_shutdown 97 | data: 98 | entry_id: your_entry_id 99 | ``` 100 | 101 | This service safely shuts down your Unraid server. 102 | 103 | #### Array Stop 104 | 105 | ```yaml 106 | service: unraid.array_stop 107 | data: 108 | entry_id: your_entry_id 109 | ``` 110 | 111 | This service safely stops the Unraid array. 112 | 113 | ### Docker Container Management 114 | 115 | #### Docker Pause 116 | 117 | ```yaml 118 | service: unraid.docker_pause 119 | data: 120 | entry_id: your_entry_id 121 | container: "container_name" 122 | ``` 123 | 124 | This service pauses a running Docker container (freezes its processes without stopping it). 125 | 126 | #### Docker Resume 127 | 128 | ```yaml 129 | service: unraid.docker_resume 130 | data: 131 | entry_id: your_entry_id 132 | container: "container_name" 133 | ``` 134 | 135 | This service resumes a paused Docker container. 136 | 137 | #### Docker Restart 138 | 139 | ```yaml 140 | service: unraid.docker_restart 141 | data: 142 | entry_id: your_entry_id 143 | container: "container_name" 144 | ``` 145 | 146 | This service gracefully restarts a Docker container. 147 | 148 | #### Execute in Container 149 | 150 | ```yaml 151 | service: unraid.execute_in_container 152 | data: 153 | entry_id: your_entry_id 154 | container: "container_name" 155 | command: "command_to_run" 156 | ``` 157 | 158 | This service executes a command inside a running Docker container. 159 | 160 | ### VM Management 161 | 162 | #### VM Pause 163 | 164 | ```yaml 165 | service: unraid.vm_pause 166 | data: 167 | entry_id: your_entry_id 168 | vm_name: "vm_name" 169 | ``` 170 | 171 | This service pauses a running virtual machine. 172 | 173 | #### VM Resume 174 | 175 | ```yaml 176 | service: unraid.vm_resume 177 | data: 178 | entry_id: your_entry_id 179 | vm_name: "vm_name" 180 | ``` 181 | 182 | This service resumes a paused virtual machine. 183 | 184 | #### VM Restart 185 | 186 | ```yaml 187 | service: unraid.vm_restart 188 | data: 189 | entry_id: your_entry_id 190 | vm_name: "vm_name" 191 | ``` 192 | 193 | This service gracefully restarts a virtual machine. 194 | 195 | #### VM Hibernate 196 | 197 | ```yaml 198 | service: unraid.vm_hibernate 199 | data: 200 | entry_id: your_entry_id 201 | vm_name: "vm_name" 202 | ``` 203 | 204 | This service hibernates a virtual machine (suspends to disk). 205 | 206 | #### VM Force Stop 207 | 208 | ```yaml 209 | service: unraid.vm_force_stop 210 | data: 211 | entry_id: your_entry_id 212 | vm_name: "vm_name" 213 | ``` 214 | 215 | This service forcefully stops a virtual machine. Use with caution as it's equivalent to pulling the power plug. 216 | 217 | ## Using Services in Automations 218 | 219 | Service commands are most powerful when used in automations. Here are some examples: 220 | 221 | ### Weekly Maintenance Reboot 222 | 223 | ```yaml 224 | automation: 225 | - alias: "Weekly Unraid Reboot" 226 | trigger: 227 | - platform: time 228 | at: "04:00:00" 229 | condition: 230 | - condition: time 231 | weekday: 232 | - mon 233 | action: 234 | - service: unraid.system_reboot 235 | data: 236 | entry_id: your_entry_id 237 | ``` 238 | 239 | ### Execute Backup Script When Home Assistant Starts 240 | 241 | ```yaml 242 | automation: 243 | - alias: "Run Backup After HA Start" 244 | trigger: 245 | - platform: homeassistant 246 | event: start 247 | action: 248 | - delay: "00:05:00" # Wait 5 minutes after startup 249 | - service: unraid.execute_user_script 250 | data: 251 | entry_id: your_entry_id 252 | script_name: "backup_ha.sh" 253 | ``` 254 | 255 | ### Restart a Problematic Container 256 | 257 | ```yaml 258 | automation: 259 | - alias: "Restart Container on Error" 260 | trigger: 261 | - platform: state 262 | entity_id: binary_sensor.container_health 263 | to: "off" 264 | action: 265 | - service: unraid.docker_restart 266 | data: 267 | entry_id: your_entry_id 268 | container: "problematic_container" 269 | ``` 270 | 271 | ## Tips for Working with Services 272 | 273 | ### Error Handling 274 | 275 | Always consider what might happen if a service command fails. For critical operations, consider: 276 | 277 | - Setting up notifications to alert you of failures 278 | - Adding conditional checks before executing commands 279 | - Testing thoroughly before relying on automations 280 | 281 | ### Sequencing Commands 282 | 283 | When executing multiple commands in sequence, use the `delay` action to ensure each command has time to complete: 284 | 285 | ```yaml 286 | action: 287 | - service: unraid.execute_command 288 | data: 289 | entry_id: your_entry_id 290 | command: "first_command" 291 | - delay: "00:00:10" # Wait 10 seconds 292 | - service: unraid.execute_command 293 | data: 294 | entry_id: your_entry_id 295 | command: "second_command" 296 | ``` 297 | 298 | ### Command Security 299 | 300 | Be cautious with the commands you execute on your Unraid server. Avoid: 301 | 302 | - Commands that could compromise system security 303 | - Commands that could cause data loss 304 | - Commands with hardcoded sensitive information 305 | 306 | Instead, consider: 307 | - Using environment variables for sensitive information 308 | - Creating specific user scripts with limited capabilities 309 | - Testing commands manually before automation 310 | 311 | ## Troubleshooting 312 | 313 | If you encounter issues with service commands: 314 | 315 | 1. **Check Logs**: Look at Home Assistant logs for error messages 316 | 2. **Verify Entry ID**: Make sure your entry_id is correct 317 | 3. **Test Manually**: Try running the command directly on Unraid 318 | 4. **Check Permissions**: Ensure your user has permissions to execute the command 319 | 5. **Verify Command Syntax**: Double-check command syntax and escape special characters -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/device-support-request.yml: -------------------------------------------------------------------------------- 1 | name: 🆘 Support & Setup Help 2 | description: Get help with Unraid Integration setup, configuration, or troubleshooting 3 | title: "[Support] " 4 | labels: ["question", "help wanted"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | # 🆘 Support & Setup Help 12 | 13 | Need help with the Unraid Integration? This template is for **setup assistance, configuration questions, and general troubleshooting**. 14 | 15 | ## 📋 When to use this template: 16 | - ✅ You need help setting up the integration for the first time 17 | - ✅ You're having trouble connecting to your Unraid server 18 | - ✅ You need clarification on configuration options 19 | - ✅ You want to understand how certain features work 20 | - ✅ You've followed troubleshooting guides but still need help 21 | 22 | ## ❌ When NOT to use this template: 23 | - 🚫 **Bug reports**: Something was working but now it's broken → Use "Bug Report" template 24 | - 🚫 **Feature requests**: Want new functionality → Use "Feature Request" template 25 | 26 | ## 📚 Before You Continue 27 | 28 | Please check these resources first: 29 | - 📖 [Documentation](https://domalab.github.io/ha-unraid/) - Comprehensive setup and usage guide 30 | - 🔧 [Troubleshooting Guide](https://domalab.github.io/ha-unraid/user-guide/troubleshooting/) - Common issues and solutions 31 | - 💬 [Community Forum](https://community.home-assistant.io/t/unraid-integration) - Community discussions and help 32 | 33 | - type: dropdown 34 | id: support_category 35 | attributes: 36 | label: 🏷️ Support Category 37 | description: "What type of help do you need?" 38 | options: 39 | - "Initial setup and configuration" 40 | - "Connection issues (can't connect to Unraid)" 41 | - "Authentication problems (SSH/credentials)" 42 | - "Missing entities or sensors" 43 | - "Understanding integration features" 44 | - "Performance or reliability issues" 45 | - "Configuration options questions" 46 | - "General usage questions" 47 | - "Other" 48 | validations: 49 | required: true 50 | 51 | - type: input 52 | id: unraid_version 53 | attributes: 54 | label: 🖥️ Unraid Version 55 | description: "Your Unraid server version" 56 | placeholder: "6.12.6" 57 | validations: 58 | required: true 59 | 60 | - type: input 61 | id: ha_version 62 | attributes: 63 | label: 🏠 Home Assistant Version 64 | description: "Your Home Assistant version" 65 | placeholder: "2025.6.0" 66 | validations: 67 | required: true 68 | 69 | - type: input 70 | id: integration_version 71 | attributes: 72 | label: 📦 Integration Version 73 | description: "Version of the Unraid Integration (check in HACS or manifest.json)" 74 | placeholder: "2025.06.05" 75 | validations: 76 | required: true 77 | 78 | - type: textarea 79 | id: problem_description 80 | attributes: 81 | label: 📝 Problem Description 82 | description: "Describe the issue you're experiencing or what you need help with" 83 | placeholder: | 84 | **What are you trying to do:** 85 | I'm trying to set up the Unraid integration but getting connection errors. 86 | 87 | **What's happening:** 88 | When I try to configure the integration, I get "Failed to connect" error. 89 | 90 | **What you've tried:** 91 | - Checked IP address and credentials 92 | - Verified SSH is enabled on Unraid 93 | - Restarted Home Assistant 94 | render: markdown 95 | validations: 96 | required: true 97 | 98 | - type: textarea 99 | id: network_setup 100 | attributes: 101 | label: 🌐 Network Setup 102 | description: "Information about your network configuration" 103 | placeholder: | 104 | - Unraid server IP: 192.168.1.100 105 | - Home Assistant IP: 192.168.1.50 106 | - Same subnet: Yes/No 107 | - VLANs or firewalls: None/Details 108 | - SSH port (if not 22): 22 109 | - Can you SSH manually from HA to Unraid: Yes/No/Unknown 110 | render: markdown 111 | validations: 112 | required: true 113 | 114 | - type: textarea 115 | id: error_messages 116 | attributes: 117 | label: ⚠️ Error Messages 118 | description: "Any error messages from Home Assistant logs or during setup" 119 | placeholder: | 120 | **Setup errors:** 121 | "Failed to connect to Unraid server" 122 | 123 | **Home Assistant logs:** 124 | ``` 125 | 2025-06-11 10:30:00 ERROR (MainThread) [custom_components.unraid] ... 126 | ``` 127 | 128 | **SSH test results:** 129 | If you can test SSH manually: ssh root@192.168.1.100 130 | render: markdown 131 | validations: 132 | required: false 133 | 134 | - type: checkboxes 135 | id: troubleshooting_steps 136 | attributes: 137 | label: 🔧 Troubleshooting Steps Completed 138 | description: "Please confirm what you've already tried" 139 | options: 140 | - label: "I have read the [documentation](https://domalab.github.io/ha-unraid/)" 141 | required: true 142 | - label: "I have checked the [troubleshooting guide](https://domalab.github.io/ha-unraid/user-guide/troubleshooting/)" 143 | required: true 144 | - label: "I have verified SSH is enabled on my Unraid server" 145 | required: false 146 | - label: "I have confirmed my credentials are correct" 147 | required: false 148 | - label: "I have tried restarting Home Assistant" 149 | required: false 150 | 151 | - type: textarea 152 | id: configuration_details 153 | attributes: 154 | label: ⚙️ Configuration Details 155 | description: "Details about your current configuration attempt" 156 | placeholder: | 157 | **Integration configuration:** 158 | - Host/IP: 192.168.1.100 159 | - Username: root 160 | - SSH Port: 22 161 | - Update intervals: Default 162 | 163 | **Unraid settings:** 164 | - SSH enabled: Yes/No 165 | - SSH port: 22 (or custom) 166 | - Any security settings or restrictions 167 | render: markdown 168 | validations: 169 | required: false 170 | 171 | - type: textarea 172 | id: additional_info 173 | attributes: 174 | label: 📝 Additional Information 175 | description: "Any other relevant information about your setup" 176 | placeholder: | 177 | - Home Assistant installation type (HAOS, Docker, etc.) 178 | - Network setup details (VLANs, firewalls, etc.) 179 | - Previous attempts or workarounds tried 180 | - Specific entities or features you're having trouble with 181 | - Screenshots of error messages (if applicable) 182 | render: markdown 183 | validations: 184 | required: false 185 | 186 | - type: checkboxes 187 | id: help_preference 188 | attributes: 189 | label: 🤝 How Can We Help? 190 | description: "What type of assistance would be most helpful?" 191 | options: 192 | - label: "I need step-by-step setup guidance" 193 | - label: "I need help troubleshooting a specific error" 194 | - label: "I need clarification on how features work" 195 | - label: "I'm willing to provide additional debugging information" 196 | - label: "I'm available for real-time troubleshooting if needed" 197 | 198 | - type: markdown 199 | attributes: 200 | value: | 201 | ## 📚 Helpful Resources 202 | 203 | - **📖 Documentation**: [Unraid Integration Docs](https://domalab.github.io/ha-unraid/) 204 | - **🔧 Troubleshooting**: [Troubleshooting Guide](https://domalab.github.io/ha-unraid/user-guide/troubleshooting/) 205 | - **💬 Community**: [Home Assistant Community Forum](https://community.home-assistant.io/t/unraid-integration) 206 | - **📋 Examples**: [Usage Examples](https://domalab.github.io/ha-unraid/advanced/examples/) 207 | 208 | ## ⏱️ What Happens Next? 209 | 210 | 1. **Review**: We'll review your setup and issue details 211 | 2. **Guidance**: We'll provide specific troubleshooting steps or configuration help 212 | 3. **Follow-up**: We may ask for additional information or logs 213 | 4. **Resolution**: We'll work with you to resolve the issue 214 | 5. **Documentation**: If needed, we'll update docs to help others with similar issues 215 | 216 | **Note**: For complex setup issues, we may suggest using the [Community Forum](https://community.home-assistant.io/t/unraid-integration) where more users can help. 217 | 218 | Thank you for using the Unraid Integration! 🙏 219 | --------------------------------------------------------------------------------