Skip to main content

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: OKEnterSelectDirectionSelect.
back navigation_device Alias chain: BackReturnExitPreviousMenuDirectionBack.
digit <0-9> number_device (falls back to channel_device) Sends Number0Number9 (or 09, 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 --out omitted).
  • 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, and configVersion bumps.

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 — typed LogicalKey (volume/channel up-down, digits, ok, enter, back, off). Routing falls back to TOML activity_routes, then auto-resolution.
  • harmony_send_command — raw IR command on a device (for vendor-specific buttons not covered by send_key).
  • harmony_set_channeldigits_then_enter or native change_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 checks harmony_get_status first and starts the activity if needed before calling harmony_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

harmonyhub_py-0.2.1.tar.gz (45.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

harmonyhub_py-0.2.1-py3-none-any.whl (43.7 kB view details)

Uploaded Python 3

File details

Details for the file harmonyhub_py-0.2.1.tar.gz.

File metadata

  • Download URL: harmonyhub_py-0.2.1.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

Hashes for harmonyhub_py-0.2.1.tar.gz
Algorithm Hash digest
SHA256 ad310a11a283ec636119d1504307de15714aff13e1b09ccc44a46959147d0208
MD5 685d7880f86e8e57aadea599923fc09b
BLAKE2b-256 4e51d5d57c1e268afcbc78726866f3431d454dc336b3db2cf675dccd80acd4b7

See more details on using hashes here.

File details

Details for the file harmonyhub_py-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: harmonyhub_py-0.2.1-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

Hashes for harmonyhub_py-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8023046696c184f3954d2dd41b7fccd26c2b52e6d49bf4c287e88dfabbb62b5e
MD5 6be3845fabdbad673d53976728e6f4f0
BLAKE2b-256 66b53a30e0665b80ef71e88f07cba35bf9634832edfc7f286d20c3165631c1bb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page