Local control of Apple HomeKit devices via HAP: library, CLI, and MCP server
Project description
homekit-py
homekit-py talks directly to your accessories over the local network — no Apple cloud, no Apple ID, no internet required. HAP is cryptographically complex (SRP, Ed25519, Curve25519, ChaCha20-Poly1305, TLV8); this project delegates the wire protocol to aiohomekit and wraps it in a stable HomeKitBackend interface with a clean entity model.
Features
- Entity model — lights, switches, sensors, locks, thermostats, covers, fans mapped to stable
domain.slugIDs - Async Python library —
async with HomeKitClient(config) as client: ... - Rich CLI — human-readable tables or
--jsonfor scripts - MCP server — expose your accessories as tools to Claude or any MCP client
- On-disk state cache — fast repeated reads, configurable TTL
- Dangerous-operations policy —
lock.unlock,garage.open,security_system.disarmgated by policy and confirmation tokens - mDNS discovery — find all accessories on the LAN in seconds
- HAP event subscriptions — real-time characteristic change events via
homekit watch
Installation
pip install homekit-py
# or with uv
uv add homekit-py
Requires Python 3.14+. Pairing material is stored via the OS keychain (keyring) with an encrypted file fallback in ~/.config/homekit-local/pairings/.
Quick start
1. Discover accessories
homekit discover
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
┃ Name ┃ Device ID ┃ Model ┃ Category ┃ Host:Port ┃ State ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━┩
│ Living Room Light │ AA:BB:CC:DD:EE:FF │ Eve Light Strip │ Lighting │ 192.168.1.42:80 │ pairable│
└────────────────────┴───────────────────┴─────────────────┴──────────┴─────────────────┴────────┘
2. Pair the accessory
[!IMPORTANT] An accessory can only be paired with one controller at a time. If the device is already paired (with Apple Home, another
homekit-pyinstall, Home Assistant, etc.) thehomekit discoveroutput will showpairedandhomekit pairwill fail withAlreadyPairedError.Before pairing, remove the device from its current controller:
- Apple Home: open the Home app → tap the accessory → Remove Accessory (do not factory-reset unless instructed by the vendor).
- Other controller: run its
unpairequivalent.- Lost the keys: factory-reset the device per the vendor's instructions (typically a long button press).
After removal the device re-advertises with
sf=1(pairable) within a few seconds; re-runhomekit discoverto confirm.
Enter the 8-digit PIN from the accessory's label or display:
homekit pair AA:BB:CC:DD:EE:FF --pin 123-45-678 --alias "Living Room"
Pairing data is saved to ~/.config/homekit-local/pairings/. You only do this once.
3. Control
# List all entities
homekit entities
# Get current state
homekit get light.living_room
# Turn on / off
homekit on light.living_room
homekit off light.living_room
# Set brightness and colour temperature
homekit brightness light.living_room 60
homekit color-temp light.living_room 2700
# Set thermostat
homekit temperature climate.hallway 21.5
# Watch real-time events
homekit watch light.living_room
CLI reference
homekit [--verbose] [--no-daemon] <command>
| Command | Description |
|---|---|
homekit discover |
mDNS browse for advertised HomeKit accessories |
homekit pair DEVICE_ID --pin PIN |
Pair with an accessory (one-time) |
homekit unpair DEVICE_ID |
Remove a stored pairing |
homekit entities |
List all entities from paired accessories |
homekit entity ENTITY_ID |
Show capability and state for one entity |
homekit get ENTITY_ID |
Fetch current state |
homekit set ENTITY_ID EXPR |
Set state or attribute (on, brightness=70) |
homekit on ENTITY_ID |
Turn on |
homekit off ENTITY_ID |
Turn off |
homekit brightness ENTITY_ID VALUE |
Set brightness (0–100) |
homekit color-temp ENTITY_ID KELVIN |
Set colour temperature |
homekit temperature ENTITY_ID CELSIUS |
Set thermostat target |
homekit lock ENTITY_ID [--confirm TOKEN] |
Lock a lock entity |
homekit unlock ENTITY_ID [--confirm TOKEN] |
Unlock a lock entity |
homekit position ENTITY_ID PERCENT |
Set the target position of a cover/window/garage |
homekit identify DEVICE_ID |
Trigger the accessory identify action |
homekit accessories DEVICE_ID |
Dump accessory, service, and characteristic details |
homekit watch [ENTITY_ID ...] |
Stream real-time state changes for one, many, or all entities |
homekit pairings list |
List stored pairings |
homekit pairings export --out FILE |
Back up pairing store to JSON |
homekit pairings import FILE |
Restore pairings from a JSON backup |
homekit diagnose mdns |
Check mDNS / Bonjour health |
homekit diagnose network |
Check network reachability |
homekit diagnose pairability |
Check whether discovered accessories are pairable |
homekit diagnose storage |
Verify pairing-store integrity |
homekit diagnose mcp-security |
Check MCP exposure and write-policy safety |
homekit diagnose all |
Run every diagnostic and exit non-zero on failures |
homekit raw read DEVICE_ID AID IID |
Read a raw HAP characteristic |
homekit raw write DEVICE_ID AID IID VALUE |
Write a raw HAP characteristic |
homekit daemon status |
Show whether the background daemon is reachable |
homekit daemon start |
Start or connect to the background daemon |
homekit daemon stop |
Stop the background daemon |
homekit daemon restart |
Restart the background daemon |
homekit daemon logs [-n LINES] |
Tail the daemon log file |
Many read-style commands accept --json for machine-readable output. Place it after the command name:
homekit entities --json | jq '.[].entity_id'
Python library
import asyncio
from homekit import HomeKitClient, load_config
async def main():
async with HomeKitClient(load_config()) as client:
# List all entities
for entity in await client.list_entities():
print(entity.entity_id, entity.domain, entity.name)
# Read state
state = await client.get_state("light.living_room", refresh=True)
print(state.state, state.attributes)
# Control
await client.turn_on("light.living_room")
await client.set_brightness("light.living_room", 60.0)
await client.set_color_temperature("light.living_room", 2700)
await client.set_target_temperature("climate.hallway", 21.5)
asyncio.run(main())
MCP server
homekit-py ships with an MCP server that exposes your accessories as tools for Claude or any MCP-compatible client.
homekit-mcp # STDIO (default)
homekit-mcp --transport streamable-http --host 127.0.0.1 --port 8765
[!WARNING] The MCP server is read-only by default. Set
[mcp].allow_write_tools = truein~/.config/homekit-local/config.tomlto expose write tools.
Claude Desktop
{
"mcpServers": {
"homekit": {
"command": "homekit-mcp"
}
}
}
VS Code (agent mode)
{
"mcp": {
"servers": {
"homekit": {
"command": "homekit-mcp",
"type": "stdio"
}
}
}
}
Available MCP tools
Read (always available)
homekit_list_entities · homekit_get_state
Write (requires allow_write_tools = true)
homekit_set_light · homekit_set_switch · homekit_set_climate · homekit_set_cover · homekit_lock · homekit_unlock
Resources
homekit://entities · homekit://entities/{entity_id} · homekit://state/{entity_id} · homekit://capabilities/{entity_id} · homekit://events/recent
Configuration
Config file: ~/.config/homekit-local/config.toml
[controller]
name = "homekit-local"
[discovery]
mdns_timeout_s = 15.0
ip_only = false
[connection]
mode = "ondemand" # "ondemand" | "persistent"
request_timeout_s = 10.0
[cache]
ttl_seconds = 3600
[storage]
backend = "keyring" # "keyring" | "file"
[mcp]
allow_write_tools = false
allow_raw_characteristic_writes = false
audit_log = true
[daemon]
enabled = true
auto_spawn = true
idle_timeout_s = 600 # seconds; 0 = never shut down
[dangerous_operations]
"lock.unlock" = "confirmation_required"
"garage.open" = "disabled"
"security_system.disarm" = "disabled"
"cover.open" = "allow"
See docs/config.toml.example for a fully-annotated reference with every available key.
Environment variable overrides:
| Variable | Overrides |
|---|---|
HOMEKIT_CONFIG_DIR |
config directory path |
HOMEKIT_PAIRING_DIR |
pairing store directory |
HOMEKIT_CONNECTION__REQUEST_TIMEOUT_S |
connection.request_timeout_s |
HOMEKIT_MCP__ALLOW_WRITE_TOOLS |
mcp.allow_write_tools |
HOMEKIT_DAEMON__ENABLED |
daemon.enabled |
HOMEKIT_DAEMON__AUTO_SPAWN |
daemon.auto_spawn |
HOMEKIT_DAEMON__IDLE_TIMEOUT_S |
daemon.idle_timeout_s |
Dangerous operations policy
Certain operations are gated to prevent accidental or unauthorised control:
| Policy | Behaviour |
|---|---|
allow |
Executes immediately |
confirmation_required |
Requires a confirmation_token argument |
disabled |
Always rejected |
Default: lock.unlock → confirmation_required, garage.open and security_system.disarm → disabled.
Docs
- docs/pairing.md — pairing flow, key backup, recovery
- docs/protocol.md — HAP primer, AID/IID, characteristic types
- docs/entity-model.md — service→domain mapping,
entities.tomloverrides - docs/daemon.md — daemon mode, wire protocol, RPC methods
- docs/config.toml.example — fully-annotated config reference
- docs/troubleshooting.md — mDNS, VLAN, connection limits
Development
git clone https://github.com/jenreh/homekit-py
cd homekit-py
uv sync
task test # pytest with coverage
task lint # ruff + mypy
task format # ruff format
[!NOTE] A
FakeBackendsimulator (tests/fake_backend.py) is included for use in tests. It stubs theHomeKitBackendinterface without requiring real accessories or network access.
Project details
Release history Release notifications | RSS feed
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 homekit_py-0.4.5.tar.gz.
File metadata
- Download URL: homekit_py-0.4.5.tar.gz
- Upload date:
- Size: 56.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","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 |
46ede8fcdd7c73f7f61504de059a710e128e6938db2284576afcb4f4f0a1ab77
|
|
| MD5 |
3d31f5599fae62586bbdb6095669fc3d
|
|
| BLAKE2b-256 |
75cd1d4e2c3c3e33c6445d2264ed8cd29c56480806027e7a50c44d79e56d8dfd
|
File details
Details for the file homekit_py-0.4.5-py3-none-any.whl.
File metadata
- Download URL: homekit_py-0.4.5-py3-none-any.whl
- Upload date:
- Size: 59.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","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 |
fac2b9fae07618d0410f6235b832d85bfe21812034a809990aed33a1ef9e663b
|
|
| MD5 |
8b962c274cd16ce64b387eeec080aeeb
|
|
| BLAKE2b-256 |
480f8df88922df7118a6ff330ae4abdc74fedacc6165b131a2fe7ee47eacd172
|