feat: Added homeassistant plugin
This commit is contained in:
315
homeassistant/README.md
Normal file
315
homeassistant/README.md
Normal 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.
|
||||||
206
homeassistant/custom_components/hyprmonitors/__init__.py
Normal file
206
homeassistant/custom_components/hyprmonitors/__init__.py
Normal 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,
|
||||||
|
)
|
||||||
134
homeassistant/custom_components/hyprmonitors/config_flow.py
Normal file
134
homeassistant/custom_components/hyprmonitors/config_flow.py
Normal 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."""
|
||||||
32
homeassistant/custom_components/hyprmonitors/const.py
Normal file
32
homeassistant/custom_components/hyprmonitors/const.py
Normal 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"
|
||||||
17
homeassistant/custom_components/hyprmonitors/manifest.json
Normal file
17
homeassistant/custom_components/hyprmonitors/manifest.json
Normal 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": []
|
||||||
|
}
|
||||||
207
homeassistant/custom_components/hyprmonitors/sensor.py
Normal file
207
homeassistant/custom_components/hyprmonitors/sensor.py
Normal 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
|
||||||
|
)
|
||||||
29
homeassistant/custom_components/hyprmonitors/services.yaml
Normal file
29
homeassistant/custom_components/hyprmonitors/services.yaml
Normal 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
|
||||||
88
homeassistant/custom_components/hyprmonitors/strings.json
Normal file
88
homeassistant/custom_components/hyprmonitors/strings.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
218
homeassistant/custom_components/hyprmonitors/switch.py
Normal file
218
homeassistant/custom_components/hyprmonitors/switch.py
Normal 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)
|
||||||
80
homeassistant/docker-compose.example.yml
Normal file
80
homeassistant/docker-compose.example.yml
Normal 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
330
homeassistant/examples.yaml
Normal 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
316
homeassistant/install.sh
Executable 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 "$@"
|
||||||
Reference in New Issue
Block a user