Python CLI and daemon to control Devialet Phantom volume over local HTTP API.
Use cases:
- scriptable local volume control
- Raspberry Pi bridge
- TV remote volume bridge over HDMI-CEC
- foundation for IR / keyboard / Home Assistant adapters
- mDNS discovery (
_whatsup._tcp.local) merged with UPnP discovery - volume commands:
getvol,setvol,volup,voldown,mute - target selection with
--system <name>(preferred for multi-device setups) - manual target override (
--ip,--port) - long-running daemon mode (
daemon --input cec) - keyboard test mode (
daemon --input keyboard) - typed config via TOML + env overrides
- packaged with
uv
- Python >= 3.10
uvinstalled: https://docs.astral.sh/uv/getting-started/installation/- same LAN as Devialet Phantom
- Devialet DOS with IP control enabled
For HDMI-CEC daemon mode:
- Linux CEC framework device available (typically
/dev/cec0) - CEC-capable adapter/device path (commonly Raspberry Pi HDMI or USB-CEC adapter)
git clone <repo>
cd dvlt-volume
uv syncList discovered speakers:
uv run devialetctl listRead current volume:
uv run devialetctl getvolRelative commands are precise:
volupincreases volume by+1voldowndecreases volume by-1
Set volume:
uv run devialetctl setvol 35Use explicit target:
uv run devialetctl --ip 192.168.1.42 --port 80 getvolUse system-name target selection (from tree output):
uv run devialetctl --system "TV" getvolRun daemon with config:
uv run devialetctl daemon --input cecOverride daemon CEC settings for one run (global options stay before subcommand):
uv run devialetctl --system "TV" daemon --input cec --cec-vendor-compat samsungThe daemon:
- consumes CEC key events from Linux CEC (
/dev/cec0, ioctl backend) - normalizes to volume actions
- answers
GIVE_AUDIO_STATUS(0x71) withREPORT_AUDIO_STATUS(0x7A) - answers System Audio/ARC requests (
0x70,0x7D,0xC3,0xC4) - answers
REQUEST_SHORT_AUDIO_DESCRIPTOR(0xA4) withREPORT_SHORT_AUDIO_DESCRIPTOR(0xA3) - by default, keeps standard CEC behavior (no vendor spoofing)
- optionally announces
DEVICE_VENDOR_ID(0x87) from Audio System when vendor compat requires spoofing - with
cec_vendor_compat = "samsung", handles Samsung vendor command0x89:0x95(SYNC_TV_VOLUME) -> replies50:89:95:01:XX- unknown/unsupported subcommands -> no response
- with
cec_vendor_compat = "samsung", consumes Samsung vendor command-with-id0xA0with no-response policy for unknown payloads - applies absolute volume from TV
SET_AUDIO_VOLUME_LEVEL(0x73) - sends updated
REPORT_AUDIO_STATUS(0x7A) after handled volume/mute events - applies dedupe/rate-limit policy
- retries with backoff if adapter/network is temporarily unavailable
Run daemon in a container (CEC mode):
docker run --rm -it \
--network host \
--device /dev/cec0:/dev/cec0 \
ghcr.io/clementperon/devialet-phantom-ctl:latest \
--system "TV" daemon --input cec --cec-vendor-compat="samsung"Notes:
--device /dev/cec0:/dev/cec0passes the Linux CEC device into the container.--network hostis required for mDNS discovery (_whatsup._tcp.local) from the container.--cec-vendor-compat samsungenables Samsung vendor compatibility mode for this run.- alternatively, set
DEVIALETCTL_CEC_VENDOR_COMPAT=samsungto make it the environment default. - with a fixed target IP (
DEVIALETCTL_IP), you can usually run without host networking.
Run daemon with keyboard input (no CEC hardware required):
uv run devialetctl daemon --input keyboardKeyboard commands:
u,+,up-> volume upd,-,down-> volume downm,mute-> toggle muteq,quit,exit-> stop daemon
In interactive terminal mode, single keys (u, d, m, q) work immediately without pressing Enter.
Default config path:
- Linux/RPi:
$XDG_CONFIG_HOME/devialetctl/config.tomlor~/.config/devialetctl/config.toml - macOS:
~/.config/devialetctl/config.toml
Example config.toml:
log_level = "INFO"
cec_device = "/dev/cec0"
cec_osd_name = "Devialet"
cec_vendor_compat = "none"
reconnect_delay_s = 2.0
dedupe_window_s = 0.08
min_interval_s = 0.12
[target]
ip = "192.168.1.42"
port = 80Use log_level = "DEBUG" (or DEVIALETCTL_LOG_LEVEL=DEBUG) to log raw HDMI-CEC frames:
CEC RX frame: ...for received CEC frames from/dev/cec0CEC TX: tx ...for transmitted frames- watcher-side
external audio-state changed; notified TV ...when Devialet-side state changes are pushed to TV
Environment overrides:
DEVIALETCTL_IPDEVIALETCTL_PORTDEVIALETCTL_BASE_PATHDEVIALETCTL_LOG_LEVELDEVIALETCTL_CEC_DEVICE
CLI target selection notes:
--ipand--systemare mutually exclusive.listandtreeare discovery-only commands and reject--ip/--system.
Kernel CEC permissions note:
- the daemon user must have read/write access to
/dev/cec0(typically viavideogroup or udev rule) - if startup fails with ioctl/device access errors, verify
ls -l /dev/cec*and group membership
Create /etc/systemd/system/devialetctl-cec.service:
[Unit]
Description=Devialet CEC volume bridge
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/dvlt-volume
ExecStart=/home/pi/.local/bin/uv run devialetctl daemon --input cec
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetEnable it:
sudo systemctl daemon-reload
sudo systemctl enable --now devialetctl-cec.service
sudo systemctl status devialetctl-cec.serviceCreate ~/Library/LaunchAgents/com.local.devialetctl.cec.plist and point it to:
- your repo directory
- your
uvbinary path - command
uv run devialetctl daemon --input cec
Then:
launchctl unload ~/Library/LaunchAgents/com.local.devialetctl.cec.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/com.local.devialetctl.cec.plist
launchctl list | rg devialetctlRun tests:
uv run pytestThe package is organized in layers:
devialetctl.domain: events and policydevialetctl.application: service, routing, daemon orchestrationdevialetctl.infrastructure: HTTP, mDNS, CEC adapter, configdevialetctl.interfaces: CLI wiring
Legacy imports remain available:
devialetctl.api.DevialetClientdevialetctl.discovery.discover