diff --git a/flake.nix b/flake.nix index 44fde30..83b89cb 100644 --- a/flake.nix +++ b/flake.nix @@ -64,7 +64,7 @@ meta = with pkgs.lib; { description = "Hyprland monitor control server"; - homepage = "https://github.com/your-username/hyprmonitors"; + homepage = "https://git.darksailor.dev/servius/hyprmonitors"; license = licenses.mit; maintainers = []; }; diff --git a/homeassistant/DOCKER_TROUBLESHOOTING.md b/homeassistant/DOCKER_TROUBLESHOOTING.md new file mode 100644 index 0000000..6f9a3ea --- /dev/null +++ b/homeassistant/DOCKER_TROUBLESHOOTING.md @@ -0,0 +1,301 @@ +# Docker Troubleshooting Guide for Hyprmonitors Home Assistant Integration + +This guide helps resolve common networking issues when running Home Assistant in Docker containers and trying to connect to the Hyprmonitors daemon running on the host system. + +## Quick Fix Summary + +**TL;DR**: Replace `localhost` with `host.docker.internal` when configuring the integration in Home Assistant. + +## The Problem + +When Home Assistant runs inside a Docker container, `localhost` refers to the container's internal network, not the host system where your Hyprmonitors daemon is running. This causes connection timeouts and failures. + +## Solutions + +### Solution 1: Use Docker Host Alias (Recommended) + +**For Docker Desktop (Windows/Mac):** +``` +Host: host.docker.internal +Port: 3000 +``` + +**For Docker on Linux:** +Add to your `docker-compose.yml`: +```yaml +services: + homeassistant: + # ... other config ... + extra_hosts: + - "host.docker.internal:host-gateway" +``` + +Or add to `docker run` command: +```bash +docker run --add-host=host.docker.internal:host-gateway ... +``` + +### Solution 2: Use Host Machine's IP Address + +Find your host machine's IP address: +```bash +# Linux/Mac +hostname -I | awk '{print $1}' +# or +ip route get 8.8.8.8 | awk '{print $7}' + +# Windows +ipconfig | findstr IPv4 +``` + +Then use this IP in Home Assistant: +``` +Host: 192.168.1.100 # Your actual IP +Port: 3000 +``` + +### Solution 3: Host Network Mode + +Run Home Assistant with host networking (Linux only): +```bash +docker run --network host ... +``` + +Or in `docker-compose.yml`: +```yaml +services: + homeassistant: + network_mode: host + # Remove ports section when using host mode +``` + +## Testing Connectivity + +### Step 1: Run the Connectivity Test + +From the `hyprmonitors/homeassistant` directory: + +```bash +# Test default localhost (will likely fail in Docker) +python3 test_connectivity.py + +# Test Docker host alias +python3 test_connectivity.py host.docker.internal 3000 + +# Test specific IP +python3 test_connectivity.py 192.168.1.100 3000 +``` + +### Step 2: Test from Inside Home Assistant Container + +```bash +# Get into the HA container +docker exec -it homeassistant bash + +# Test connectivity +curl http://host.docker.internal:3000/health +# or +curl http://192.168.1.100:3000/health +``` + +Expected response: +```json +{"success":true,"message":"Hyprland Monitor Control Server is running","monitor":null} +``` + +## Common Docker Configurations + +### Docker Compose Example + +```yaml +version: '3.8' + +services: + homeassistant: + container_name: homeassistant + image: ghcr.io/home-assistant/home-assistant:stable + volumes: + - ./config:/config + - /etc/localtime:/etc/localtime:ro + environment: + - TZ=America/New_York + ports: + - "8123:8123" + restart: unless-stopped + extra_hosts: + - "host.docker.internal:host-gateway" # Linux + # For Windows/Mac, this is automatic +``` + +### Docker Run Example + +```bash +docker run -d \ + --name homeassistant \ + --privileged \ + --restart=unless-stopped \ + -e TZ=America/New_York \ + -v /home/user/homeassistant:/config \ + -v /etc/localtime:/etc/localtime:ro \ + --add-host=host.docker.internal:host-gateway \ + -p 8123:8123 \ + ghcr.io/home-assistant/home-assistant:stable +``` + +## Firewall Configuration + +Make sure port 3000 is accessible from Docker containers: + +### UFW (Ubuntu) +```bash +sudo ufw allow 3000 +``` + +### iptables +```bash +# Allow Docker containers to access host port 3000 +sudo iptables -A INPUT -i docker0 -p tcp --dport 3000 -j ACCEPT +``` + +### firewalld (RHEL/CentOS) +```bash +sudo firewall-cmd --permanent --add-port=3000/tcp +sudo firewall-cmd --reload +``` + +## Troubleshooting Steps + +### 1. Verify Hyprmonitors is Running +```bash +# Check if service is running +systemctl --user status hyprmonitors + +# Check if port is listening +ss -tlnp | grep :3000 + +# Test locally on host +curl http://localhost:3000/health +``` + +### 2. Check Docker Network +```bash +# See Docker networks +docker network ls + +# Inspect Home Assistant container network +docker inspect homeassistant | grep -A 20 NetworkSettings +``` + +### 3. Debug Container DNS +```bash +# From inside HA container +docker exec -it homeassistant bash + +# Test DNS resolution +nslookup host.docker.internal + +# Test network connectivity +ping host.docker.internal + +# Check what localhost resolves to +ping localhost # Should be 127.0.0.1 (container internal) +``` + +### 4. Check Home Assistant Logs +```bash +# View HA logs for connection errors +docker logs homeassistant | grep hyprmonitors + +# Enable debug logging in Home Assistant configuration.yaml: +# logger: +# default: info +# logs: +# custom_components.hyprmonitors: debug +``` + +## Platform-Specific Notes + +### Docker Desktop (Windows/Mac) +- `host.docker.internal` works automatically +- No extra configuration needed +- Gateway IP is handled transparently + +### Docker Engine (Linux) +- Must add `--add-host=host.docker.internal:host-gateway` +- Or use actual host IP address +- Gateway IP varies by Docker version + +### Podman +- Use `host.containers.internal` instead of `host.docker.internal` +- May need `--add-host=host.containers.internal:host-gateway` + +## Alternative Solutions + +### 1. Run Hyprmonitors in Container +Create a Dockerfile for Hyprmonitors and run it in the same Docker network as Home Assistant. + +### 2. Use Docker Bridge Network +Create a custom bridge network and connect both containers. + +### 3. Use Docker Secrets/Configs +Store connection details in Docker secrets for better security. + +## Error Messages Reference + +### "Connection timed out" +- **Cause**: Cannot reach the host from container +- **Solution**: Use `host.docker.internal` or host IP + +### "Connection refused" +- **Cause**: Service not running or wrong port +- **Solution**: Check if Hyprmonitors daemon is running + +### "Name or service not known" +- **Cause**: DNS resolution failure +- **Solution**: Use IP address instead of hostname + +### "No route to host" +- **Cause**: Network/firewall blocking connection +- **Solution**: Check firewall rules and Docker network config + +## Getting Help + +If you're still having issues: + +1. Run the connectivity test: `python3 test_connectivity.py` +2. Check Home Assistant logs for detailed error messages +3. Verify your Docker configuration matches the examples above +4. Test the connection manually using curl from inside the container + +## Example Working Configuration + +Here's a complete working example: + +**docker-compose.yml:** +```yaml +version: '3.8' +services: + homeassistant: + container_name: homeassistant + image: ghcr.io/home-assistant/home-assistant:stable + volumes: + - ./config:/config + ports: + - "8123:8123" + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped +``` + +**Home Assistant Integration Config:** +``` +Host: host.docker.internal +Port: 3000 +``` + +**Test Command:** +```bash +docker exec -it homeassistant curl http://host.docker.internal:3000/health +``` + +This configuration should work in most Docker setups and resolve the networking issues between Home Assistant and your Hyprmonitors daemon. \ No newline at end of file diff --git a/homeassistant/README.md b/homeassistant/README.md index 2e346bf..e714356 100644 --- a/homeassistant/README.md +++ b/homeassistant/README.md @@ -255,19 +255,62 @@ filter: ## Troubleshooting +### Docker Networking Issues + +**Most Common Issue**: If you're running Home Assistant in Docker and getting connection timeouts: + +1. **Use Docker host networking**: + - Replace `localhost` with `host.docker.internal` (Docker Desktop) + - Or use your host machine's IP address (e.g., `192.168.1.100`) + - Or add `--network host` to your Docker run command + +2. **Docker Compose example**: + ```yaml + version: '3' + services: + homeassistant: + container_name: homeassistant + image: ghcr.io/home-assistant/home-assistant:stable + volumes: + - ./config:/config + environment: + - TZ=America/New_York + ports: + - "8123:8123" + extra_hosts: + - "host.docker.internal:host-gateway" # For Docker on Linux + ``` + +3. **Testing connectivity from Docker**: + ```bash + # From inside the Home Assistant container: + docker exec -it homeassistant curl http://host.docker.internal:3000/health + + # Or test with your host IP: + docker exec -it homeassistant curl http://192.168.1.100:3000/health + ``` + ### Connection Issues 1. **Cannot connect to server**: + - **Docker users**: Use `host.docker.internal` instead of `localhost` - Verify the Hyprmonitors server is running: `curl http://localhost:3000/health` - Check host and port configuration - - Ensure firewall allows connections + - Ensure firewall allows connections on port 3000 -2. **Monitors not detected**: +2. **Timeout errors in logs**: + - Check if Home Assistant is in Docker (see Docker section above) + - Verify network connectivity between Home Assistant and hyprmonitors server + - Check if firewall is blocking port 3000 + - Try increasing timeout in integration settings + +3. **Monitors not detected**: - Check if Hyprland is running - Verify monitors are detected: `hyprctl monitors` - Restart the Hyprmonitors server + - Check server logs for errors -3. **Integration not loading**: +4. **Integration not loading**: - Check Home Assistant logs for errors - Verify all files are copied correctly - Restart Home Assistant completely diff --git a/homeassistant/custom_components/hyprmonitors/config_flow.py b/homeassistant/custom_components/hyprmonitors/config_flow.py index 7c82045..9b97032 100644 --- a/homeassistant/custom_components/hyprmonitors/config_flow.py +++ b/homeassistant/custom_components/hyprmonitors/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Hyprland Monitor Control integration.""" from __future__ import annotations +import asyncio import logging from typing import Any @@ -47,12 +48,17 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, if not result.get("success", False): raise CannotConnect + except asyncio.TimeoutError: + _LOGGER.error("Timeout connecting to Hyprmonitors API at %s (10s timeout exceeded)", url) + _LOGGER.error("If running Home Assistant in Docker, use host.docker.internal instead of localhost") + raise CannotConnect except aiohttp.ClientError as err: - _LOGGER.error("Error connecting to Hyprmonitors API: %s", err) + _LOGGER.error("Error connecting to Hyprmonitors API at %s: %s", url, err) + _LOGGER.error("Make sure the hyprmonitors daemon is running and accessible") raise CannotConnect from err except Exception as err: - _LOGGER.exception("Unexpected exception") - raise InvalidAuth from err + _LOGGER.exception("Unexpected exception connecting to %s", url) + raise CannotConnect from err # Try to get monitor list to verify full functionality try: @@ -64,8 +70,11 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, result = await response.json() monitors = result.get("monitors", {}) + except asyncio.TimeoutError: + _LOGGER.error("Timeout getting monitor status from %s:%s (10s timeout exceeded)", host, port) + raise CannotConnect except aiohttp.ClientError as err: - _LOGGER.error("Error getting monitor status: %s", err) + _LOGGER.error("Error getting monitor status from %s:%s: %s", host, port, err) raise CannotConnect from err # Return info that you want to store in the config entry. diff --git a/homeassistant/custom_components/hyprmonitors/const.py b/homeassistant/custom_components/hyprmonitors/const.py index fee018a..756a468 100644 --- a/homeassistant/custom_components/hyprmonitors/const.py +++ b/homeassistant/custom_components/hyprmonitors/const.py @@ -11,6 +11,19 @@ DEFAULT_HOST = "localhost" DEFAULT_PORT = 3000 DEFAULT_SCAN_INTERVAL = 30 +# Error messages for better user guidance +ERROR_CANNOT_CONNECT = "cannot_connect" +ERROR_TIMEOUT = "timeout" +ERROR_INVALID_RESPONSE = "invalid_response" +ERROR_UNKNOWN = "unknown" + +# Docker networking hints +DOCKER_HOST_ALTERNATIVES = [ + "host.docker.internal", # Docker Desktop + "172.17.0.1", # Default Docker bridge gateway + "docker.host.internal" # Some Docker variants +] + # Configuration keys CONF_HOST = "host" CONF_PORT = "port" diff --git a/homeassistant/custom_components/hyprmonitors/strings.json b/homeassistant/custom_components/hyprmonitors/strings.json index 400f6d6..78be492 100644 --- a/homeassistant/custom_components/hyprmonitors/strings.json +++ b/homeassistant/custom_components/hyprmonitors/strings.json @@ -15,9 +15,11 @@ } }, "error": { - "cannot_connect": "Failed to connect to the Hyprmonitors server. Please check the host and port.", + "cannot_connect": "Failed to connect to the Hyprmonitors server. If running Home Assistant in Docker, try using 'host.docker.internal' instead of 'localhost'. Please check the host and port are correct.", + "timeout": "Connection timed out. If running Home Assistant in Docker, use 'host.docker.internal' or your host machine's IP address instead of 'localhost'.", + "invalid_response": "Invalid response from Hyprmonitors server. Please verify the server is running correctly.", "invalid_auth": "Authentication failed. Please check your credentials.", - "unknown": "Unexpected error occurred." + "unknown": "Unexpected error occurred. Check Home Assistant logs for details." }, "abort": { "already_configured": "This Hyprmonitors server is already configured." diff --git a/homeassistant/install.sh b/homeassistant/install.sh index 0ae5f75..7c3708c 100755 --- a/homeassistant/install.sh +++ b/homeassistant/install.sh @@ -175,10 +175,12 @@ check_hyprmonitors_server() { else print_warning "Hyprmonitors server not reachable at $host:$port" print_info "Make sure the Rust server is running: cargo run" + print_info "For Docker users, run: python3 test_connectivity.py to diagnose issues" return 1 fi else print_warning "curl not available, cannot test server connection" + print_info "Use: python3 test_connectivity.py to test connectivity" return 1 fi } @@ -192,17 +194,23 @@ show_next_steps() { echo " - If using Docker: docker restart homeassistant" echo " - If using HAOS: Settings → System → Restart" echo - echo "2. Add the integration:" + echo "2. Test connectivity (especially for Docker users):" + echo " - cd $(dirname "$SOURCE_DIR")/homeassistant" + echo " - python3 test_connectivity.py" + echo " - For Docker: python3 test_connectivity.py host.docker.internal 3000" + echo + echo "3. Add the integration:" echo " - Go to Settings → Devices & Services" echo " - Click 'Add Integration'" echo " - Search for 'Hyprland Monitor Control'" echo " - Configure with your server details" + echo " - Docker users: Use 'host.docker.internal' instead of 'localhost'" echo - echo "3. Make sure your Hyprmonitors server is running:" + echo "4. Make sure your Hyprmonitors server is running:" echo " - cd $(dirname "$SOURCE_DIR")" echo " - cargo run" echo - echo "4. Optional: Check the examples.yaml file for automation ideas" + echo "5. Optional: Check the examples.yaml file for automation ideas" echo print_success "Enjoy controlling your monitors from Home Assistant! šŸ–„ļø" } @@ -215,11 +223,13 @@ usage() { echo " -h, --help Show this help message" echo " --check-server HOST:PORT Check if Hyprmonitors server is running" echo " --dry-run Show what would be done without making changes" + echo " --test-connectivity Run connectivity test with detailed diagnostics" echo echo "Examples:" echo " $0 # Auto-detect HA config directory" echo " $0 -c /config # Specify config directory" echo " $0 --check-server localhost:3000 # Test server connection" + echo " $0 --test-connectivity # Run full connectivity test" echo " $0 --dry-run # Preview installation" } @@ -227,6 +237,7 @@ main() { local ha_config="" local check_server="" local dry_run=false + local test_connectivity=false # Parse arguments while [[ $# -gt 0 ]]; do @@ -243,6 +254,10 @@ main() { check_server="$2" shift 2 ;; + --test-connectivity) + test_connectivity=true + shift + ;; --dry-run) dry_run=true shift @@ -264,6 +279,24 @@ main() { exit $? fi + # Handle connectivity test + if [[ "$test_connectivity" == true ]]; then + local test_script="$SOURCE_DIR/test_connectivity.py" + if [[ -f "$test_script" ]]; then + print_info "Running connectivity test..." + if command -v python3 &> /dev/null; then + python3 "$test_script" + exit $? + else + print_error "Python3 not found, cannot run connectivity test" + exit 1 + fi + else + print_error "Connectivity test script not found: $test_script" + exit 1 + fi + fi + # Detect Home Assistant config directory if [[ -z "$ha_config" ]]; then ha_config=$(detect_ha_config) diff --git a/homeassistant/test_connectivity.py b/homeassistant/test_connectivity.py new file mode 100644 index 0000000..bec8770 --- /dev/null +++ b/homeassistant/test_connectivity.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +""" +Hyprmonitors Home Assistant Connectivity Test + +This script helps diagnose connectivity issues between Home Assistant and +the Hyprmonitors server, particularly for Docker setups. + +Usage: + python test_connectivity.py [host] [port] + +Examples: + python test_connectivity.py + python test_connectivity.py localhost 3000 + python test_connectivity.py host.docker.internal 3000 + python test_connectivity.py 192.168.1.100 3000 +""" + +import asyncio +import aiohttp +import sys +import time +from typing import Optional, Tuple + + +async def test_connection(host: str = "localhost", port: int = 3000) -> bool: + """Test connection to Hyprmonitors server.""" + url = f"http://{host}:{port}/health" + print(f"Testing connection to {url}...") + + try: + timeout = aiohttp.ClientTimeout(total=10) + async with aiohttp.ClientSession(timeout=timeout) as session: + start_time = time.time() + async with session.get(url) as response: + end_time = time.time() + response_time = (end_time - start_time) * 1000 # Convert to ms + + print(f"āœ“ Connection successful! Response time: {response_time:.2f}ms") + print(f" Status: {response.status}") + print(f" Content-Type: {response.headers.get('content-type', 'unknown')}") + + if response.status == 200: + try: + data = await response.json() + print(f" Response: {data}") + + if data.get("success"): + print("āœ“ Health check passed!") + return True + else: + print("āœ— Health check failed - server returned success=false") + return False + except Exception as e: + print(f"āœ— Failed to parse JSON response: {e}") + text = await response.text() + print(f" Raw response: {text[:200]}...") + return False + else: + print(f"āœ— Server returned status {response.status}") + return False + + except asyncio.TimeoutError: + print("āœ— Connection timed out (10 seconds)") + print(" This usually means:") + print(" - The server is not running") + print(" - Wrong host/port configuration") + print(" - Network/firewall blocking the connection") + return False + + except aiohttp.ClientConnectorError as e: + print(f"āœ— Connection failed: {e}") + print(" This usually means:") + print(" - The server is not running") + print(" - Wrong host/port configuration") + print(" - If using Docker: try 'host.docker.internal' instead of 'localhost'") + return False + + except Exception as e: + print(f"āœ— Unexpected error: {e}") + return False + + +async def test_monitor_status(host: str = "localhost", port: int = 3000) -> bool: + """Test monitor status endpoint.""" + url = f"http://{host}:{port}/monitors/status" + print(f"\nTesting monitor status endpoint: {url}...") + + try: + timeout = aiohttp.ClientTimeout(total=10) + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(url) as response: + if response.status == 200: + try: + data = await response.json() + print(f"āœ“ Monitor status retrieved successfully!") + print(f" Success: {data.get('success')}") + monitors = data.get('monitors', {}) + print(f" Found {len(monitors)} monitors:") + for monitor, status in monitors.items(): + print(f" - {monitor}: {status}") + return True + except Exception as e: + print(f"āœ— Failed to parse monitor status response: {e}") + return False + else: + print(f"āœ— Monitor status request failed with status {response.status}") + return False + + except Exception as e: + print(f"āœ— Monitor status test failed: {e}") + return False + + +def test_docker_alternatives(port: int = 3000) -> list: + """Test common Docker host alternatives.""" + alternatives = [ + "host.docker.internal", + "172.17.0.1", + "docker.host.internal", + "gateway.docker.internal" + ] + + print("\n" + "="*60) + print("TESTING DOCKER HOST ALTERNATIVES") + print("="*60) + + working_hosts = [] + + for host in alternatives: + print(f"\nTesting {host}:{port}...") + try: + result = asyncio.run(test_connection(host, port)) + if result: + working_hosts.append(host) + print(f"āœ“ {host} works!") + else: + print(f"āœ— {host} failed") + except Exception as e: + print(f"āœ— {host} error: {e}") + + return working_hosts + + +def print_summary(host: str, port: int, health_ok: bool, status_ok: bool): + """Print test summary and recommendations.""" + print("\n" + "="*60) + print("TEST SUMMARY") + print("="*60) + + print(f"Host: {host}") + print(f"Port: {port}") + print(f"Health check: {'āœ“ PASS' if health_ok else 'āœ— FAIL'}") + print(f"Monitor status: {'āœ“ PASS' if status_ok else 'āœ— FAIL'}") + + if health_ok and status_ok: + print("\nšŸŽ‰ All tests passed! You can use these settings in Home Assistant:") + print(f" Host: {host}") + print(f" Port: {port}") + else: + print("\nāŒ Tests failed. Try these troubleshooting steps:") + print("\n1. Make sure the Hyprmonitors server is running:") + print(" systemctl --user status hyprmonitors") + print(" # or if using cargo:") + print(" cargo run --release") + + print("\n2. Test locally on the host machine:") + print(f" curl http://localhost:{port}/health") + + print("\n3. If Home Assistant is in Docker, try:") + print(" - Use 'host.docker.internal' instead of 'localhost'") + print(" - Use your host machine's IP address") + print(" - Add '--network host' to Docker run command") + + print("\n4. Check firewall settings:") + print(f" sudo ufw allow {port}") + print(f" # or check iptables rules") + + if host == "localhost": + print("\n5. For Docker users, run this script with Docker alternatives:") + print(" python test_connectivity.py host.docker.internal 3000") + + +async def main(): + """Main function.""" + # Parse command line arguments + host = "localhost" + port = 3000 + + if len(sys.argv) > 1: + host = sys.argv[1] + if len(sys.argv) > 2: + try: + port = int(sys.argv[2]) + except ValueError: + print(f"Invalid port: {sys.argv[2]}") + sys.exit(1) + + print("Hyprmonitors Home Assistant Connectivity Test") + print("="*50) + + # Test basic connectivity and health + health_ok = await test_connection(host, port) + + # Test monitor status endpoint + status_ok = False + if health_ok: + status_ok = await test_monitor_status(host, port) + + # If localhost failed and we might be in Docker, test alternatives + if not health_ok and host == "localhost": + print(f"\nLocalhost failed. Testing Docker alternatives...") + working_hosts = test_docker_alternatives(port) + + if working_hosts: + print(f"\nāœ“ Working Docker hosts found: {working_hosts}") + print(f"Recommendation: Use '{working_hosts[0]}' as your host in Home Assistant") + + # Test the first working host + print(f"\nTesting monitor status with {working_hosts[0]}...") + status_ok = await test_monitor_status(working_hosts[0], port) + host = working_hosts[0] # Update for summary + health_ok = True # We know it works + + # Print summary and recommendations + print_summary(host, port, health_ok, status_ok) + + # Exit with appropriate code + sys.exit(0 if (health_ok and status_ok) else 1) + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\n\nTest cancelled by user") + sys.exit(130) + except Exception as e: + print(f"\nUnexpected error: {e}") + sys.exit(1)