Add Home Manager service integration

This commit is contained in:
ltadeu6 2026-04-15 17:34:03 -03:00
parent be697a6d1c
commit 6cdfab05c0
No known key found for this signature in database
GPG key ID: FB9FDAB6B6F3418D
3 changed files with 216 additions and 4 deletions

102
README.md Normal file
View file

@ -0,0 +1,102 @@
# music-light
`luz-musica.py` changes a Home Assistant light to match the active media artwork.
## Home Manager integration
This repository includes a Home Manager module at [home-manager/music-light.nix](/home/ltadeu6/music-light/home-manager/music-light.nix) that creates:
- a `systemd --user` service called `music-light.service`
- helper commands:
- `toggle-music-light`
- `start-music-light`
- `stop-music-light`
- `music-light-status`
- an optional desktop launcher: `Music Light Toggle`
### 1. Import the module
Add this to your Home Manager configuration:
```nix
{
imports = [
/home/ltadeu6/music-light/home-manager/music-light.nix
];
programs.music-light = {
enable = true;
scriptDir = "/home/ltadeu6/music-light";
pythonPath = "/usr/bin/python3";
playerctlPath = "/usr/bin/playerctl";
enableDesktopEntry = true;
autostart = false;
};
}
```
If your Home Manager config is inside a flake, add the module to the relevant `homeManagerConfiguration`.
### 2. Install the host runtime on Ubuntu
On the Ubuntu host, install the runtime used by the service:
```bash
sudo apt update
sudo apt install -y python3 python3-venv playerctl
cd /home/ltadeu6/music-light
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
```
Then point Home Manager at the venv interpreter:
```nix
programs.music-light.pythonPath = "/home/ltadeu6/music-light/.venv/bin/python";
```
If you prefer system-wide Python packages instead of a venv, keep `pythonPath = "/usr/bin/python3"` and make sure `requests` and `Pillow` are installed on the host.
### 3. Apply Home Manager for the host user
The service must run in the host user's `systemd --user`, not inside the Docker container.
The Docker container can generate the Home Manager files as long as it writes into the real home directory used by the Ubuntu user. After activation, reload the user units on the host:
```bash
systemctl --user daemon-reload
```
Then control it with:
```bash
toggle-music-light
music-light-status
start-music-light
stop-music-light
journalctl --user -u music-light.service -f
```
### 4. Desktop button
With `enableDesktopEntry = true`, Home Manager creates a launcher named `Music Light Toggle`.
It typically appears in:
- the application menu
- launchers that read `.desktop` files
- `~/.local/share/applications/`
The generated launcher simply runs `toggle-music-light`, which starts or stops `music-light.service`.
## Important detail about Docker + Ubuntu host
If Home Manager is executed inside a container, that is fine for generating the user configuration files, but:
- the target home directory must be the real home directory used by the host user
- the service is started by the host's `systemd --user`
- `python3`, `playerctl`, the virtualenv, and the media-session DBus must all be available on the host
- the service must not point to executables that exist only inside the container or its `/nix/store`
If `systemctl --user` inside the container does not control the host user session, run the final activation from the host side.

View file

@ -0,0 +1,108 @@
{ config, lib, pkgs, ... }:
let
cfg = config.programs.music-light;
toggleScript = pkgs.writeShellScriptBin "toggle-music-light" ''
if systemctl --user is-active --quiet music-light.service; then
exec systemctl --user stop music-light.service
else
exec systemctl --user start music-light.service
fi
'';
startScript = pkgs.writeShellScriptBin "start-music-light" ''
exec systemctl --user start music-light.service
'';
stopScript = pkgs.writeShellScriptBin "stop-music-light" ''
exec systemctl --user stop music-light.service
'';
statusScript = pkgs.writeShellScriptBin "music-light-status" ''
if systemctl --user is-active --quiet music-light.service; then
echo on
else
echo off
fi
'';
in
{
options.programs.music-light = {
enable = lib.mkEnableOption "music-light integration managed by Home Manager";
scriptDir = lib.mkOption {
type = lib.types.str;
default = "${config.home.homeDirectory}/music-light";
example = "/home/ltadeu6/music-light";
description = "Directory that contains `luz-musica.py`.";
};
pythonPath = lib.mkOption {
type = lib.types.str;
default = "/usr/bin/python3";
example = "/usr/bin/python3";
description = "Path to the host Python interpreter used by the service.";
};
playerctlPath = lib.mkOption {
type = lib.types.str;
default = "/usr/bin/playerctl";
example = "/usr/bin/playerctl";
description = "Path to the host `playerctl` binary used by the service.";
};
enableDesktopEntry = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to create a desktop launcher that toggles the service.";
};
autostart = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to start `music-light.service` automatically at login.";
};
};
config = lib.mkIf cfg.enable {
home.packages = [
toggleScript
startScript
stopScript
statusScript
];
xdg.desktopEntries = lib.mkIf cfg.enableDesktopEntry {
music-light-toggle = {
name = "Music Light Toggle";
comment = "Start or stop the music-light sync service";
exec = "${toggleScript}/bin/toggle-music-light";
terminal = false;
categories = [ "Utility" ];
};
};
systemd.user.services.music-light = {
Unit = {
Description = "Sync Home Assistant light color with the active media artwork";
After = [ "graphical-session.target" "network-online.target" ];
PartOf = [ "graphical-session.target" ];
};
Service = {
Type = "simple";
WorkingDirectory = cfg.scriptDir;
ExecStart = "${cfg.pythonPath} ${cfg.scriptDir}/luz-musica.py";
Restart = "on-failure";
RestartSec = 3;
Environment = [
"PATH=/usr/local/bin:/usr/bin:/bin"
"PLAYERCTL_BIN=${cfg.playerctlPath}"
];
};
Install.WantedBy = lib.mkIf cfg.autostart [ "default.target" ];
};
};
}

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import colorsys
import math
import os
import subprocess
import time
from io import BytesIO
@ -26,10 +27,11 @@ PALETTE_SIZE = 32
OKLCH_LIGHTNESS = 0.72
OKLCH_CHROMA = 0.24
MAX_BRIGHTNESS_STEP = 0.10
PLAYERCTL_BIN = os.environ.get("PLAYERCTL_BIN", "playerctl")
def run_playerctl(field: str, player: str | None = None) -> str:
command = ["playerctl"]
command = [PLAYERCTL_BIN]
if player:
command.extend(["--player", player])
command.extend(["metadata", field])
@ -46,7 +48,7 @@ def run_playerctl(field: str, player: str | None = None) -> str:
def list_players() -> list[str]:
result = subprocess.run(
["playerctl", "-l"],
[PLAYERCTL_BIN, "-l"],
capture_output=True,
text=True,
)
@ -57,7 +59,7 @@ def list_players() -> list[str]:
def player_status(player: str) -> str:
result = subprocess.run(
["playerctl", "--player", player, "status"],
[PLAYERCTL_BIN, "--player", player, "status"],
capture_output=True,
text=True,
)
@ -79,7 +81,7 @@ def get_track_url(player: str) -> str:
def get_player_name(player: str) -> str:
result = subprocess.run(
["playerctl", "--player", player, "metadata", "--format", "{{playerName}}"],
[PLAYERCTL_BIN, "--player", player, "metadata", "--format", "{{playerName}}"],
capture_output=True,
text=True,
)