feat: Added homeassistant plugin
Some checks failed
build / checks-matrix (push) Failing after 19m7s
build / checks-build (push) Has been skipped
build / codecov (push) Failing after 19m7s
docs / docs (push) Failing after 28m31s

This commit is contained in:
uttarayan21
2025-08-15 18:19:01 +05:30
parent f0dce3f233
commit fdd7065e78
12 changed files with 1972 additions and 0 deletions

315
homeassistant/README.md Normal file
View File

@@ -0,0 +1,315 @@
# Hyprland Monitor Control Integration for Home Assistant
This custom integration allows you to control your Hyprland desktop monitors directly from Home Assistant. It connects to your Hyprmonitors Rust server to provide full monitor control capabilities.
## Features
- **Monitor Status Sensors**: Real-time status of all monitors (on/off/mixed)
- **Individual Monitor Switches**: Control each monitor separately
- **Master Switch**: Turn all monitors on/off at once
- **Custom Services**: Advanced automation capabilities
- **Auto-discovery**: Automatically detects available monitors
- **Real-time Updates**: Monitor status updates every 30 seconds
## Prerequisites
1. **Hyprmonitors Server**: The Rust server must be running and accessible
2. **Home Assistant**: Version 2023.1.0 or newer
3. **Network Access**: Home Assistant must be able to reach your Hyprmonitors server
## Installation
### Method 1: Manual Installation
1. Create the custom components directory in your Home Assistant configuration:
```bash
mkdir -p config/custom_components/hyprmonitors
```
2. Copy all files from this directory to your Home Assistant custom components:
```bash
cp -r custom_components/hyprmonitors/* config/custom_components/hyprmonitors/
```
3. Restart Home Assistant
### Method 2: HACS (Recommended when available)
1. Add this repository to HACS as a custom repository
2. Search for "Hyprland Monitor Control" in HACS
3. Install the integration
4. Restart Home Assistant
## Configuration
### Via UI (Recommended)
1. Go to **Settings** → **Devices & Services**
2. Click **Add Integration**
3. Search for "Hyprland Monitor Control"
4. Enter your Hyprmonitors server details:
- **Host**: IP address or hostname (default: localhost)
- **Port**: Port number (default: 3000)
5. Click **Submit**
The integration will automatically discover your monitors and create entities.
### Via configuration.yaml (Legacy)
```yaml
hyprmonitors:
host: localhost
port: 3000
```
## Entities Created
### Sensors
- **`sensor.hyprmonitors_monitor_status`**: Overall status of all monitors
- States: `all_on`, `all_off`, `mixed`, `unavailable`
- Attributes: total monitors, monitors on/off count, monitor details
- **`sensor.hyprmonitors_[monitor_name]_status`**: Individual monitor status
- States: `on`, `off`, `unknown`, `unavailable`
- Attributes: monitor name, friendly name
### Switches
- **`switch.hyprmonitors_all_monitors`**: Master switch for all monitors
- Turn all monitors on/off simultaneously
- Shows mixed state when some monitors are on/off
- **`switch.hyprmonitors_[monitor_name]`**: Individual monitor switches
- Control specific monitors by name
- Examples: `switch.hyprmonitors_dp_1`, `switch.hyprmonitors_hdmi_a_1`
## Services
The integration provides several services for advanced automation:
### `hyprmonitors.turn_on_monitor`
Turn on a specific monitor by name.
```yaml
service: hyprmonitors.turn_on_monitor
data:
monitor: "DP-1"
```
### `hyprmonitors.turn_off_monitor`
Turn off a specific monitor by name.
```yaml
service: hyprmonitors.turn_off_monitor
data:
monitor: "HDMI-A-1"
```
### `hyprmonitors.turn_on_all_monitors`
Turn on all monitors.
```yaml
service: hyprmonitors.turn_on_all_monitors
```
### `hyprmonitors.turn_off_all_monitors`
Turn off all monitors.
```yaml
service: hyprmonitors.turn_off_all_monitors
```
## Automation Examples
### Turn off monitors when away
```yaml
automation:
- alias: "Turn off monitors when away"
trigger:
- platform: state
entity_id: person.your_name
to: "not_home"
action:
- service: hyprmonitors.turn_off_all_monitors
- alias: "Turn on monitors when home"
trigger:
- platform: state
entity_id: person.your_name
to: "home"
action:
- service: hyprmonitors.turn_on_all_monitors
```
### Scheduled monitor control
```yaml
automation:
- alias: "Turn off monitors at night"
trigger:
- platform: time
at: "23:00:00"
action:
- service: switch.turn_off
entity_id: switch.hyprmonitors_all_monitors
- alias: "Turn on monitors in the morning"
trigger:
- platform: time
at: "07:00:00"
condition:
- condition: state
entity_id: person.your_name
state: "home"
action:
- service: switch.turn_on
entity_id: switch.hyprmonitors_all_monitors
```
### Smart meeting room control
```yaml
automation:
- alias: "Turn off secondary monitor during meetings"
trigger:
- platform: calendar
event: start
entity_id: calendar.work
condition:
- condition: template
value_template: "{{ 'meeting' in trigger.calendar_event.summary.lower() }}"
action:
- service: hyprmonitors.turn_off_monitor
data:
monitor: "DP-2" # Secondary monitor
- alias: "Turn on secondary monitor after meetings"
trigger:
- platform: calendar
event: end
entity_id: calendar.work
condition:
- condition: template
value_template: "{{ 'meeting' in trigger.calendar_event.summary.lower() }}"
action:
- service: hyprmonitors.turn_on_monitor
data:
monitor: "DP-2"
```
## Dashboard Cards
### Monitor Status Card
```yaml
type: entities
entities:
- entity: sensor.hyprmonitors_monitor_status
name: Monitor Status
- entity: switch.hyprmonitors_all_monitors
name: All Monitors
title: Monitor Control
```
### Individual Monitor Controls
```yaml
type: glance
entities:
- entity: switch.hyprmonitors_dp_1
name: Main Monitor
icon: mdi:monitor
- entity: switch.hyprmonitors_hdmi_a_1
name: Secondary
icon: mdi:monitor-speaker
- entity: switch.hyprmonitors_all_monitors
name: All Monitors
icon: mdi:monitor-multiple
title: Monitor Control
```
### Monitor Status with Attributes
```yaml
type: custom:auto-entities
card:
type: entities
title: Monitor Details
filter:
include:
- entity_id: "sensor.*monitor*status"
options:
type: custom:multiple-entity-row
name: "{{ state_attr(config.entity, 'friendly_name') }}"
show_state: true
entities:
- attribute: monitors_on
name: "On"
- attribute: monitors_off
name: "Off"
- attribute: total_monitors
name: "Total"
```
## Troubleshooting
### Connection Issues
1. **Cannot connect to server**:
- Verify the Hyprmonitors server is running: `curl http://localhost:3000/health`
- Check host and port configuration
- Ensure firewall allows connections
2. **Monitors not detected**:
- Check if Hyprland is running
- Verify monitors are detected: `hyprctl monitors`
- Restart the Hyprmonitors server
3. **Integration not loading**:
- Check Home Assistant logs for errors
- Verify all files are copied correctly
- Restart Home Assistant completely
### Debug Logging
Add to your `configuration.yaml` to enable debug logging:
```yaml
logger:
default: info
logs:
custom_components.hyprmonitors: debug
```
### Monitor Names
To find your exact monitor names, check:
- Hyprmonitors server logs on startup
- Run `hyprctl monitors` in terminal
- Check the sensor attributes in Home Assistant
Common patterns:
- **DisplayPort**: `DP-1`, `DP-2`, etc.
- **HDMI**: `HDMI-A-1`, `HDMI-A-2`, etc.
- **Laptop screen**: `eDP-1`
## Configuration Options
### Advanced Configuration
The integration supports these advanced options (configure via UI):
- **Scan Interval**: How often to check monitor status (default: 30 seconds)
- **Timeout**: API request timeout (default: 10 seconds)
## Support
- **Issues**: Report bugs and feature requests on GitHub
- **Documentation**: Check the main Hyprmonitors README
- **Community**: Join the Home Assistant Community forums
## License
This integration is part of the Hyprmonitors project and follows the same license terms.

