Local control of the Logitech Harmony Hub: library, CLI, and MCP server
Project description
harmonyhub-py
Local control of the Logitech Harmony Hub from Python — usable as a library, a CLI, and an MCP server.
The library talks to the hub on the local network only (port 8088 over WebSocket). No Logitech cloud round-trip is required after the one-time provisioning POST that the hub itself answers locally.
Installation
uv sync # uv-managed checkout (recommended; uses .venv.mac)
pip install -e . # or plain pip from the repo root
pip install -e .[dev] # plus ruff / mypy / pytest
Python ≥ 3.14 is required (declared in pyproject.toml; matches the pinned
interpreter in uv.lock and .python-version).
Quick start (library)
import asyncio
from harmonyhub import HarmonyHubClient
async def main() -> None:
async with HarmonyHubClient("192.168.178.50") as hub:
info = await hub.get_info()
print(f"Hub: {info.friendly_name} (remote-id: {info.remote_id})")
for activity in await hub.list_activities():
print(f" {activity.label} (id={activity.id})")
await hub.start_activity("Watch TV")
await hub.send_key("volume_up")
result = await hub.set_channel("101")
print(f"Channel via {result.method}")
asyncio.run(main())
Quick start (CLI)
export HARMONY_HUB_HOST=192.168.178.50
harmony info
harmony activities list
harmony activities start "Watch TV"
harmony key volume-up
harmony key mute
harmony key digit 5
harmony channel 101
harmony status
harmony doctor # end-to-end diagnostic
harmony discover # find Harmony Hubs
All read commands accept --json for machine-readable output. All logs go to
stderr, so stdout stays scriptable.
CLI command reference
Every command resolves the hub host from (in order) --host, the
HARMONY_HUB_HOST env var, or [hub].host in config.toml.
Top-level
| Command | Purpose |
|---|---|
harmony info [--json] |
Hub identity: remote ID, redacted email, firmware, friendly name, discovery server. |
harmony status [--json] |
Current activity, last channel (library-tracked), connection state. |
harmony power-off |
Run the PowerOff activity. |
harmony listen [--json] |
Stream spontaneous hub events to stdout until Ctrl+C. |
harmony send --device DEV --command CMD [--hold-ms N] [--json] |
Send a raw IR command name to a specific device. Bypasses logical-key routing and aliases. |
harmony doctor |
End-to-end diagnostic: host reachability → port 8088 → provisioning → WebSocket → config. |
harmony discover [--name N] [--id ID] [--timeout S] [--json] |
Find hubs on the LAN via SSDP M-SEARCH and a parallel /24 port scan. |
harmony channel <n> [--device DEV] [--json] |
Switch channels. digits_then_enter (default) or native change_channel per config. |
harmony activities list/current/start … |
List, query, or switch activities (list, current, start). |
harmony devices list/commands … |
List all devices and their IR commands (list, commands). |
harmony device power-on/off <device> |
Power a single device on or off (power-on, power-off). |
harmony key volume-up/down/mute/ok/back/digit … |
Send a logical key with automatic routing (volume, channel, ok, back, digit). |
harmony config pull/show/diff … |
Fetch, display, or diff the hub config (pull, show, diff). |
harmony sequence list/run … |
List and fire hub-defined macro sequences (list, run). |
Activities — harmony activities ...
| Subcommand | Purpose |
|---|---|
list [--json] |
All activities defined on the hub. |
current [--json] |
The active activity (or PowerOff). |
start <name-or-id> |
Switch to an activity. Triggers device power-ons via the hub. |
Devices — harmony devices ...
| Subcommand | Purpose |
|---|---|
list [--type KIND] [--json] |
All devices: ID, label, kind, manufacturer, command count. --type filters by kind. |
commands <device> [--group G] [--grouped] [--json] |
List commands; --grouped groups by control group, --group <name> restricts to one. |
Recognised --type values: television, avreceiver, speaker, stb, game,
appletv, other.
Single-device shortcuts — harmony device ...
| Subcommand | Purpose |
|---|---|
power-on <device> |
PowerOn (falls back to PowerToggle if configured). |
power-off <device> |
PowerOff. |
Logical keys — harmony key ...
Each subcommand accepts --device to override routing and --host for the
hub. Without --device, the key routes via [activity_routes.<label>] in
config.toml; if no route is configured the lib picks the single device
that owns the command, or fails with an ambiguity error.
| Subcommand | Default route field | Notes |
|---|---|---|
volume-up |
volume_device |
Honours [volume] repeat / hold_ms. |
volume-down |
volume_device |
Honours [volume] repeat / hold_ms. |
mute |
volume_device |
Single press; toggles AVR/TV mute. |
channel-up |
channel_device |
|
channel-down |
channel_device |
|
ok |
navigation_device |
Alias chain: OK → Enter → Select → DirectionSelect. |
back |
navigation_device |
Alias chain: Back → Return → Exit → PreviousMenu → DirectionBack. |
digit <0-9> |
number_device (falls back to channel_device) |
Sends Number0…Number9 (or 0…9, depending on hub config). |
harmony key digit 5 is the canonical form; harmony key 5 is not exposed
— digits go through the digit subcommand.
Channel control — harmony channel
harmony channel <number> [--device DEV] [--json]
Switch channels. In digits_then_enter mode, presses each digit then Enter;
in change_channel mode, fires the hub's native changeChannel command. The
target device is taken from [activity_routes].channel_device for the active
activity, or auto-resolved. Pass --device to override.
Hub-config helpers — harmony config ...
pull [--out PATH]— fetch the raw hub config and write it as JSON (stdout when--outomitted).show [--json]— parsed summary: version, locale, devices (kind, capabilities, control groups), activities (roles), sequences.diff [--json]— compare the on-disk cached config to a fresh pull; surfaces added/removed devices, activities, commands, andconfigVersionbumps.
Sequences — harmony sequence ...
Hub-defined macros (multi-step IR runs configured in the Harmony app):
| Subcommand | Purpose |
|---|---|
list [--json] |
List sequences with ID, label, and step count. |
run <sequence-id> |
Fire every step in order. Reports per-step success / failure rows. |
Shell completions (zsh)
Auto-install (modifies ~/.zshrc):
harmony --install-completion zsh
exec zsh # or open a new shell
Manual install (no edits to ~/.zshrc; uses $fpath):
mkdir -p ~/.zfunc
harmony --show-completion zsh > ~/.zfunc/_harmony
# add to ~/.zshrc once:
# fpath=(~/.zfunc $fpath)
# autoload -U compinit && compinit
Completion drives off the live binary, so it stays in sync as new subcommands are added.
Exit codes
| Code | Meaning |
|---|---|
0 |
success |
2 |
usage / validation error |
10 |
hub unavailable on the network |
11 |
protocol error (timeout, malformed payload) |
12 |
command or alias not found |
13 |
routing ambiguous — pass an explicit --device |
Configuration
The library reads ~/.config/harmony-local/config.toml (or %APPDATA% on
Windows). Every value can be overridden by an environment variable or a CLI
argument (precedence: CLI > env > file > default).
[hub]
host = "192.168.178.50"
[connection]
mode = "persistent" # persistent | ondemand
protocol = "websocket" # websocket | xmpp
keepalive_interval_s = 50
request_timeout_s = 10
[channel]
mode = "digits_then_enter" # digits_then_enter | change_channel
inter_digit_delay_ms = 150
send_enter = true
[volume]
# Fired per `harmony key volume-up` / `volume-down` (mute is always single).
repeat = 1 # IR presses per logical key (raise for AVRs with fine steps, e.g. 4 → 2 dB on Yamaha)
hold_ms = 0 # extra IR hold per press (some AVRs need ≥100 ms to register repeat-frames)
inter_press_delay_ms = 80 # gap between repeats when repeat > 1
[activity_routes."Watch TV"]
volume_device = "Denon AVR"
channel_device = "Vodafone Receiver"
navigation_device = "Vodafone Receiver"
number_device = "Vodafone Receiver"
Recognised environment variables: HARMONY_HUB_HOST, HARMONY_PROTOCOL,
HARMONY_CONNECTION_MODE.
MCP server
harmony-mcp runs the FastMCP server over stdio. Wire it into Claude Desktop:
{
"mcpServers": {
"harmony": {
"command": "harmony-mcp",
"args": [],
"env": { "HARMONY_HUB_HOST": "192.168.178.50" }
}
}
}
Tools exposed:
harmony_get_status— current activity, last channel, connection state.harmony_list_activities,harmony_start_activity,harmony_power_off.harmony_list_devices,harmony_list_device_commands.harmony_device_power_on,harmony_device_power_off.harmony_send_key— typedLogicalKey(volume/channel up-down, digits, ok, enter, back, off). Routing falls back to TOMLactivity_routes, then auto-resolution.harmony_send_command— raw IR command on a device (for vendor-specific buttons not covered bysend_key).harmony_set_channel—digits_then_enteror nativechange_channel.harmony_refresh_config— re-fetch and replace the cached config.
Resources exposed: harmony://config, harmony://activities,
harmony://devices, harmony://status.
The server never logs to stdout (that would corrupt the MCP framing) and does not return raw account IDs or unredacted email addresses.
Natural-language skill
A drop-in skill for Claude Code / Claude Desktop lives at
skill/harmonyhub/SKILL.md. It teaches the
model to translate voice-style requests ("schalte Pro7 ein", "lauter",
"alles aus") into the matching MCP tool calls or CLI invocations.
Highlights:
- Hard rule: channel requests are only valid when the active activity is
Fernsehen. The skill checksharmony_get_statusfirst and starts the activity if needed before callingharmony_set_channel. - Channel-name → number map (ARD=1, ZDF=2, RTL=3, Sat1=4, Pro7=5, …).
- Activity aliases for this hub:
Fernsehen,Apple TV sehen,Musik hören,TV,PowerOff. - Volume / mute / OK / Back / channel ±/digit routing tables.
- Replies in the user's language in one short sentence — no tool names, no JSON.
See docs/skill.md for installation notes.
What the library will not tell you
The hub is a one-way IR transmitter for most operations, so the library is honest about what it cannot know:
| Question | Verdict |
|---|---|
| Which activity is active? | Reliable — read directly from the hub. |
| What is the current channel? | Unknown to the hub. Tracked only when set via this library, with last_channel_source flagged. |
| Is a specific device powered on? | Inferred from the active activity; unverified. |
| Was the user pressing buttons on the original remote? | Invisible to the hub. |
If anything outside Harmony changed a device's state, the library will not
pretend otherwise. Run harmony status to inspect what is actually known.
Documentation
| File | Summary |
|---|---|
| docs/protocol.md | Raw WebSocket/HTTP payloads the library uses; reverse-engineered hub wire format. |
| docs/routing.md | How logical keys resolve to a target device — precedence rules and config examples. |
| docs/skill.md | Installation guide for the Claude Code natural-language agent skill. |
| docs/troubleshooting.md | Common errors (HTTP 401, timeout, provisioning failures) and fixes. |
Tests
pytest # unit + simulator tests, no real hub needed
HARMONY_HUB_HOST=... pytest -m integration # opt-in real-hub smoke test
Repository layout
See spec/harmony_hub_implementierungsplan(1).md for the full design plan.
Runtime code lives in harmonyhub/:
harmonyhub/
├── client.py # HarmonyHubClient — public API
├── models.py # Frozen dataclasses (Device.command_actions maps function.name → IR command)
├── exceptions.py # Error hierarchy
├── aliases.py # Logical-key → IR-command fallbacks
├── status.py # last_channel persistence
├── cache.py # XDG paths, JSON cache helpers
├── config.py # TOML loader + env overlay (Connection / Channel / Volume / ActivityRoute)
├── discovery.py # mDNS stub (post-MVP)
├── cli.py # Typer CLI (zsh completion via `--install-completion zsh`)
├── mcp_server.py # FastMCP server
├── simulator.py # Fake hub (WebSocket only — HTTP mocked in tests)
└── protocol/
├── http.py # Provisioning POST (exports HUB_ORIGIN constant)
├── websocket.py # WebSocket transport — request/response + fire-and-forget notify()
└── xmpp.py # Stub (post-MVP)
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file harmonyhub_py-0.2.0.tar.gz.
File metadata
- Download URL: harmonyhub_py-0.2.0.tar.gz
- Upload date:
- Size: 45.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad9a41cbbf5166b1d6c875731778fd403d56d1b3b47ed65ebe805a2f1567e565
|
|
| MD5 |
4de6ddff0217e7782afd50532fba6791
|
|
| BLAKE2b-256 |
885e50268d4b6e05e073c1de87da1911e27bc244466f4848085fbf013d23268c
|
File details
Details for the file harmonyhub_py-0.2.0-py3-none-any.whl.
File metadata
- Download URL: harmonyhub_py-0.2.0-py3-none-any.whl
- Upload date:
- Size: 43.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5217eb718d98b7204d6004569353e542fe1233b3818a93568e4b72a86592e317
|
|
| MD5 |
6b96269c7b70c2392b8c053c7c832fdd
|
|
| BLAKE2b-256 |
7bc8932aab826565fe9372e16724b4c88719cf9da127a7fa43ae3e14cd5ab2c8
|