Omarchy.SonyXM4
An Omarchy shell plugin: left earbud, right earbud, and charging-case battery for a Sony WF-1000XM4, in the bar and broken out in a popup.

Install
git clone https://github.com/SpoilHeap/Omarchy.SonyXM4.git ~/.config/omarchy/plugins/io.github.spoilheap.sonyxm4
omarchy plugin enable io.github.spoilheap.sonyxm4
omarchy restart shell
No further setup: if your WF-1000XM4 is already paired (bluetoothctl devices
shows it), the plugin finds it by name on its own. The deviceMac setting is
only needed as an override — see Settings below — if you have
more than one WF-1000XM4 paired at once, or renamed yours to something that
no longer contains "WF-1000XM4".
Dependencies
python3— stdlib only (socket,subprocess,json), no packages to install.bluetoothctl(bluez-utils) — already present on any Omarchy install; used read-only, to list paired devices and check connection state.- Your earbuds paired once, the normal way, before installing this plugin.
No root, no extra groups, no background service, no network access, and no
files written outside this plugin's own checkout — sony-xm4-status.py only
opens a Bluetooth socket to a device already paired to your user session.
What this does, and how
The WF-1000XM4 doesn't expose battery over the standard BlueZ Battery1
D-Bus interface, and Sony hasn't published the protocol. Both had to be
worked out by capturing what the earbuds actually send:
- Battery rides RFCOMM channel 23, found by resolving the generic Serial
Port Profile UUID (
0x1101) that this device advertises over SDP. No request has to be sent — a couple of seconds after the socket connects, the earbuds push an unsolicited status burst on their own. - That burst is a flat run of TLV records,
[0x03][field id][2-byte length][payload]back to back, no outer framing. The record with field id0x03is a 3-byte payload:[left%, right%, case%]. That's the one this plugin reads; the others in the burst (firmware/device info, a per-connection nonce) aren't understood and are ignored.
sony-xm4-status.py checks bluetoothctl info <mac> first, so it never
blocks trying to open a socket to earbuds that are off or out of range. If
they're connected, it opens channel 23, reads the burst, and prints the three
percentages as JSON; if anything goes wrong it still exits 0 with an
"ok": false error, so the widget always has something to render.
Settings are not supported. ANC, ambient sound, and Speak-to-Chat live
behind a different, separate command channel (RFCOMM 9, an escaped
0x3e…0x3c-framed protocol distinct from the plain TLV battery channel).
That channel was found and its framing decoded, but the actual command
opcodes for the XM4's settings were not — commands reconstructed from
publicly documented protocols for older Sony headphones (which use a
different, simpler framing entirely) produced only a generic ACK from the
XM4, with no audible or physical effect. Getting this working for real would
need a real byte-level capture of the Sony Sound Connect app talking to the
earbuds (e.g. a rooted-phone Bluetooth snoop log), which wasn't available.
This is a known gap, not an oversight — see the commit history / issues for
where this was left off if you'd like to pick it up.
Caveat on channel 23 itself: it was found empirically against one unit's
firmware. RFCOMM channel numbers are normally stable per firmware revision,
but if your earbuds report nothing, check bluetoothctl and, if you're
comfortable reversing it yourself, re-scan for the channel that pushes data
on connect (sdptool records <mac> or a channel sweep) and update the
BATTERY_CHANNEL constant in sony-xm4-status.py.
Uninstall
omarchy plugin remove io.github.spoilheap.sonyxm4
No restart needed — remove unloads the plugin from the running shell
immediately, deletes the checkout, and drops its entry from the bar layout
in ~/.config/omarchy/shell.json. This plugin keeps no other state, key, or
cache anywhere else, so there's nothing left to clean up afterward, and it
doesn't touch Bluetooth pairing — your earbuds stay paired to the system.
(omarchy plugin disable io.github.spoilheap.sonyxm4 instead just turns it
off without deleting anything, if you want it back later without a reinstall.)
Interactions
- Bar icon — left: panel · right: refresh without opening
- Panel —
rrefreshes, Tab moves to the neighboring bar panel, Esc closes - IPC —
omarchy-shell io.github.spoilheap.sonyxm4 <open|close|toggle|refresh|status>
$ omarchy-shell io.github.spoilheap.sonyxm4 status
L 100% · R 100% · Case 20%
Settings
In this widget's entry in ~/.config/omarchy/shell.json, or via
omarchy bar set io.github.spoilheap.sonyxm4 <key> <value>:
| Key | Default | What it does |
|---|---|---|
refreshIntervalSec |
30 |
How often the helper is run |
deviceMac |
"" |
Override MAC — leave empty to auto-detect by name (see Install) |
omarchy bar set io.github.spoilheap.sonyxm4 deviceMac 'AA:BB:CC:DD:EE:FF'
omarchy bar set io.github.spoilheap.sonyxm4 refreshIntervalSec 60 --json
Files
| File | What it is |
|---|---|
manifest.json |
plugin declaration and settings schema |
Panel.qml |
the bar button and the popup |
Service.qml |
runs the helper on a timer, holds the result |
Model.js |
formatting — tiers, tooltip and status text |
sony-xm4-status.py |
checks connection state, reads channel 23, prints one JSON object |
sony-xm4-status.py always exits 0 with JSON, so a disconnected device or a
socket error is a state the panel renders rather than an error it swallows.
It can be run by hand to see exactly what the widget sees:
python3 ~/.config/omarchy/plugins/io.github.spoilheap.sonyxm4/sony-xm4-status.py
# or, to override auto-detect:
python3 ~/.config/omarchy/plugins/io.github.spoilheap.sonyxm4/sony-xm4-status.py AA:BB:CC:DD:EE:FF
Editing any file here reloads the plugin. If a change does not take —
QML that is already loaded can hold on — force it with
omarchy restart shell.