View File

@@ -0,0 +1,206 @@
"""Hyprland Monitor Control integration for Home Assistant."""
from __future__ import annotations
import asyncio
import logging
from typing import Any
import aiohttp
import async_timeout
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN, PLATFORMS, SERVICE_TURN_ON_MONITOR, SERVICE_TURN_OFF_MONITOR, SERVICE_TURN_ON_ALL, SERVICE_TURN_OFF_ALL
_LOGGER = logging.getLogger(__name__)
type HyprmonitorsConfigEntry = ConfigEntry[HyprmonitorsData]
class HyprmonitorsData:
"""Data for Hyprmonitors integration."""
def __init__(self, session: aiohttp.ClientSession, host: str, port: int) -> None:
"""Initialize the data object."""
self.session = session
self.host = host
self.port = port
self.base_url = f"http://{host}:{port}"
async def async_get_monitors(self) -> dict[str, Any]:
"""Get monitor status from the API."""
url = f"{self.base_url}/monitors/status"
try:
async with async_timeout.timeout(10):
async with self.session.get(url) as response:
response.raise_for_status()
data = await response.json()
return data.get("monitors", {})
except asyncio.TimeoutError as err:
_LOGGER.error("Timeout connecting to Hyprmonitors API at %s", url)
raise ConfigEntryNotReady("Timeout connecting to API") from err
except aiohttp.ClientError as err:
_LOGGER.error("Error connecting to Hyprmonitors API at %s: %s", url, err)
raise ConfigEntryNotReady("Error connecting to API") from err
async def async_turn_monitor_on(self, monitor: str) -> bool:
"""Turn on a monitor."""
url = f"{self.base_url}/monitors/{monitor}/on"
try:
async with async_timeout.timeout(10):
async with self.session.post(url) as response:
response.raise_for_status()
data = await response.json()
return data.get("success", False)
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error turning on monitor %s: %s", monitor, err)
return False
async def async_turn_monitor_off(self, monitor: str) -> bool:
"""Turn off a monitor."""
url = f"{self.base_url}/monitors/{monitor}/off"
try:
async with async_timeout.timeout(10):
async with self.session.post(url) as response:
response.raise_for_status()
data = await response.json()
return data.get("success", False)
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error turning off monitor %s: %s", monitor, err)
return False
async def async_turn_all_monitors_on(self) -> bool:
"""Turn on all monitors."""
url = f"{self.base_url}/monitors/on"
try:
async with async_timeout.timeout(10):
async with self.session.post(url) as response:
response.raise_for_status()
data = await response.json()
return data.get("success", False)
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error turning on all monitors: %s", err)
return False
async def async_turn_all_monitors_off(self) -> bool:
"""Turn off all monitors."""
url = f"{self.base_url}/monitors/off"
try:
async with async_timeout.timeout(10):
async with self.session.post(url) as response:
response.raise_for_status()
data = await response.json()
return data.get("success", False)
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error turning off all monitors: %s", err)
return False
async def async_test_connection(self) -> bool:
"""Test connection to the API."""
url = f"{self.base_url}/health"
try:
async with async_timeout.timeout(5):
async with self.session.get(url) as response:
response.raise_for_status()
data = await response.json()
return data.get("success", False)
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.debug("Connection test failed: %s", err)
return False
async def async_setup_entry(hass: HomeAssistant, entry: HyprmonitorsConfigEntry) -> bool:
"""Set up Hyprmonitors from a config entry."""
host = entry.data["host"]
port = entry.data["port"]
session = async_get_clientsession(hass)
data = HyprmonitorsData(session, host, port)
# Test connection
if not await data.async_test_connection():
raise ConfigEntryNotReady("Unable to connect to Hyprmonitors API")
# Store the data object in the config entry
entry.runtime_data = data
# Forward the setup to the sensor and switch platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Register services
await async_setup_services(hass, data)
return True
async def async_unload_entry(hass: HomeAssistant, entry: HyprmonitorsConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_reload_entry(hass: HomeAssistant, entry: HyprmonitorsConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)
async def async_setup_services(hass: HomeAssistant, api: HyprmonitorsData) -> None:
"""Set up services for Hyprmonitors."""
async def turn_on_monitor_service(call) -> None:
"""Service to turn on a specific monitor."""
monitor = call.data.get("monitor")
success = await api.async_turn_monitor_on(monitor)
if not success:
_LOGGER.error("Failed to turn on monitor %s", monitor)
async def turn_off_monitor_service(call) -> None:
"""Service to turn off a specific monitor."""
monitor = call.data.get("monitor")
success = await api.async_turn_monitor_off(monitor)
if not success:
_LOGGER.error("Failed to turn off monitor %s", monitor)
async def turn_on_all_monitors_service(call) -> None:
"""Service to turn on all monitors."""
success = await api.async_turn_all_monitors_on()
if not success:
_LOGGER.error("Failed to turn on all monitors")
async def turn_off_all_monitors_service(call) -> None:
"""Service to turn off all monitors."""
success = await api.async_turn_all_monitors_off()
if not success:
_LOGGER.error("Failed to turn off all monitors")
# Register services
hass.services.async_register(
DOMAIN,
SERVICE_TURN_ON_MONITOR,
turn_on_monitor_service,
schema=vol.Schema({vol.Required("monitor"): cv.string}),
)
hass.services.async_register(
DOMAIN,
SERVICE_TURN_OFF_MONITOR,
turn_off_monitor_service,
schema=vol.Schema({vol.Required("monitor"): cv.string}),
)
hass.services.async_register(
DOMAIN,
SERVICE_TURN_ON_ALL,
turn_on_all_monitors_service,
)
hass.services.async_register(
DOMAIN,
SERVICE_TURN_OFF_ALL,
turn_off_all_monitors_service,
)

View File

@@ -0,0 +1,134 @@
"""Config flow for Hyprland Monitor Control integration."""
from __future__ import annotations
import logging
from typing import Any
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_HOST, DEFAULT_PORT, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
host = data[CONF_HOST]
port = data[CONF_PORT]
session = async_get_clientsession(hass)
url = f"http://{host}:{port}/health"
try:
async with async_timeout.timeout(10):
async with session.get(url) as response:
if response.status != 200:
raise CannotConnect
result = await response.json()
if not result.get("success", False):
raise CannotConnect
except aiohttp.ClientError as err:
_LOGGER.error("Error connecting to Hyprmonitors API: %s", err)
raise CannotConnect from err
except Exception as err:
_LOGGER.exception("Unexpected exception")
raise InvalidAuth from err
# Try to get monitor list to verify full functionality
try:
async with async_timeout.timeout(10):
async with session.get(f"http://{host}:{port}/monitors/status") as response:
if response.status != 200:
raise CannotConnect
result = await response.json()
monitors = result.get("monitors", {})
except aiohttp.ClientError as err:
_LOGGER.error("Error getting monitor status: %s", err)
raise CannotConnect from err
# Return info that you want to store in the config entry.
return {
"title": f"Hyprmonitors ({host}:{port})",
"host": host,
"port": port,
"monitor_count": len(monitors),
}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Hyprland Monitor Control."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
# Create a unique ID based on host and port
unique_id = f"{user_input[CONF_HOST]}_{user_input[CONF_PORT]}"
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=info["title"],
data={
CONF_HOST: info["host"],
CONF_PORT: info["port"],
},
)
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={
"default_host": DEFAULT_HOST,
"default_port": str(DEFAULT_PORT),
},
)
async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult:
"""Handle import from configuration.yaml."""
return await self.async_step_user(import_info)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@@ -0,0 +1,32 @@
"""Constants for the Hyprland Monitor Control integration."""
from homeassistant.const import Platform
DOMAIN = "hyprmonitors"
# Platforms
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
# Default values
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 3000
DEFAULT_SCAN_INTERVAL = 30
# Configuration keys
CONF_HOST = "host"
CONF_PORT = "port"
# Attributes
ATTR_MONITOR_NAME = "monitor_name"
ATTR_RESOLUTION = "resolution"
ATTR_REFRESH_RATE = "refresh_rate"
ATTR_SCALE = "scale"
ATTR_POSITION = "position"
# Service names
SERVICE_TURN_ON_MONITOR = "turn_on_monitor"
SERVICE_TURN_OFF_MONITOR = "turn_off_monitor"
SERVICE_TURN_ON_ALL = "turn_on_all_monitors"
SERVICE_TURN_OFF_ALL = "turn_off_all_monitors"
# Entity names
ENTITY_ID_ALL_MONITORS = "switch.hyprmonitors_all_monitors"

