Skip to content

Commit 5bbc3e9

Browse files
JarbasAlclaude
andcommitted
feat: audio + MPRIS control, bidirectional MQTT, Lovelace dashboard
- audio.py: detect PipeWire/PulseAudio/ALSA; read+control default sink/source volume and mute via pactl (pulse+pipewire) or amixer. Exposed as HA number (volume) + switch (mute) entities driven by MQTT command topics. - mpris.py: playerctl now-playing (status/title/artist) sensors + play-pause/ next/previous as HA button entities. - mqtt_client: bidirectional support — command registry, on_message dispatch, subscribe on (re)connect, register_command; number/switch/button discovery helpers. First HA->device path in the bridge. - USE_AUDIO / USE_MPRIS config; image ships pactl + playerctl. - dashboards/linux2mqtt.yaml: ready Lovelace dashboard (power/system/cpu-gpu/ rpi/audio views). docs/audio.md. Inactive (no entities) on hosts without an audio server / playerctl. 65 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5776652 commit 5bbc3e9

13 files changed

Lines changed: 674 additions & 2 deletions

File tree

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
66
git \
77
powerstat \
88
dmidecode \
9+
pulseaudio-utils \
10+
playerctl \
911
&& rm -rf /var/lib/apt/lists/*
1012

1113
WORKDIR /app

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ Under one Home Assistant device:
1919
- **System** — memory/swap, disk usage + free (per mount), uptime, load average,
2020
network + disk-IO throughput, fan speed, CPU cores, OS info, and a configurable
2121
process/service watch (`WATCH_PROCESSES`). See [docs/system.md](docs/system.md).
22+
- **Audio & media** — PipeWire/PulseAudio/ALSA status, **volume/mic control**
23+
(number/switch), and **MPRIS** now-playing + transport buttons. See
24+
[docs/audio.md](docs/audio.md).
25+
26+
A ready Lovelace dashboard is in [`dashboards/linux2mqtt.yaml`](dashboards/linux2mqtt.yaml).
2227

2328
## Run
2429

dashboards/linux2mqtt.yaml

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# linux2mqtt — Home Assistant Lovelace dashboard
2+
#
3+
# Entity ids follow `<domain>.<device_name_slug>_<key>`. This example uses the
4+
# slug `ha_pi4` (DEVICE_NAME "HA Pi4"). If your DEVICE_NAME differs, find/replace
5+
# `ha_pi4` with your slug. Cards referencing optional components (GPU, Pi, audio,
6+
# MPRIS) can be removed if you don't have them.
7+
#
8+
# Add via Settings → Dashboards → New dashboard → Edit → Raw configuration editor,
9+
# or drop under `views:` in an existing dashboard.
10+
11+
title: linux2mqtt
12+
views:
13+
- title: Power
14+
path: power
15+
icon: mdi:flash
16+
cards:
17+
- type: gauge
18+
name: Power
19+
entity: sensor.ha_pi4_power
20+
unit: W
21+
needle: true
22+
severity: {green: 0, yellow: 30, red: 80}
23+
- type: entities
24+
title: Power detail
25+
entities:
26+
- entity: sensor.ha_pi4_power
27+
- entity: sensor.ha_pi4_source
28+
- entity: sensor.ha_pi4_error_margin
29+
- entity: sensor.ha_pi4_power_floor
30+
- entity: sensor.ha_pi4_power_ceiling
31+
- entity: sensor.ha_pi4_energy
32+
- entity: sensor.ha_pi4_cost
33+
- entity: sensor.ha_pi4_model
34+
- type: history-graph
35+
title: Power & envelope (24h)
36+
hours_to_show: 24
37+
entities:
38+
- sensor.ha_pi4_power
39+
- sensor.ha_pi4_power_floor
40+
- sensor.ha_pi4_power_ceiling
41+
- type: statistics-graph
42+
title: Energy
43+
chart_type: bar
44+
period: hour
45+
stat_types: [state]
46+
entities: [sensor.ha_pi4_energy]
47+
48+
- title: System
49+
path: system
50+
icon: mdi:chip
51+
cards:
52+
- type: glance
53+
title: Host
54+
entities:
55+
- entity: sensor.ha_pi4_uptime
56+
- entity: sensor.ha_pi4_ip_address
57+
- entity: sensor.ha_pi4_cpu_cores
58+
- entity: sensor.ha_pi4_operating_system
59+
- entity: sensor.ha_pi4_architecture
60+
- type: horizontal-stack
61+
cards:
62+
- type: gauge
63+
name: CPU
64+
entity: sensor.ha_pi4_cpu_utilization
65+
unit: "%"
66+
severity: {green: 0, yellow: 60, red: 90}
67+
- type: gauge
68+
name: RAM
69+
entity: sensor.ha_pi4_ram_usage
70+
unit: "%"
71+
severity: {green: 0, yellow: 70, red: 90}
72+
- type: gauge
73+
name: Disk
74+
entity: sensor.ha_pi4_disk_host_usage
75+
unit: "%"
76+
severity: {green: 0, yellow: 75, red: 90}
77+
- type: entities
78+
title: Load & memory
79+
entities:
80+
- entity: sensor.ha_pi4_load_1m
81+
- entity: sensor.ha_pi4_load_5m
82+
- entity: sensor.ha_pi4_load_15m
83+
- entity: sensor.ha_pi4_ram_used
84+
- entity: sensor.ha_pi4_swap_usage
85+
- entity: sensor.ha_pi4_disk_host_free
86+
- type: history-graph
87+
title: Throughput (kB/s)
88+
hours_to_show: 6
89+
entities:
90+
- sensor.ha_pi4_network_up
91+
- sensor.ha_pi4_network_down
92+
- sensor.ha_pi4_disk_read
93+
- sensor.ha_pi4_disk_write
94+
- type: entities
95+
title: Services
96+
entities:
97+
- binary_sensor.ha_pi4_sshd_running
98+
- binary_sensor.ha_pi4_dockerd_running
99+
- binary_sensor.ha_pi4_mosquitto_running
100+
101+
- title: CPU / GPU
102+
path: cpu-gpu
103+
icon: mdi:cpu-64-bit
104+
cards:
105+
- type: entities
106+
title: CPU
107+
entities:
108+
- entity: sensor.ha_pi4_cpu_utilization
109+
- entity: sensor.ha_pi4_cpu_frequency
110+
- entity: sensor.ha_pi4_cpu_temperature
111+
- entity: sensor.ha_pi4_cpu_power
112+
- type: gauge
113+
name: CPU temperature
114+
entity: sensor.ha_pi4_cpu_temperature
115+
unit: °C
116+
severity: {green: 0, yellow: 70, red: 80}
117+
- type: entities
118+
title: GPU (if present)
119+
entities:
120+
- entity: sensor.ha_pi4_gpu_utilization
121+
- entity: sensor.ha_pi4_gpu_temperature
122+
- entity: sensor.ha_pi4_gpu_memory
123+
- entity: sensor.ha_pi4_gpu_power
124+
125+
- title: Raspberry Pi
126+
path: rpi
127+
icon: mdi:raspberry-pi
128+
cards:
129+
- type: entities
130+
title: Health
131+
entities:
132+
- entity: binary_sensor.ha_pi4_undervoltage
133+
- entity: binary_sensor.ha_pi4_throttled
134+
- entity: binary_sensor.ha_pi4_frequency_capped
135+
- entity: binary_sensor.ha_pi4_overheating
136+
- entity: binary_sensor.ha_pi4_overclocked
137+
- type: entities
138+
title: History (since boot)
139+
entities:
140+
- entity: binary_sensor.ha_pi4_undervoltage_occurred
141+
- entity: binary_sensor.ha_pi4_throttling_occurred
142+
- entity: binary_sensor.ha_pi4_overheating_occurred
143+
- type: entities
144+
title: Clocks & voltage
145+
entities:
146+
- entity: sensor.ha_pi4_arm_clock
147+
- entity: sensor.ha_pi4_configured_arm_freq
148+
- entity: sensor.ha_pi4_core_voltage
149+
- entity: sensor.ha_pi4_over_voltage
150+
151+
- title: Audio
152+
path: audio
153+
icon: mdi:volume-high
154+
cards:
155+
- type: entities
156+
title: Audio (if an audio server is running)
157+
entities:
158+
- entity: sensor.ha_pi4_audio_server
159+
- entity: number.ha_pi4_volume
160+
- entity: switch.ha_pi4_mute
161+
- entity: number.ha_pi4_mic_volume
162+
- entity: switch.ha_pi4_mic_mute
163+
- type: entities
164+
title: Now playing (MPRIS)
165+
entities:
166+
- entity: sensor.ha_pi4_media_status
167+
- entity: sensor.ha_pi4_media_title
168+
- entity: sensor.ha_pi4_media_artist
169+
- type: horizontal-stack
170+
cards:
171+
- type: button
172+
name: Previous
173+
icon: mdi:skip-previous
174+
tap_action: {action: toggle}
175+
entity: button.ha_pi4_media_previous
176+
- type: button
177+
name: Play/Pause
178+
icon: mdi:play-pause
179+
entity: button.ha_pi4_media_play_pause
180+
- type: button
181+
name: Next
182+
icon: mdi:skip-next
183+
entity: button.ha_pi4_media_next

docs/audio.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Audio & media
2+
3+
For audio-capable hosts (desktops, OVOS devices, media boxes) linux2mqtt exposes
4+
the audio stack and now-playing media to Home Assistant — including **control**,
5+
not just status. On a headless box with no audio server these entities simply
6+
don't appear.
7+
8+
## Audio (`USE_AUDIO`)
9+
10+
Detects the running server (**PipeWire / PulseAudio / ALSA**) and the default
11+
sink/source:
12+
13+
| Entity | Type | |
14+
| --- | --- | --- |
15+
| Audio Server | sensor | `pipewire` / `pulseaudio` / `alsa` (diagnostic) |
16+
| Volume | **number** (slider, %) | set output volume |
17+
| Mute | **switch** | mute/unmute output |
18+
| Mic Volume | **number** | set microphone volume (if a source exists) |
19+
| Mic Mute | **switch** | mute/unmute microphone |
20+
21+
Backends: `pactl` (PulseAudio **and** PipeWire via pipewire-pulse) preferred,
22+
`amixer` (ALSA) fallback. The number/switch entities use MQTT command topics
23+
(`<prefix>/audio/set/...`) — moving the slider in HA runs `pactl`/`amixer` on the
24+
host and republishes the new state.
25+
26+
## Media — MPRIS (`USE_MPRIS`)
27+
28+
Via `playerctl`, the active player's now-playing and transport:
29+
30+
| Entity | Type | |
31+
| --- | --- | --- |
32+
| Media Status / Title / Artist | sensor | Playing/Paused + metadata |
33+
| Media Play/Pause, Next, Previous | **button** | transport control |
34+
35+
HA does not support a full MQTT `media_player`, so this is sensors + buttons —
36+
enough to see what's playing and drive transport from a dashboard.
37+
38+
## In a container
39+
40+
Audio control needs the host's audio socket. For PipeWire/PulseAudio, share the
41+
user runtime dir and set the server, e.g.:
42+
43+
```
44+
--user 1000 -e XDG_RUNTIME_DIR=/run/user/1000 \
45+
-v /run/user/1000/pulse:/run/user/1000/pulse
46+
```
47+
48+
and ensure `pactl`/`playerctl` are available (the image ships them). Running
49+
linux2mqtt directly on the host (not containerised) is simplest for audio.
50+
51+
## Configuration
52+
53+
| Variable | Default | Meaning |
54+
| --- | --- | --- |
55+
| `USE_AUDIO` | `true` | audio server status + volume/mute control |
56+
| `USE_MPRIS` | `true` | MPRIS now-playing + transport buttons |

docs/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ library; this project adds the telemetry and the MQTT/HA bridge.
1212
temperature, frequency/VRAM, validated power).
1313
- **[System telemetry](system.md)** — memory, disk, uptime, load, network,
1414
disk-IO, fan, and a process/service watch.
15+
- **[Audio & media](audio.md)** — PipeWire/PulseAudio/ALSA status, volume/mic
16+
control, and MPRIS now-playing + transport (MQTT command topics).
17+
- **[Dashboard](../dashboards/linux2mqtt.yaml)** — a ready Lovelace dashboard.
1518
- **[Raspberry Pi](raspberry-pi.md)** — Pi 5 PMIC power, INA219 HATs, and the
1619
throttling / overheating / overclocking sensors; `vcgencmd` in a container.
1720
- **[Configuration](configuration.md)** — every environment variable.

linux2mqtt/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- GPU — utilization, temperature, VRAM, and validated power (NVIDIA)
99
- Raspberry Pi — throttling, overheating, overclocking, undervoltage
1010
- system — memory, disk, uptime, load, network, disk-IO, fan, process watch
11+
- audio — PipeWire/PulseAudio/ALSA status, volume/mic control, MPRIS media
1112
- energy (kWh) and cost
1213
1314
Run ``python -m linux2mqtt`` (or the ``linux2mqtt`` console script) with MQTT

linux2mqtt/__main__.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,53 @@ def main() -> None:
119119
LOG.info("System telemetry: disks=%s, watching=%s, fan=%s",
120120
[p for _, p in sysmon.disks], sysmon.watch or "none", sysmon.has_fan)
121121

122+
audio = None
123+
if Config.USE_AUDIO:
124+
from .audio import AudioMonitor
125+
a = AudioMonitor()
126+
if a.available:
127+
audio = a
128+
LOG.info("Audio: %s server detected", a.server)
129+
130+
mpris = None
131+
if Config.USE_MPRIS:
132+
from .mpris import MPRISMonitor
133+
m = MPRISMonitor()
134+
if m.available:
135+
mpris = m
136+
LOG.info("MPRIS: playerctl available")
137+
138+
audio_has_mic = bool(audio and "mic_volume" in audio.read())
122139
mqtt_client = MQTTClient(has_battery=monitor.has_battery,
123140
has_gpu=gpu is not None, has_gpu_power=gpu_power_ok,
124141
has_cpu=cpu is not None, has_cpu_power=cpu_power_ok,
125142
has_rpi=has_rpi,
126143
has_system=sysmon is not None,
127144
disks=sysmon.disks if sysmon else [],
128145
watch_processes=sysmon.watch if sysmon else [],
129-
has_fan=sysmon.has_fan if sysmon else False)
146+
has_fan=sysmon.has_fan if sysmon else False,
147+
has_audio=audio is not None, has_mic=audio_has_mic,
148+
has_mpris=mpris is not None)
149+
150+
if audio is not None:
151+
cmd = f"{Config.MQTT_TOPIC_PREFIX}/audio/set"
152+
mqtt_client.register_command(f"{cmd}/volume",
153+
lambda p: (audio.set_volume(int(float(p))),
154+
mqtt_client.publish_audio(audio.read())))
155+
mqtt_client.register_command(f"{cmd}/mute",
156+
lambda p: (audio.set_mute(p == "ON"),
157+
mqtt_client.publish_audio(audio.read())))
158+
mqtt_client.register_command(f"{cmd}/mic_volume",
159+
lambda p: (audio.set_mic_volume(int(float(p))),
160+
mqtt_client.publish_audio(audio.read())))
161+
mqtt_client.register_command(f"{cmd}/mic_mute",
162+
lambda p: (audio.set_mic_mute(p == "ON"),
163+
mqtt_client.publish_audio(audio.read())))
164+
if mpris is not None:
165+
mqtt_client.register_command(f"{Config.MQTT_TOPIC_PREFIX}/media/set",
166+
lambda p: (mpris.command(p),
167+
mqtt_client.publish_media(mpris.read())))
168+
130169
mqtt_client.connect()
131170
mqtt_client.publish_model(monitor.model)
132171
if sysmon:
@@ -151,6 +190,10 @@ def on_reading(reading: Reading) -> None:
151190
if sysmon is not None:
152191
mqtt_client.publish_system(sysmon.read())
153192
mqtt_client.publish_procs(sysmon.processes())
193+
if audio is not None:
194+
mqtt_client.publish_audio(audio.read())
195+
if mpris is not None:
196+
mqtt_client.publish_media(mpris.read())
154197
if dataset_fh and reading.measured:
155198
from powerguess.model import current_features, device_arch
156199
dataset_fh.write(json.dumps({

0 commit comments

Comments
 (0)