18 Commits

Author SHA1 Message Date
finn.markwitz b3471a308a feat(home-assistant): add alexa_media_player custom component
Package the Alexa Media Player HACS component declaratively (v5.15.7),
so HA can drive the Echo devices (TTS/announce, media, sensors). Needs
dictor 0.1.12 (absent from nixpkgs) and an alexapy 1.29.22 -> 1.29.25
bump to satisfy the manifest's exact requirement pins; both build
against HA's interpreter. Config-flow based: add via the HA UI and sign
in with the Amazon account after rebuild.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RCn8YgGQMGpauatdeTdQYf
2026-07-29 17:11:25 +02:00
finn.markwitz bf36f935e8 docs(home-assistant): implementation plan for alexa_media_player
Task-by-task plan with verified hashes and build-check commands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RCn8YgGQMGpauatdeTdQYf
2026-07-29 17:07:05 +02:00
finn.markwitz 7bfbf4de9a docs(home-assistant): design for alexa_media_player integration
Spec for packaging the Alexa Media Player HACS component declaratively
on jupiter (HA -> Echo: TTS/announce, media, sensors). Requires bumping
alexapy 1.29.22 -> 1.29.25 and packaging dictor 0.1.12 (absent from
nixpkgs) to satisfy the manifest's exact requirement pins. Alexa -> HA
voice control is explicitly deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RCn8YgGQMGpauatdeTdQYf
2026-07-29 17:01:41 +02:00
finn.markwitz 28b23fe9f2 feat(home-assistant): add ha-sourdough custom component
Package the Sourdough Monitor HACS component declaratively via
buildHomeAssistantComponent (pinned to v1.1.3), avoiding a HACS
runtime. Config-flow based, so it's added through the HA UI after
rebuild. No external pip deps; requires HA >= 2026.3.0 (have 2026.5.4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NHApfYHvEmVLpsqkXdx4oq
2026-07-28 16:31:18 +02:00
finn.markwitz f5f8e88966 Merge pull request 'revert(mibook): remove boot-time terminal/desktop choice feature' (#7) from revert/mibook-boot-choice into main
Reviewed-on: #7
2026-07-28 15:21:01 +02:00
finn.markwitz bf874d962a revert(mibook): remove boot-time terminal/desktop choice feature
Back out the entire "choose terminal-only vs KDE at boot" work. The first
attempt (default multi-user.target + on-demand desktop) locked mibook out —
it is WiFi-only with credentials in KWallet, so a headless boot has no
network and no SSH. The follow-up specialisation redesign is no longer
wanted either.

mibook returns to plain always-KDE boot (the pre-feature baseline):
- machines/mibook/configuration.nix: drop specialisation.terminal and the
  NetworkManager-wait-online override.
- machines/mibook/environments.nix: drop kde-desktop.startOnBoot.
- modules/environments/kde-desktop/default.nix: drop the startOnBoot option.
- remove the boot-desktop-choice design spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1wvPy5kbGp8pUQX61WBCY
2026-07-28 14:51:31 +02:00
finn.markwitz f01b1f2c4f Merge pull request 'Feat/jellyfin hw transcoding' (#3) from feat/jellyfin-hw-transcoding into main
Reviewed-on: #3
2026-07-27 09:56:11 +02:00
finn.markwitz ff34fe762e Merge pull request 'Feat/mibook boot desktop choice' (#4) from feat/mibook-boot-desktop-choice into main
Reviewed-on: #4
2026-07-27 09:55:47 +02:00
finn.markwitz 7e4407a1f8 feat(mibook): boot to terminal, launch KDE on demand
Add my.profiles.kde-desktop.startOnBoot (default true, no change for
existing consumers). When false, boot to multi-user.target, keep SDDM
out of any boot target's wants, and provide a 'desktop' command that
starts the display manager on demand. Enable this on mibook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CfozKLQdUh5TzqyjSigLUx
2026-07-27 09:48:13 +02:00
finn.markwitz 539fb26791 docs: spec for mibook boot-time terminal/desktop choice
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CfozKLQdUh5TzqyjSigLUx
2026-07-27 09:27:37 +02:00
marthsincemelee 003a2f77dd docs: record post-deploy QSV/OpenCL runtime fixes in spec
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:42:41 +02:00
marthsincemelee 86e7f9c1a8 fix(jupiter): add Intel compute-runtime for OpenCL HDR tone-mapping
HDR HEVC transcodes were failing with "Failed to get number of OpenCL
platforms: -1001" (CL_PLATFORM_NOT_FOUND_KHR). The tonemap_opencl
filter jellyfin-ffmpeg uses for HDR-to-SDR tone-mapping needs an OpenCL
ICD, which intel-media-driver/vpl-gpu-rt don't provide on their own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:34:36 +02:00
marthsincemelee adb7fcfad7 fix(jupiter): add oneVPL/MFX runtime for QSV hardware transcoding
HEVC HDR transcodes were failing with "Error creating a MFX session: -9"
because intel-media-driver only provides VAAPI, not the separate
oneVPL/MFX runtime QSV needs to create a hardware session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:27:31 +02:00
marthsincemelee 5235f5abcb feat(jellyfin): grant iGPU access for Quick Sync hardware transcoding 2026-07-26 14:51:01 +02:00
marthsincemelee f53f2331d0 docs: add implementation plan for Jellyfin hardware transcoding
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:31:20 +02:00
marthsincemelee 84cec9e935 docs: add design spec for Jellyfin hardware transcoding on jupiter
Jupiter's Intel iGPU is configured at the OS level but Jellyfin has no
access to it, so transcodes run on CPU only and stutter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:27:30 +02:00
finn.markwitz 79b26ddfda chore: Flake Update 2026-07-24 12:01:15 +02:00
finn.markwitz b8961fb47d Merge pull request 'feat(mibook): do nothing on lid close while on AC power' (#2) from feat/mibook-lid-close-ac into main
Reviewed-on: #2
2026-07-24 11:56:02 +02:00
9 changed files with 732 additions and 15 deletions
@@ -0,0 +1,154 @@
# Jellyfin Hardware Transcoding (jupiter) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Give jupiter's Jellyfin service access to the Intel iGPU's VAAPI render node so Quick Sync hardware transcoding can be enabled, instead of every transcode falling back to CPU.
**Architecture:** One NixOS module change (`modules/environments/jellyfin/default.nix`) grants the `jellyfin` systemd service supplementary access to the `video`/`render` groups and installs `libva-utils` for verification. This is declarative and build-verifiable from the Mac. Enabling Quick Sync inside Jellyfin's own dashboard, and the on-machine verification, is a manual step run by the user on jupiter after deploy — the NixOS module has no option for it and this environment's convention is that the assistant never SSHes into jupiter directly (see `docs/superpowers/specs/2026-07-26-jellyfin-hw-transcoding.md`).
**Tech Stack:** NixOS (flake-parts), nixpkgs `services.jellyfin` module, VAAPI/`intel-media-driver`, `libva-utils`.
## Global Constraints
- No SSH from the assistant into jupiter — all on-machine commands are given to the user to run and paste back.
- Follow the existing profile pattern in `modules/environments/jellyfin/default.nix` (`config = lib.mkIf cfg.enable { ... }`); don't introduce a new toggle option — hardcode the hardware-acceleration wiring on, per the approved spec.
- Verify locally via `nix eval` / `nix build` before asking the user to deploy.
---
### Task 1: Grant Jellyfin access to the iGPU and verify the build
**Files:**
- Modify: `modules/environments/jellyfin/default.nix`
**Interfaces:**
- Produces: `systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [ "video" "render" ];` — verified via `nix eval` in Step 2.
- [ ] **Step 1: Add the device-access config and `libva-utils` package**
Read the current file first (`modules/environments/jellyfin/default.nix`), then edit the `config = lib.mkIf cfg.enable { ... }` block so it reads:
```nix
config = lib.mkIf cfg.enable {
services.jellyfin = {
enable = true;
openFirewall = true;
};
environment.systemPackages = [ pkgs.libva-utils ];
my.homepage.services = [
{
group = "Media";
name = "Jellyfin";
description = "Media server";
href = "http://${hostName}:${toString port}";
icon = "jellyfin.png";
}
];
systemd.services.jellyfin = {
after = [ "network-online.target" ];
serviceConfig.SupplementaryGroups = [
"video"
"render"
];
};
};
```
Note the two existing `systemd.services.jellyfin` keys (`after`) and the new `serviceConfig.SupplementaryGroups` now live in the same attrset — don't create a second `systemd.services.jellyfin = { ... }` block, it would overwrite the first.
- [ ] **Step 2: Verify the rendered config with `nix eval`**
Run (from the repo root on the Mac):
```bash
nix eval '.#nixosConfigurations.jupiter.config.systemd.services.jellyfin.serviceConfig.SupplementaryGroups' \
--extra-experimental-features 'nix-command flakes'
```
Expected output: `[ "video" "render" ]`
- [ ] **Step 3: Verify the machine still builds**
Run:
```bash
nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel' \
--extra-experimental-features 'nix-command flakes' --no-link
```
Expected: build succeeds with no errors (may take a while; watch for any evaluation error mentioning `jellyfin` or `libva-utils`).
- [ ] **Step 4: Format and commit**
```bash
nixfmt-rfc-style modules/environments/jellyfin/default.nix
git add modules/environments/jellyfin/default.nix
git commit -m "feat(jellyfin): grant iGPU access for Quick Sync hardware transcoding"
```
---
### Task 2: Deploy on jupiter and enable Quick Sync (user-executed)
**Files:** none (on-machine deploy + Jellyfin dashboard UI)
**Interfaces:**
- Consumes: the `SupplementaryGroups` change from Task 1, already merged into the flake.
These steps run **on jupiter**, by the user — paste the output back so we can confirm each one before moving to the next.
- [ ] **Step 1: Deploy**
```bash
sudo nixos-rebuild switch --flake '.#jupiter'
```
Expected: switch succeeds, no errors mentioning `jellyfin`.
- [ ] **Step 2: Confirm the service picked up the new groups**
```bash
systemctl show jellyfin -p SupplementaryGroups
systemctl status jellyfin --no-pager
```
Expected: `SupplementaryGroups=video render` (order may vary) and the service is `active (running)`.
- [ ] **Step 3: Confirm VAAPI driver loads**
```bash
vainfo
```
Expected: output starts with something like `vainfo: VA-API version: 1.x` and `Driver version: Intel iHD driver`, followed by a list of supported VAProfiles/VAEntrypoints (e.g. `VAProfileH264Main : VAEntrypointVLD`, `VAEntrypointEncSlice`).
If this instead prints a permissions or "no VA display" error, paste it back — that means the group grant isn't reaching the process and Task 1 needs a follow-up fix (e.g. the jellyfin service may be more sandboxed than expected, requiring an explicit `DeviceAllow=char-drm rw` in `serviceConfig` as well).
- [ ] **Step 4: Enable Quick Sync in the Jellyfin dashboard**
In the Jellyfin web UI:
1. **Dashboard → Playback**.
2. Hardware acceleration: **Intel QuickSync (QSV)**.
3. VA-API device: `/dev/dri/renderD128`.
4. Enable hardware decoding for the codecs your library uses (H264 at minimum).
5. If the library has HDR content, enable tone-mapping.
6. Save.
- [ ] **Step 5: Functional check**
Play a file that requires transcoding (or force a lower quality in the client's playback settings to trigger one), then:
```bash
journalctl -u jellyfin -n 50 --no-pager
```
Look for a line referencing `qsv` or `vaapi` in the transcode command. Separately, watch CPU usage (`htop`) during playback — it should stay low on the core doing the transcode, rather than pegging at 100%, since the iGPU is now doing the encode/decode work.
## Self-Review Notes
- Spec coverage: NixOS change (Task 1) ✓, manual dashboard step (Task 2 Step 4) ✓, verification via `vainfo`/build (Task 1 Step 2-3, Task 2 Step 3) ✓, functional check (Task 2 Step 5) ✓. Toggle option explicitly excluded per approved spec — not present, correctly.
- No placeholders — every step has literal commands/code.
- `SupplementaryGroups` key/value matches exactly between Task 1 (produced) and Task 2 (consumed/checked).
@@ -0,0 +1,189 @@
# Alexa Media Player Integration — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Package the `alexa_media_player` HACS component declaratively on jupiter's Home Assistant so HA can drive the household Echo devices (TTS/announce, media, sensors).
**Architecture:** Add one `buildHomeAssistantComponent` entry to `services.home-assistant.customComponents` (same pattern as the existing `ha-sourdough`). The component has real Python deps, so the module's `let` block also defines two supporting derivations built against HA's own interpreter: a new `dictor` package and an `alexapy` version bump. No HACS runtime, no YAML config — the integration is added through the HA UI after the rebuild.
**Tech Stack:** NixOS, `pkgs.buildHomeAssistantComponent`, `buildPythonPackage`, `fetchFromGitHub`/`fetchFromGitLab`/`fetchPypi`.
## Global Constraints
- Target machine: **jupiter** only. All edits live in `modules/environments/home-assistant/default.nix`.
- Python dep derivations MUST build against Home Assistant's interpreter set — `pkgs.home-assistant.python3Packages` — the same set `buildHomeAssistantComponent` uses. Do not use top-level `pkgs.python3Packages`.
- Manifest requirements are enforced at build time by `manifestCheckPhase` and again by HA at runtime. Every requirement must resolve to an installed dist satisfying its specifier. Exact pins (verbatim from `alexa_media_player` v5.15.7 `manifest.json`):
- `alexapy==1.29.25`
- `dictor>=0.1.12,<0.2`
- `wrapt>=1.14.0` (nixpkgs 1.17.2 — already satisfied)
- `packaging>=20.3` (nixpkgs 26.1 — already satisfied)
- Pinned artifacts (all three hashes verified to build during planning):
| Artifact | Fetcher | Ref / version | Hash |
| ------------------ | ---------------- | ------------- | ------------------------------------------------- |
| alexa_media_player | fetchFromGitHub | `v5.15.7` | `sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI=` |
| alexapy | fetchFromGitLab | `v1.29.25` | `sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4=` |
| dictor | fetchPypi (sdist)| `0.1.12` | `sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs=` |
---
## File Structure
Single file touched: `modules/environments/home-assistant/default.nix`.
- Its `let` block gains `haPython`, `dictor`, and `alexapy` bindings.
- Its `services.home-assistant.customComponents` list gains a second entry (`alexa_media`) alongside the existing `sourdough` entry.
No new files. The two supporting derivations are small and specific to this component, so they live inline in the module's `let` block next to their only consumer (files that change together live together).
---
### Task 1: Package `alexa_media_player` with its Python dependencies
**Files:**
- Modify: `modules/environments/home-assistant/default.nix` (the `let` block, currently lines 9-12; and the `customComponents` list, currently lines 29-41)
**Interfaces:**
- Consumes: `pkgs.home-assistant.python3Packages` (HA interpreter set), `pkgs.buildHomeAssistantComponent`, `pkgs.fetchFromGitHub`, `pkgs.fetchFromGitLab`.
- Produces: a second `customComponents` entry with `domain = "alexa_media"`. No other module consumes these `let` bindings.
- [ ] **Step 1: Add the supporting derivations to the `let` block**
Edit the `let` block so it reads exactly:
```nix
let
cfg = config.my.profiles.home-assistant;
hostName = config.networking.hostName;
# Python deps for the alexa_media_player custom component (Task: Alexa).
# Built against Home Assistant's own interpreter — the same set
# buildHomeAssistantComponent uses — so HA's build-time and runtime
# manifest-requirement checks are satisfied.
haPython = pkgs.home-assistant.python3Packages;
# dictor is not in nixpkgs; alexa_media_player needs dictor>=0.1.12,<0.2.
# Pure-Python, no runtime deps, legacy setup.py.
dictor = haPython.buildPythonPackage {
pname = "dictor";
version = "0.1.12";
format = "setuptools";
src = haPython.fetchPypi {
pname = "dictor";
version = "0.1.12";
hash = "sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs=";
};
build-system = [ haPython.setuptools ];
doCheck = false;
pythonImportsCheck = [ "dictor" ];
};
# nixpkgs ships alexapy 1.29.22; the manifest pins ==1.29.25. Patch bump.
# nixpkgs fetches alexapy from GitLab (keatontaylor/alexapy), tag v<version>.
alexapy = haPython.alexapy.overridePythonAttrs (old: {
version = "1.29.25";
src = pkgs.fetchFromGitLab {
owner = "keatontaylor";
repo = "alexapy";
tag = "v1.29.25";
hash = "sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4=";
};
});
in
```
- [ ] **Step 2: Add the `alexa_media` entry to `customComponents`**
In `services.home-assistant.customComponents`, immediately after the closing `)` of the existing `sourdough` entry (current line 40) and before the list's closing `]` (current line 41), add:
```nix
(pkgs.buildHomeAssistantComponent {
owner = "Alandtse";
domain = "alexa_media";
version = "5.15.7";
src = pkgs.fetchFromGitHub {
owner = "Alandtse";
repo = "alexa_media_player";
rev = "v5.15.7";
hash = "sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI=";
};
# Every manifest requirement must be importable at a satisfying
# version or manifestCheckPhase fails the build.
dependencies = [
alexapy
dictor
haPython.wrapt
haPython.packaging
];
})
```
- [ ] **Step 3: Format the file**
Run: `nixfmt-rfc-style modules/environments/home-assistant/default.nix`
Expected: exits 0, no diff surprises (re-read the file if unsure).
- [ ] **Step 4: Build the component in isolation first (fast feedback)**
This is the real test: it runs `manifestCheckPhase`, which fails loudly if any of the four requirements is unmet. It was verified to succeed during planning.
Run:
```bash
nix build --impure --no-link --print-out-paths --expr '
let
pkgs = (builtins.getFlake (toString ./.)).nixosConfigurations.jupiter.pkgs;
py = pkgs.home-assistant.python3Packages;
dictor = py.buildPythonPackage {
pname = "dictor"; version = "0.1.12"; format = "setuptools";
src = py.fetchPypi { pname = "dictor"; version = "0.1.12"; hash = "sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs="; };
build-system = [ py.setuptools ]; doCheck = false; pythonImportsCheck = [ "dictor" ];
};
alexapy = py.alexapy.overridePythonAttrs (old: {
version = "1.29.25";
src = pkgs.fetchFromGitLab { owner = "keatontaylor"; repo = "alexapy"; tag = "v1.29.25"; hash = "sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4="; };
});
in pkgs.buildHomeAssistantComponent {
owner = "Alandtse"; domain = "alexa_media"; version = "5.15.7";
src = pkgs.fetchFromGitHub { owner = "Alandtse"; repo = "alexa_media_player"; rev = "v5.15.7"; hash = "sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI="; };
dependencies = [ alexapy dictor py.wrapt py.packaging ];
}'
```
Expected: prints a `/nix/store/...-Alandtse-alexa_media-5.15.7` path, exit 0. If it fails with `<pkg><specifier> not satisfied by version ...`, a dependency version drifted — re-check the manifest pin against the provided dep.
- [ ] **Step 5: Build the whole jupiter system (integration gate)**
Run: `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'`
Expected: builds to completion, exit 0. This confirms the module edits evaluate and the component is wired into HA's package.
- [ ] **Step 6: Commit**
```bash
git add modules/environments/home-assistant/default.nix
git commit -m "$(cat <<'EOF'
feat(home-assistant): add alexa_media_player custom component
Package the Alexa Media Player HACS component declaratively (v5.15.7),
so HA can drive the Echo devices (TTS/announce, media, sensors). Needs
dictor 0.1.12 (absent from nixpkgs) and an alexapy 1.29.22 -> 1.29.25
bump to satisfy the manifest's exact requirement pins; both build
against HA's interpreter. Config-flow based: add via the HA UI and sign
in with the Amazon account after rebuild.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EOF
)"
```
---
## Post-implementation (manual, by the user — not an automatable step)
After `sudo nixos-rebuild switch --flake '.#jupiter'`:
1. Settings → Devices & Services → Add Integration → **Alexa Media Player**.
2. Sign in with the Amazon account; complete any 2FA / app-password prompt in the UI flow.
3. Confirm media_player entities and sensors appear; test a TTS/announce service call to an Echo.
## Self-Review
- **Spec coverage:** goal (alexa_media_player packaging) → Task 1; alexapy bump → Step 1; dictor packaging → Step 1; component entry → Step 2; wrapt/packaging already-satisfied → included as deps in Step 2; build-time manifest check → Steps 4-5; runtime config-flow → Post-implementation. Deferred Alexa→HA scope: intentionally absent. No gaps.
- **Placeholder scan:** none — every step has concrete code/commands and the verified hashes.
- **Type/name consistency:** `haPython`, `dictor`, `alexapy` defined in Step 1 and referenced by those exact names in Step 2; domain `alexa_media` consistent throughout; hashes identical across plan, spec, and the verified build.
@@ -0,0 +1,145 @@
# Intel Quick Sync hardware transcoding for Jellyfin on jupiter
## Problem
Jellyfin streams stutter on jupiter whenever a client needs a transcode
(unsupported codec/container, bitrate cap, or a client that can't
direct-play). Transcoding currently runs entirely on CPU.
jupiter's Intel iGPU is already usable at the OS level:
- `hardware.graphics.enable = true` with `intel-media-driver` (the `iHD`
VAAPI driver) is configured in
`machines/jupiter/hardware-configuration.nix:20-27`.
- The commented-out `i915.force_probe = "9a49"` kernel param there
corresponds to a Quick-Sync-capable Intel UHD iGPU, confirming the
hardware supports it.
But `modules/environments/jellyfin/default.nix` never grants the
`jellyfin` systemd service access to `/dev/dri`, so Jellyfin has no path
to the GPU and silently falls back to software transcoding.
## Goal
Give the Jellyfin service access to the iGPU's VAAPI render node, so
Quick Sync can be enabled in Jellyfin's own dashboard and transcodes are
offloaded from the CPU.
## Non-goals
- Remote/external access or reverse-proxy tuning.
- General CPU/RAM headroom review of jupiter.
- A toggle option (`my.profiles.jellyfin.hardwareAcceleration.enable`) —
jupiter only has the one iGPU, so this is hardcoded on rather than
made configurable.
## Design
### NixOS change (declarative)
In `modules/environments/jellyfin/default.nix`, inside the existing
`config = lib.mkIf cfg.enable { ... }` block, grant the systemd service
supplementary access to the `video` and `render` groups (the groups that
own `/dev/dri/card*` and `/dev/dri/renderD*`):
```nix
systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [
"video"
"render"
];
```
This is additive to the existing `systemd.services.jellyfin.after = [
"network-online.target" ];` block already in the file — both apply to
the same service.
Also add `libva-utils` to `environment.systemPackages` (or scoped to
this module) so `vainfo` is available on jupiter to verify the driver
loads correctly.
### Manual step (not declarative)
Jellyfin stores its transcoding/hardware-acceleration choice in its own
internal `encoding.xml`, which the NixOS module does not expose as an
option. After deploying the Nix change, one-time manual configuration in
the Jellyfin dashboard is required:
1. **Dashboard → Playback**.
2. Hardware acceleration: **Intel QuickSync (QSV)**.
3. VA-API device: `/dev/dri/renderD128`.
4. Enable hardware decoding for the codecs your library actually uses
(H264 at minimum; HEVC/VP9 depending on iGPU generation).
5. If any HDR content exists in the library, enable tone-mapping — this
is one of the more CPU-expensive operations Quick Sync can offload.
## Verification
Build-time (from the Mac, no SSH needed):
```
nix eval '.#nixosConfigurations.jupiter.config.systemd.services.jellyfin.serviceConfig.SupplementaryGroups' \
--extra-experimental-features 'nix-command flakes'
```
Expect `[ "video" "render" ]`.
On jupiter after `sudo nixos-rebuild switch --flake '.#jupiter'`:
```
systemctl status jellyfin
journalctl -u jellyfin -n 50 --no-pager
vainfo
```
`vainfo` should list the `iHD` driver and print supported VAEntrypoints
(VLD decode / encode profiles for H264/HEVC).
Functional check: play a file on a client that forces transcoding (or
force it manually via Jellyfin's playback quality setting), then in
Jellyfin's dashboard **Activity/Now Playing** panel confirm the
transcode reason and check that CPU usage on jupiter (`htop`) stays low
during playback rather than pegging a core — Quick Sync offload should
show up as low CPU, some GPU (`intel_gpu_top`) activity instead.
## Open items
- Exact supported codec list depends on the iGPU generation (device ID
`9a49`) — confirm via `vainfo` output once run, and enable only the
hardware decode paths it actually reports.
## Post-deploy fix: two additional runtime packages required
After the initial deploy (Task 1's `SupplementaryGroups` grant) and
enabling QSV in the dashboard, HEVC HDR playback hung indefinitely
(Direct Play worked for some titles; titles that needed a real
transcode+tonemap never produced output). Root-caused via
`journalctl -u jellyfin` and the per-session ffmpeg transcode log
(`find / -xdev -iname '*ffmpeg-transcode*'`) — two separate runtimes
were missing beyond `intel-media-driver` (which only provides VAAPI):
1. **QSV session creation failed:** `Error creating a MFX session: -9`
/ `Error initializing an MFX session: -3` on
`-init_hw_device qsv=qs@va`. VAAPI and QSV are separate runtimes on
Linux — QSV needs the oneVPL/MFX GPU implementation. Fix: added
`pkgs.vpl-gpu-rt` ("oneAPI Video Processing Library Intel GPU
implementation"; note `onevpl-intel-gpu` is the old, renamed
attribute) to `hardware.graphics.extraPackages` in
`machines/jupiter/hardware-configuration.nix`.
2. **OpenCL device creation failed:** `Failed to get number of OpenCL
platforms: -1001` (`CL_PLATFORM_NOT_FOUND_KHR`) on
`-init_hw_device opencl=ocl@va`. The `tonemap_opencl` filter jellyfin
uses for HDR→SDR tone-mapping needs a working OpenCL ICD, which
nothing installed so far provides. Fix: added
`pkgs.intel-compute-runtime` ("Intel Graphics Compute Runtime oneAPI
Level Zero and OpenCL, supporting 12th Gen and newer" — matches
jupiter's Tiger Lake/Xe iGPU) to the same `extraPackages` list.
Confirmed working end-to-end: HEVC HDR transcode with QSV encode +
OpenCL tone-map runs at `speed=2.68x` realtime on jupiter's iGPU, and
plays smoothly on Apple TV (JellyTV app).
Both packages live in `machines/jupiter/hardware-configuration.nix`
(`hardware.graphics.extraPackages`), alongside `intel-media-driver`,
rather than in the jellyfin module itself — they're iGPU runtime
capabilities, not something specific to the jellyfin service.
@@ -0,0 +1,152 @@
# Amazon Alexa integration for Home Assistant (jupiter) — design
**Date:** 2026-07-29
**Machine:** jupiter
**Status:** approved, ready for implementation plan
## Goal
Let Home Assistant control and read the household Amazon Echo devices via the
unofficial [`alexa_media_player`](https://github.com/Alandtse/alexa_media_player)
integration: text-to-speech / announcements, media control, and per-device
sensors (last-called device, next alarm/timer, DND state, etc.).
This uses the user's Amazon account through an unofficial API. It is a
config-flow integration: after the rebuild it is added through the HA UI, not
via YAML.
## Non-goals (deferred)
Alexa → HA voice control ("Alexa, turn on the light") is **out of scope**. It
would need either `emulated_hue` bound to port 80 on the LAN, or a public HTTPS
endpoint plus the AWS-Lambda Smart Home Skill. The user chose to decide on that
later. Nothing in this change touches the firewall, port 80, systemd unit
capabilities, or network exposure.
## Approach
Package the integration declaratively, following the existing `ha-sourdough`
pattern in `modules/environments/home-assistant/default.nix` (a
`buildHomeAssistantComponent` entry in `services.home-assistant.customComponents`).
No HACS runtime.
Unlike sourdough, `alexa_media_player` has real Python dependencies. Its
`manifest.json` (v5.15.7) declares:
```
alexapy==1.29.25
packaging>=20.3
wrapt>=1.14.0
dictor>=0.1.12,<0.2
```
`buildHomeAssistantComponent` runs `manifestCheckPhase` at build time
(`check_manifest.py`): every requirement must resolve to an installed
distribution **whose version satisfies the specifier**, or the build fails.
Home Assistant repeats this check at runtime. Therefore each requirement must be
satisfied exactly — the exact `==1.29.25` pin in particular.
Dependency status in the pinned nixpkgs (`nixos-25.11`):
| Requirement | nixpkgs today | Action |
| ---------------------- | ------------- | --------------------------------------- |
| `alexapy==1.29.25` | 1.29.22 | **Bump** to 1.29.25 via override |
| `dictor>=0.1.12,<0.2` | *absent* | **Package** dictor 0.1.12 (new) |
| `wrapt>=1.14.0` | 1.17.2 | none — already satisfied |
| `packaging>=20.3` | 26.1 | none — already satisfied |
`authcaptureproxy` (in nixpkgs at 1.3.7) is a transitive dependency of
`alexapy`, not listed in the manifest, so it needs no direct handling.
## Components
All changes live in `modules/environments/home-assistant/default.nix` (plus the
hashes below). Three small pieces, wired together in the module's `let` block:
### 1. `dictor` package (new)
Not in nixpkgs. Pure-Python, no runtime dependencies (`requires_dist: null`),
ships a `setup.py`. A minimal `pkgs.python3Packages.buildPythonPackage`:
- pname `dictor`, version `0.1.12`
- `src = fetchPypi { pname = "dictor"; version = "0.1.12"; hash = "sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs="; }`
- setuptools format (legacy `setup.py`); `build-system = [ setuptools ]`
- `doCheck = false` (no meaningful test suite); `pythonImportsCheck = [ "dictor" ]`
Build against the HA Python set so the version lands in HA's venv — i.e. use
`config.services.home-assistant.package.python.pkgs` (the same interpreter the
component check and HA runtime use), not the top-level `pkgs.python3Packages`.
### 2. `alexapy` 1.29.25 (override)
nixpkgs `alexapy` is fetched from **GitLab** (`keatontaylor/alexapy`, tag
`v<version>`), not PyPI. Override just the version + src via
`overridePythonAttrs`, reusing the existing build-system and dependency list:
- `version = "1.29.25"`
- `src = fetchFromGitLab { owner = "keatontaylor"; repo = "alexapy"; tag = "v1.29.25"; hash = "sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4="; }`
This is a patch bump (1.29.22 → 1.29.25); the dependency set is expected to be
unchanged. Override the HA-Python-set `alexapy` so it shares the interpreter
with dictor and the component.
### 3. `alexa_media_player` component (new `customComponents` entry)
```
buildHomeAssistantComponent {
owner = "Alandtse";
domain = "alexa_media";
version = "5.15.7";
src = fetchFromGitHub {
owner = "Alandtse";
repo = "alexa_media_player";
rev = "v5.15.7";
hash = "sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI=";
};
dependencies = [ alexapy' dictor' wrapt packaging ]; # HA-python-set packages
}
```
`dependencies` must make every manifest requirement importable at the required
version for `manifestCheckPhase` to pass. Include all four (the two custom ones
plus `wrapt` and `packaging` from the HA Python set) to be explicit.
## Data flow
1. `nixos-rebuild switch` builds the component; `manifestCheckPhase` validates
the four requirements against the provided `dependencies`.
2. HA starts; the custom component is present but not configured.
3. User adds **Alexa Media Player** in Settings → Devices & Services, signs in
with the Amazon account (email/password; 2FA or app-password handled in the
UI flow at runtime).
4. HA creates media_player entities and per-device sensors; TTS/announce
services become available for automations.
## Error handling / risks
- **Version-pin drift.** If a future `alexa_media_player` bump changes the
`alexapy==` pin, `alexapy` must be re-bumped in lockstep, or the build fails
loudly at `manifestCheckPhase` (fail-safe, not silent).
- **Amazon login fragility.** The unofficial API can break on Amazon's side
(captcha/2FA changes). This is a runtime concern, independent of packaging;
not addressed here.
- **`dictor` upper bound `<0.2`.** 0.1.12 is the current release and satisfies
it. If nixpkgs later gains a `dictor` ≥ 0.2, prefer our pinned 0.1.12 for
this component.
## Verification
- `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'`
succeeds — this exercises the build-time manifest-requirements check for all
three new/overridden derivations.
- `nixfmt-rfc-style` clean on the edited module.
- Post-deploy (manual, by the user): the integration appears under Add
Integration, and a TTS/announce call reaches an Echo.
## Pinned artifacts
| Artifact | Source | Ref / version | Hash |
| --------------------------- | ---------- | ------------- | ------------------------------------------------- |
| alexa_media_player | GitHub | v5.15.7 | sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI= |
| alexapy | GitLab | v1.29.25 | sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4= |
| dictor | PyPI sdist | 0.1.12 | sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs= |
Generated
+14 -14
View File
@@ -89,11 +89,11 @@
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1784375420,
"narHash": "sha256-dt1zVj4MRpmffhsgQk+3tx1m/pFTXbtVWrn5KZ1y+8Q=",
"lastModified": 1784762557,
"narHash": "sha256-R/r6jRnANV50c8F5Fz5+1Q1moab0IGWRk+cg5ME2nMY=",
"owner": "NixOS",
"repo": "nix",
"rev": "bebd2f851a304e9fb2e143ce0cbeff577c6a37ac",
"rev": "d10c84cd0cc0efdcb29cf2611caf5fbcd10fa071",
"type": "github"
},
"original": {
@@ -143,11 +143,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1784310968,
"narHash": "sha256-rkSPTePrKqs4dg+i7ZFCq93+HrClac6oSwXX927SVjA=",
"lastModified": 1784723954,
"narHash": "sha256-1CfD8ZUjCkTgjsneLZ/lxCHhgDfqxxE7/GX0MmsgiqA=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "779c32a00155994c86cde8213a8dd4df139d4355",
"rev": "a017f5b72210026af5b3ac5949f08d94380a6fbd",
"type": "github"
},
"original": {
@@ -218,11 +218,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1784497964,
"narHash": "sha256-WhdsTtaih3DgTPP/PX023b36UNQyMzEoi6GkPyGx0y4=",
"rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163",
"lastModified": 1784796856,
"narHash": "sha256-vwxWgF+Gj276WznzGb1LxGsK/39HaQwgQXiU3EkC844=",
"rev": "e2587caef70cea85dd97d7daab492899902dbf5d",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1037713.241313f4e8e5/nixexprs.tar.xz"
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1040357.e2587caef70c/nixexprs.tar.xz"
},
"original": {
"id": "nixpkgs",
@@ -245,11 +245,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1784432872,
"narHash": "sha256-IX7j5TvSD7hYC9NZ8mgmq7pZ97mYyl24Qs4Tz4uyvO4=",
"rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870",
"lastModified": 1784707089,
"narHash": "sha256-DUedXhD2Rg8q4Xyd07Sb90eZGy4gg6W+Vl/WbLNwAZo=",
"rev": "b3fe9581c9061c749abef42b6d4ee7b7c05c33fa",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.5591.fd1462031fde/nixexprs.tar.xz"
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.5845.b3fe9581c906/nixexprs.tar.xz"
},
"original": {
"id": "nixpkgs",
@@ -35,6 +35,8 @@
#vaapiIntel # LIBVA_DRIVER_NAME=i965 (older but works better for Firefox/Chromium)
libva-vdpau-driver
libvdpau-va-gl
vpl-gpu-rt # oneVPL/MFX runtime, required for QSV (h264_qsv/hevc_qsv) session creation
intel-compute-runtime # OpenCL runtime, required for tonemap_opencl (HDR tone-mapping)
];
};
@@ -9,6 +9,39 @@
let
cfg = config.my.profiles.home-assistant;
hostName = config.networking.hostName;
# Python deps for the alexa_media_player custom component. Built against
# Home Assistant's own interpreter — the same set buildHomeAssistantComponent
# uses — so HA's build-time and runtime manifest-requirement checks pass.
haPython = pkgs.home-assistant.python3Packages;
# dictor is not in nixpkgs; alexa_media_player needs dictor>=0.1.12,<0.2.
# Pure-Python, no runtime deps, legacy setup.py.
dictor = haPython.buildPythonPackage {
pname = "dictor";
version = "0.1.12";
format = "setuptools";
src = haPython.fetchPypi {
pname = "dictor";
version = "0.1.12";
hash = "sha256-bbSDda4eU9ye2EToWzj04/v79qTmC+yjd1Fa0URTuRs=";
};
build-system = [ haPython.setuptools ];
doCheck = false;
pythonImportsCheck = [ "dictor" ];
};
# nixpkgs ships alexapy 1.29.22; the manifest pins ==1.29.25. Patch bump.
# nixpkgs fetches alexapy from GitLab (keatontaylor/alexapy), tag v<version>.
alexapy = haPython.alexapy.overridePythonAttrs (old: {
version = "1.29.25";
src = pkgs.fetchFromGitLab {
owner = "keatontaylor";
repo = "alexapy";
tag = "v1.29.25";
hash = "sha256-P/hvgqZVaBJF5dbmHrDjQMC+pwV3EEhKyFIS5KmhgD4=";
};
});
in
{
@@ -23,6 +56,42 @@ in
services.home-assistant = {
enable = true;
openFirewall = true;
# HACS-style custom components, packaged declaratively (no HACS runtime).
# Config-flow based: add via Settings > Devices & Services after rebuild.
customComponents = [
(pkgs.buildHomeAssistantComponent {
owner = "Matts-Baps";
domain = "sourdough";
version = "1.1.3";
src = pkgs.fetchFromGitHub {
owner = "Matts-Baps";
repo = "ha-sourdough";
rev = "v1.1.3";
hash = "sha256-Uoid/2f6GxZMuE5Keu2VjHPYuOxnYG8hsCD6BYcaTvM=";
};
})
(pkgs.buildHomeAssistantComponent {
owner = "Alandtse";
domain = "alexa_media";
version = "5.15.7";
src = pkgs.fetchFromGitHub {
owner = "Alandtse";
repo = "alexa_media_player";
rev = "v5.15.7";
hash = "sha256-1rcZVSX1xA1Lc4qSu39MOitVEciZFhoPQy2y5+PpoAI=";
};
# Every manifest requirement must be importable at a satisfying
# version or manifestCheckPhase fails the build.
dependencies = [
alexapy
dictor
haPython.wrapt
haPython.packaging
];
})
];
extraComponents = [
"matter"
"mobile_app"
@@ -22,6 +22,8 @@ in
openFirewall = true;
};
environment.systemPackages = [ pkgs.libva-utils ];
my.homepage.services = [
{
group = "Media";
@@ -34,6 +36,10 @@ in
systemd.services.jellyfin = {
after = [ "network-online.target" ];
serviceConfig.SupplementaryGroups = [
"video"
"render"
];
};
};
}
+1 -1
View File
@@ -24,4 +24,4 @@ in
numix-icon-theme
];
};
}
}