View File

@@ -0,0 +1,17 @@
{
"domain": "hyprmonitors",
"name": "Hyprland Monitor Control",
"codeowners": ["@hyprmonitors"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/your-username/hyprmonitors",
"homeassistant": "2023.1.0",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/your-username/hyprmonitors/issues",
"requirements": [
"aiohttp>=3.8.0"
],
"ssdp": [],
"version": "1.0.0",
"zeroconf": []
}

View File

@@ -0,0 +1,207 @@
"""Sensor platform for Hyprland Monitor Control integration."""
from __future__ import annotations
import logging
from datetime import timedelta
from typing import Any
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from . import HyprmonitorsConfigEntry, HyprmonitorsData
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
class HyprmonitorsDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""
def __init__(
self,
hass: HomeAssistant,
*,
api: HyprmonitorsData,
) -> None:
"""Initialize."""
self.api = api
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
try:
return await self.api.async_get_monitors()
except Exception as exception:
raise UpdateFailed(f"Error communicating with API: {exception}") from exception
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HyprmonitorsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
api = config_entry.runtime_data
# Create coordinator
coordinator = HyprmonitorsDataUpdateCoordinator(hass, api=api)
# Fetch initial data so we have data when entities are added
await coordinator.async_config_entry_first_refresh()
# Create sensors for each monitor
entities = []
# Add overall status sensor
entities.append(
HyprmonitorsOverallSensor(
coordinator=coordinator,
config_entry=config_entry,
)
)
# Add individual monitor sensors
if coordinator.data:
for monitor_name in coordinator.data:
entities.append(
HyprmonitorsMonitorSensor(
coordinator=coordinator,
config_entry=config_entry,
monitor_name=monitor_name,
)
)
async_add_entities(entities)
class HyprmonitorsBaseSensor(CoordinatorEntity, SensorEntity):
"""Base class for Hyprmonitors sensors."""
def __init__(
self,
coordinator: HyprmonitorsDataUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._config_entry = config_entry
self._attr_has_entity_name = True
@property
def device_info(self) -> DeviceInfo:
"""Return device information."""
return DeviceInfo(
identifiers={(DOMAIN, self._config_entry.entry_id)},
name="Hyprland Monitors",
manufacturer="Hyprland",
model="Monitor Control",
sw_version="1.0.0",
configuration_url=f"http://{self._config_entry.data['host']}:{self._config_entry.data['port']}",
)
class HyprmonitorsOverallSensor(HyprmonitorsBaseSensor):
"""Sensor for overall monitor status."""
def __init__(
self,
coordinator: HyprmonitorsDataUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, config_entry)
self._attr_name = "Monitor Status"
self._attr_unique_id = f"{config_entry.entry_id}_overall_status"
self._attr_icon = "mdi:monitor-multiple"
@property
def native_value(self) -> str | None:
"""Return the state of the sensor."""
if not self.coordinator.data:
return "unavailable"
monitors = self.coordinator.data
total_monitors = len(monitors)
on_monitors = sum(1 for status in monitors.values() if status == "on")
if on_monitors == 0:
return "all_off"
elif on_monitors == total_monitors:
return "all_on"
else:
return "mixed"
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
if not self.coordinator.data:
return {}
monitors = self.coordinator.data
total_monitors = len(monitors)
on_monitors = sum(1 for status in monitors.values() if status == "on")
off_monitors = total_monitors - on_monitors
return {
"total_monitors": total_monitors,
"monitors_on": on_monitors,
"monitors_off": off_monitors,
"monitor_details": monitors,
}
class HyprmonitorsMonitorSensor(HyprmonitorsBaseSensor):
"""Sensor for individual monitor status."""
def __init__(
self,
coordinator: HyprmonitorsDataUpdateCoordinator,
config_entry: ConfigEntry,
monitor_name: str,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, config_entry)
self._monitor_name = monitor_name
self._attr_name = f"{monitor_name} Status"
self._attr_unique_id = f"{config_entry.entry_id}_{monitor_name}_status"
self._attr_icon = "mdi:monitor"
@property
def native_value(self) -> str | None:
"""Return the state of the sensor."""
if not self.coordinator.data:
return "unavailable"
return self.coordinator.data.get(self._monitor_name, "unknown")
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
return {
"monitor_name": self._monitor_name,
"friendly_name": f"Monitor {self._monitor_name}",
}
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and self.coordinator.data is not None
and self._monitor_name in self.coordinator.data
)

View File

@@ -0,0 +1,29 @@
turn_on_monitor:
name: Turn on monitor
description: Turn on a specific monitor by name
fields:
monitor:
name: Monitor name
description: Name of the monitor to turn on (e.g., DP-1, HDMI-A-1)
required: true
selector:
text:
turn_off_monitor:
name: Turn off monitor
description: Turn off a specific monitor by name
fields:
monitor:
name: Monitor name
description: Name of the monitor to turn off (e.g., DP-1, HDMI-A-1)
required: true
selector:
text:
turn_on_all_monitors:
name: Turn on all monitors
description: Turn on all monitors connected to Hyprland
turn_off_all_monitors:
name: Turn off all monitors
description: Turn off all monitors connected to Hyprland

View File

@@ -0,0 +1,88 @@
{
"config": {
"step": {
"user": {
"title": "Hyprland Monitor Control",
"description": "Configure connection to your Hyprmonitors server",
"data": {
"host": "Host",
"port": "Port"
},
"data_description": {
"host": "IP address or hostname of the Hyprmonitors server (default: {default_host})",
"port": "Port number of the Hyprmonitors server (default: {default_port})"
}
}
},
"error": {
"cannot_connect": "Failed to connect to the Hyprmonitors server. Please check the host and port.",
"invalid_auth": "Authentication failed. Please check your credentials.",
"unknown": "Unexpected error occurred."
},
"abort": {
"already_configured": "This Hyprmonitors server is already configured."
}
},
"options": {
"step": {
"init": {
"title": "Hyprland Monitor Control Options",
"description": "Configure advanced options for the integration",
"data": {
"scan_interval": "Scan Interval (seconds)"
}
}
}
},
"entity": {
"sensor": {
"monitor_status": {
"name": "Monitor Status",
"state": {
"all_on": "All On",
"all_off": "All Off",
"mixed": "Mixed",
"unavailable": "Unavailable"
}
}
},
"switch": {
"all_monitors": {
"name": "All Monitors"
},
"monitor": {
"name": "Monitor {monitor_name}"
}
}
},
"services": {
"turn_on_monitor": {
"name": "Turn on monitor",
"description": "Turn on a specific monitor by name",
"fields": {
"monitor": {
"name": "Monitor name",
"description": "Name of the monitor to turn on (e.g., DP-1, HDMI-A-1)"
}
}
},
"turn_off_monitor": {
"name": "Turn off monitor",
"description": "Turn off a specific monitor by name",
"fields": {
"monitor": {
"name": "Monitor name",
"description": "Name of the monitor to turn off (e.g., DP-1, HDMI-A-1)"
}
}
},
"turn_on_all_monitors": {
"name": "Turn on all monitors",
"description": "Turn on all monitors connected to Hyprland"
},
"turn_off_all_monitors": {
"name": "Turn off all monitors",
"description": "Turn off all monitors connected to Hyprland"
}
}
}

View File

@@ -0,0 +1,218 @@
"""Switch platform for Hyprland Monitor Control integration."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HyprmonitorsConfigEntry, HyprmonitorsData
from .const import DOMAIN
from .sensor import HyprmonitorsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HyprmonitorsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the switch platform."""
api = config_entry.runtime_data
# Get coordinator from sensor platform
coordinator = None
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.entry_id == config_entry.entry_id:
# Create coordinator if it doesn't exist
coordinator = HyprmonitorsDataUpdateCoordinator(hass, api=api)
await coordinator.async_config_entry_first_refresh()
break
if coordinator is None:
_LOGGER.error("Could not find coordinator for config entry")
return
entities = []
# Add master switch for all monitors
entities.append(
HyprmonitorsAllMonitorsSwitch(
coordinator=coordinator,
config_entry=config_entry,
api=api,
)
)
# Add individual monitor switches
if coordinator.data:
for monitor_name in coordinator.data:
entities.append(
HyprmonitorsMonitorSwitch(
coordinator=coordinator,
config_entry=config_entry,
api=api,
monitor_name=monitor_name,
)
)
async_add_entities(entities)
class HyprmonitorsBaseSwitch(CoordinatorEntity, SwitchEntity):
"""Base class for Hyprmonitors switches."""
def __init__(
self,
coordinator: HyprmonitorsDataUpdateCoordinator,
config_entry: ConfigEntry,
api: HyprmonitorsData,
) -> None:
"""Initialize the switch."""
super().__init__(coordinator)
self._api = api
self._config_entry = config_entry
self._attr_has_entity_name = True
@property
def device_info(self) -> DeviceInfo:
"""Return device information."""
return DeviceInfo(
identifiers={(DOMAIN, self._config_entry.entry_id)},
name="Hyprland Monitors",
manufacturer="Hyprland",
model="Monitor Control",
sw_version="1.0.0",
configuration_url=f"http://{self._config_entry.data['host']}:{self._config_entry.data['port']}",
)
class HyprmonitorsAllMonitorsSwitch(HyprmonitorsBaseSwitch):
"""Switch to control all monitors at once."""
def __init__(
self,
coordinator: HyprmonitorsDataUpdateCoordinator,
config_entry: ConfigEntry,
api: HyprmonitorsData,
) -> None:
"""Initialize the switch."""
super().__init__(coordinator, config_entry, api)
self._attr_name = "All Monitors"
self._attr_unique_id = f"{config_entry.entry_id}_all_monitors"
self._attr_icon = "mdi:monitor-multiple"
@property
def is_on(self) -> bool | None:
"""Return true if all monitors are on."""
if not self.coordinator.data:
return None
monitors = self.coordinator.data
if not monitors:
return None
# Return True only if ALL monitors are on
return all(status == "on" for status in monitors.values())
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
if not self.coordinator.data:
return {}
monitors = self.coordinator.data
total_monitors = len(monitors)
on_monitors = sum(1 for status in monitors.values() if status == "on")
return {
"total_monitors": total_monitors,
"monitors_on": on_monitors,
"monitors_off": total_monitors - on_monitors,
"monitor_names": list(monitors.keys()),
}
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn all monitors on."""
success = await self._api.async_turn_all_monitors_on()
if success:
await self.coordinator.async_request_refresh()
else:
_LOGGER.error("Failed to turn on all monitors")
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn all monitors off."""
success = await self._api.async_turn_all_monitors_off()
if success:
await self.coordinator.async_request_refresh()
else:
_LOGGER.error("Failed to turn off all monitors")
class HyprmonitorsMonitorSwitch(HyprmonitorsBaseSwitch):
"""Switch to control individual monitors."""
def __init__(
self,
coordinator: HyprmonitorsDataUpdateCoordinator,
config_entry: ConfigEntry,
api: HyprmonitorsData,
monitor_name: str,
) -> None:
"""Initialize the switch."""
super().__init__(coordinator, config_entry, api)
self._monitor_name = monitor_name
self._attr_name = f"Monitor {monitor_name}"
self._attr_unique_id = f"{config_entry.entry_id}_{monitor_name}"
self._attr_icon = "mdi:monitor"
@property
def is_on(self) -> bool | None:
"""Return true if the monitor is on."""
if not self.coordinator.data:
return None
status = self.coordinator.data.get(self._monitor_name)
if status is None:
return None
return status == "on"
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
return {
"monitor_name": self._monitor_name,
"friendly_name": f"Monitor {self._monitor_name}",
}
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and self.coordinator.data is not None
and self._monitor_name in self.coordinator.data
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the monitor on."""
success = await self._api.async_turn_monitor_on(self._monitor_name)
if success:
await self.coordinator.async_request_refresh()
else:
_LOGGER.error("Failed to turn on monitor %s", self._monitor_name)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the monitor off."""
success = await self._api.async_turn_monitor_off(self._monitor_name)
if success:
await self.coordinator.async_request_refresh()
else:
_LOGGER.error("Failed to turn off monitor %s", self._monitor_name)

View File

@@ -0,0 +1,80 @@
version: '3.8'
services:
homeassistant:
container_name: homeassistant
image: ghcr.io/home-assistant/home-assistant:stable
volumes:
# Main Home Assistant configuration
- ./homeassistant-config:/config
# Mount the Hyprmonitors integration directly
- ./custom_components/hyprmonitors:/config/custom_components/hyprmonitors:ro
# Optional: Mount examples for reference
- ./examples.yaml:/config/hyprmonitors-examples.yaml:ro
# System access (needed for some integrations)
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped
privileged: true
network_mode: host
environment:
# Set timezone
- TZ=America/New_York # Change to your timezone
# Health check
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8123/api/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# Optional: Include the Hyprmonitors server in the same compose file
hyprmonitors:
container_name: hyprmonitors-server
build:
context: ../
dockerfile: Dockerfile
ports:
- "3000:3000"
restart: unless-stopped
environment:
- HYPRMONITORS_HOST=0.0.0.0
- HYPRMONITORS_PORT=3000
- RUST_LOG=info
# Mount Hyprland socket (adjust path as needed)
volumes:
- /tmp/hypr:/tmp/hypr:ro
- $XDG_RUNTIME_DIR/hypr:/run/user/1000/hypr:ro
# Network access to Hyprland
network_mode: host
# Ensure Hyprland environment is available
environment:
- XDG_RUNTIME_DIR=/run/user/1000
- HYPRLAND_INSTANCE_SIGNATURE=${HYPRLAND_INSTANCE_SIGNATURE}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# Optional: Create a custom network if not using host networking
networks:
homelab:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
volumes:
homeassistant-config:
driver: local

330
homeassistant/examples.yaml Normal file
View File

@@ -0,0 +1,330 @@
# Sample Home Assistant configuration for Hyprland Monitor Control
# Copy relevant sections to your configuration files
# Automations
automation:
# Basic presence-based control
- alias: "Monitors - Turn off when away"
description: "Turn off all monitors when nobody is home"
trigger:
- platform: state
entity_id: group.all_persons
to: "not_home"
for: "00:05:00" # Wait 5 minutes to avoid false triggers
action:
- service: hyprmonitors.turn_off_all_monitors
- service: notify.persistent_notification
data:
title: "Monitor Control"
message: "All monitors turned off - nobody home"
- alias: "Monitors - Turn on when arriving home"
description: "Turn on monitors when someone arrives home"
trigger:
- platform: state
entity_id: group.all_persons
to: "home"
condition:
- condition: time
after: "07:00:00"
before: "23:00:00"
action:
- service: hyprmonitors.turn_on_all_monitors
- service: notify.persistent_notification
data:
title: "Monitor Control"
message: "Monitors turned on - welcome home!"
# Time-based control
- alias: "Monitors - Morning startup"
description: "Turn on monitors in the morning on weekdays"
trigger:
- platform: time
at: "07:30:00"
condition:
- condition: time
weekday:
- mon
- tue
- wed
- thu
- fri
- condition: state
entity_id: group.all_persons
state: "home"
action:
- service: switch.turn_on
entity_id: switch.hyprmonitors_all_monitors
- alias: "Monitors - Evening shutdown"
description: "Turn off monitors late at night"
trigger:
- platform: time
at: "23:30:00"
action:
- service: switch.turn_off
entity_id: switch.hyprmonitors_all_monitors
# Work mode automation
- alias: "Monitors - Focus mode"
description: "Turn off secondary monitors during focus time"
trigger:
- platform: state
entity_id: input_boolean.focus_mode
to: "on"
action:
- service: hyprmonitors.turn_off_monitor
data:
monitor: "DP-2" # Adjust to your secondary monitor
- service: hyprmonitors.turn_off_monitor
data:
monitor: "HDMI-A-1" # Adjust as needed
- alias: "Monitors - Exit focus mode"
description: "Turn on all monitors when exiting focus mode"
trigger:
- platform: state
entity_id: input_boolean.focus_mode
to: "off"
action:
- service: hyprmonitors.turn_on_all_monitors
# Meeting automation (requires calendar integration)
- alias: "Monitors - Meeting mode"
description: "Adjust monitors for meetings"
trigger:
- platform: calendar
event: start
entity_id: calendar.work
condition:
- condition: template
value_template: >
{{ 'meeting' in trigger.calendar_event.summary.lower() or
'call' in trigger.calendar_event.summary.lower() }}
action:
# Keep only main monitor on for meetings
- service: hyprmonitors.turn_on_monitor
data:
monitor: "DP-1" # Main monitor
- service: hyprmonitors.turn_off_monitor
data:
monitor: "DP-2" # Secondary monitor
# Power saving based on computer usage
- alias: "Monitors - Idle detection"
description: "Turn off monitors when computer is idle"
trigger:
- platform: state
entity_id: binary_sensor.computer_active # You'd need to create this
to: "off"
for: "00:10:00" # 10 minutes idle
condition:
- condition: time
after: "09:00:00"
before: "22:00:00"
action:
- service: hyprmonitors.turn_off_all_monitors
- alias: "Monitors - Activity detected"
description: "Turn on monitors when computer becomes active"
trigger:
- platform: state
entity_id: binary_sensor.computer_active
to: "on"
condition:
- condition: state
entity_id: group.all_persons
state: "home"
action:
- service: hyprmonitors.turn_on_all_monitors
# Input Booleans for manual control
input_boolean:
focus_mode:
name: "Focus Mode"
icon: mdi:focus-field
monitor_automation:
name: "Monitor Automation Enabled"
initial: true
icon: mdi:auto-mode
# Scripts for complex monitor control
script:
gaming_mode:
alias: "Gaming Mode"
description: "Optimize monitors for gaming"
sequence:
- service: hyprmonitors.turn_on_monitor
data:
monitor: "DP-1" # Main gaming monitor
- service: hyprmonitors.turn_off_monitor
data:
monitor: "DP-2" # Turn off secondary to reduce distractions
- service: notify.persistent_notification
data:
title: "Monitor Control"
message: "Gaming mode activated"
work_mode:
alias: "Work Mode"
description: "Setup monitors for work"
sequence:
- service: hyprmonitors.turn_on_all_monitors
- delay: "00:00:02" # Small delay between commands
- service: notify.persistent_notification
data:
title: "Monitor Control"
message: "Work mode activated - all monitors on"
presentation_mode:
alias: "Presentation Mode"
description: "Setup for presentations"
sequence:
- service: hyprmonitors.turn_on_monitor
data:
monitor: "HDMI-A-1" # Projector/external display
- service: hyprmonitors.turn_on_monitor
data:
monitor: "DP-1" # Laptop screen for notes
- service: notify.persistent_notification
data:
title: "Monitor Control"
message: "Presentation mode ready"
# Sensors for monitoring
sensor:
- platform: template
sensors:
monitor_power_usage:
friendly_name: "Estimated Monitor Power Usage"
unit_of_measurement: "W"
value_template: >
{% set total_monitors = state_attr('sensor.hyprmonitors_monitor_status', 'total_monitors') | int %}
{% set monitors_on = state_attr('sensor.hyprmonitors_monitor_status', 'monitors_on') | int %}
{{ monitors_on * 30 }} # Assuming 30W per monitor
icon_template: mdi:lightning-bolt
# Groups for organization
group:
monitor_controls:
name: "Monitor Controls"
entities:
- switch.hyprmonitors_all_monitors
- sensor.hyprmonitors_monitor_status
- input_boolean.focus_mode
- input_boolean.monitor_automation
# Lovelace Dashboard Configuration
# Add this to your dashboard YAML or use the UI editor
# Main Monitor Control Card
type: vertical-stack
cards:
- type: entities
title: "Monitor Status"
entities:
- entity: sensor.hyprmonitors_monitor_status
name: "Overall Status"
icon: mdi:monitor-multiple
- entity: sensor.monitor_power_usage
name: "Est. Power Usage"
- type: divider
- entity: switch.hyprmonitors_all_monitors
name: "All Monitors"
icon: mdi:power
- type: horizontal-stack
cards:
- type: button
entity: script.work_mode
name: "Work Mode"
icon: mdi:briefcase
tap_action:
action: call-service
service: script.work_mode
- type: button
entity: script.gaming_mode
name: "Gaming"
icon: mdi:gamepad-variant
tap_action:
action: call-service
service: script.gaming_mode
- type: button
entity: script.presentation_mode
name: "Present"
icon: mdi:presentation
tap_action:
action: call-service
service: script.presentation_mode
- type: entities
title: "Individual Monitors"
entities:
- entity: switch.hyprmonitors_dp_1
name: "Main Monitor (DP-1)"
icon: mdi:monitor
- entity: switch.hyprmonitors_dp_2
name: "Secondary (DP-2)"
icon: mdi:monitor
- entity: switch.hyprmonitors_hdmi_a_1
name: "External (HDMI)"
icon: mdi:monitor-speaker
- type: entities
title: "Automation Settings"
entities:
- entity: input_boolean.monitor_automation
name: "Enable Automation"
- entity: input_boolean.focus_mode
name: "Focus Mode"
# Alternative Glance Card Layout
type: glance
entities:
- entity: sensor.hyprmonitors_monitor_status
name: "Status"
- entity: switch.hyprmonitors_all_monitors
name: "All"
- entity: switch.hyprmonitors_dp_1
name: "Main"
- entity: switch.hyprmonitors_dp_2
name: "Secondary"
title: "Monitor Quick Control"
columns: 4
# Advanced Card with Custom Button Row
type: custom:button-card
name: "Monitor Control Hub"
styles:
card:
- height: 120px
tap_action:
action: more-info
entity: sensor.hyprmonitors_monitor_status
custom_fields:
status: |
[[[
const status = states['sensor.hyprmonitors_monitor_status'].state;
const icon = status === 'all_on' ? 'mdi:monitor-multiple' :
status === 'all_off' ? 'mdi:monitor-off' : 'mdi:monitor-shimmer';
return `<ha-icon icon="${icon}" style="color: var(--primary-color); font-size: 24px;"></ha-icon>`;
]]]
buttons: |
[[[
return `
<div style="display: flex; gap: 8px; margin-top: 10px;">
<mwc-button dense outlined onclick="hass.callService('script', 'turn_on', {entity_id: 'script.work_mode'})">
Work
</mwc-button>
<mwc-button dense outlined onclick="hass.callService('script', 'turn_on', {entity_id: 'script.gaming_mode'})">
Game
</mwc-button>
<mwc-button dense outlined onclick="hass.callService('hyprmonitors', 'turn_off_all_monitors')">
Off
</mwc-button>
</div>
`;
]]]

316
homeassistant/install.sh Executable file
View File

@@ -0,0 +1,316 @@
#!/bin/bash
# Hyprland Monitor Control - Home Assistant Integration Installer
# This script helps install the custom integration into Home Assistant
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default paths
DEFAULT_HA_CONFIG="$HOME/.homeassistant"
DEFAULT_HA_CONFIG_ALT="/config"
INTEGRATION_NAME="hyprmonitors"
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
print_header() {
echo -e "${BLUE}================================================${NC}"
echo -e "${BLUE} Hyprland Monitor Control - HA Integration${NC}"
echo -e "${BLUE}================================================${NC}"
echo
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
detect_ha_config() {
local config_dir=""
# Try common Home Assistant config directories
if [[ -d "$DEFAULT_HA_CONFIG" ]]; then
config_dir="$DEFAULT_HA_CONFIG"
elif [[ -d "$DEFAULT_HA_CONFIG_ALT" ]]; then
config_dir="$DEFAULT_HA_CONFIG_ALT"
elif [[ -d "/usr/share/hassio/homeassistant" ]]; then
config_dir="/usr/share/hassio/homeassistant"
elif [[ -d "/data" ]]; then
config_dir="/data"
fi
echo "$config_dir"
}
check_ha_running() {
if command -v systemctl &> /dev/null; then
if systemctl is-active --quiet home-assistant || systemctl is-active --quiet homeassistant; then
return 0
fi
fi
# Check for common HA processes
if pgrep -f "homeassistant" &> /dev/null || pgrep -f "hass" &> /dev/null; then
return 0
fi
return 1
}
create_backup() {
local target_dir="$1"
local backup_dir="${target_dir}.backup.$(date +%Y%m%d_%H%M%S)"
if [[ -d "$target_dir" ]]; then
print_info "Creating backup of existing integration..."
cp -r "$target_dir" "$backup_dir"
print_success "Backup created: $backup_dir"
return 0
fi
return 1
}
install_integration() {
local ha_config="$1"
local custom_components_dir="$ha_config/custom_components"
local target_dir="$custom_components_dir/$INTEGRATION_NAME"
local source_components_dir="$SOURCE_DIR/custom_components/$INTEGRATION_NAME"
# Verify source directory exists
if [[ ! -d "$source_components_dir" ]]; then
print_error "Source integration directory not found: $source_components_dir"
exit 1
fi
# Create custom_components directory if it doesn't exist
mkdir -p "$custom_components_dir"
print_success "Custom components directory ready: $custom_components_dir"
# Create backup if integration already exists
create_backup "$target_dir"
# Copy integration files
print_info "Installing Hyprland Monitor Control integration..."
cp -r "$source_components_dir" "$target_dir"
# Set appropriate permissions
chmod -R 755 "$target_dir"
print_success "Integration installed successfully!"
print_info "Installation location: $target_dir"
# List installed files
echo
print_info "Installed files:"
find "$target_dir" -type f -name "*.py" -o -name "*.json" -o -name "*.yaml" | sort | while read -r file; do
echo " - ${file#$target_dir/}"
done
}
verify_installation() {
local ha_config="$1"
local target_dir="$ha_config/custom_components/$INTEGRATION_NAME"
# Check required files
local required_files=(
"__init__.py"
"manifest.json"
"config_flow.py"
"const.py"
"sensor.py"
"switch.py"
"strings.json"
"services.yaml"
)
print_info "Verifying installation..."
for file in "${required_files[@]}"; do
if [[ -f "$target_dir/$file" ]]; then
print_success "Found: $file"
else
print_error "Missing: $file"
return 1
fi
done
# Check manifest.json structure
if command -v python3 &> /dev/null; then
if python3 -c "import json; json.load(open('$target_dir/manifest.json'))" 2>/dev/null; then
print_success "manifest.json is valid JSON"
else
print_warning "manifest.json may have syntax errors"
fi
fi
return 0
}
check_hyprmonitors_server() {
local host="${1:-localhost}"
local port="${2:-3000}"
print_info "Checking Hyprmonitors server connection..."
if command -v curl &> /dev/null; then
if curl -s --max-time 5 "http://$host:$port/health" > /dev/null 2>&1; then
print_success "Hyprmonitors server is reachable at $host:$port"
return 0
else
print_warning "Hyprmonitors server not reachable at $host:$port"
print_info "Make sure the Rust server is running: cargo run"
return 1
fi
else
print_warning "curl not available, cannot test server connection"
return 1
fi
}
show_next_steps() {
echo
print_info "Installation complete! Next steps:"
echo
echo "1. Restart Home Assistant"
echo " - If using systemctl: sudo systemctl restart home-assistant"
echo " - If using Docker: docker restart homeassistant"
echo " - If using HAOS: Settings → System → Restart"
echo
echo "2. 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
echo "3. 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
print_success "Enjoy controlling your monitors from Home Assistant! 🖥️"
}
usage() {
echo "Usage: $0 [OPTIONS]"
echo
echo "Options:"
echo " -c, --config-dir DIR Home Assistant configuration directory"
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
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 --dry-run # Preview installation"
}
main() {
local ha_config=""
local check_server=""
local dry_run=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-c|--config-dir)
ha_config="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
--check-server)
check_server="$2"
shift 2
;;
--dry-run)
dry_run=true
shift
;;
*)
print_error "Unknown option: $1"
usage
exit 1
;;
esac
done
print_header
# Handle server check
if [[ -n "$check_server" ]]; then
IFS=':' read -r host port <<< "$check_server"
check_hyprmonitors_server "$host" "$port"
exit $?
fi
# Detect Home Assistant config directory
if [[ -z "$ha_config" ]]; then
ha_config=$(detect_ha_config)
if [[ -z "$ha_config" ]]; then
print_error "Could not find Home Assistant configuration directory"
print_info "Please specify it with: $0 -c /path/to/homeassistant/config"
exit 1
fi
print_info "Auto-detected HA config: $ha_config"
fi
# Verify config directory exists
if [[ ! -d "$ha_config" ]]; then
print_error "Home Assistant config directory not found: $ha_config"
exit 1
fi
# Check if HA is running
if check_ha_running; then
print_warning "Home Assistant appears to be running"
print_info "You'll need to restart it after installation"
fi
if [[ "$dry_run" == true ]]; then
print_info "DRY RUN - Would install integration to:"
print_info " Source: $SOURCE_DIR/custom_components/$INTEGRATION_NAME"
print_info " Target: $ha_config/custom_components/$INTEGRATION_NAME"
exit 0
fi
# Install the integration
install_integration "$ha_config"
# Verify installation
if verify_installation "$ha_config"; then
print_success "Installation verification passed!"
else
print_error "Installation verification failed!"
exit 1
fi
# Check server connection
check_hyprmonitors_server
# Show next steps
show_next_steps
}
# Run main function
main "$@"