Skip to content

Instantly share code, notes, and snippets.

@wizardofozzie
Created November 2, 2025 09:57
Show Gist options
  • Select an option

  • Save wizardofozzie/a5f07db5b0862ed3a3898ca4f613641f to your computer and use it in GitHub Desktop.

Select an option

Save wizardofozzie/a5f07db5b0862ed3a3898ca4f613641f to your computer and use it in GitHub Desktop.
decode or encode HomeKit uris
#!/usr/bin/env python3
# hkuri.py — encode/decode HomeKit "X-HM://" URIs
import argparse
import json
import re
import string
import sys
BASE36 = string.digits + string.ascii_uppercase
def b36encode(n: int) -> str:
"""Encode an integer to a Base36 string."""
if n == 0:
return "0"
s = []
while n:
n, r = divmod(n, 36)
s.append(BASE36[r])
return "".join(reversed(s))
def b36decode(s: str) -> int:
"""Decode a Base36 string to an integer."""
return int(s.upper(), 36)
def fmt_code(n: int) -> str:
"""Format an integer as a setup code (XXX-XX-XXX)."""
code8 = f"{n:08d}"
return f"{code8[:3]}-{code8[3:5]}-{code8[5:]}"
def parse_code(code: str) -> int:
"""Parse a setup code string into an integer."""
d = re.sub(r"\D", "", code)
if not d or len(d) > 8:
raise ValueError("Setup code must be ≤8 digits")
n = int(d)
if n > 0x7FFFFFF:
raise ValueError("Setup code exceeds 27-bit range")
return n
def validate_setup_id(sid: str) -> str:
"""Validate the setup ID format."""
sid = sid.strip().upper()
if not re.fullmatch(r"[A-Z0-9]{4}", sid):
raise ValueError("Setup ID must be 4 chars A–Z/0–9")
return sid
def encode_uri(setup_code: str, category: int, *, setup_id: str, version: int = 0, reserved: int = 0,
flags: int | None = None, wac=False, ble=False, ip=False, paired=False) -> str:
"""Encode fields into an X-HM URI."""
sc = parse_code(setup_code)
if not (0 <= version <= 7):
raise ValueError("Version must be between 0 and 7")
if not (0 <= reserved <= 15):
raise ValueError("Reserved must be between 0 and 15")
if not (0 <= category <= 255):
raise ValueError("Category must be between 0 and 255")
setup_id = validate_setup_id(setup_id)
if flags is None:
flags = (0
| (0b1000 if wac else 0)
| (0b0100 if ble else 0)
| (0b0010 if ip else 0)
| (0b0001 if paired else 0))
if not (0 <= flags <= 15):
raise ValueError("Flags must be between 0 and 15")
n = ((version & 0b111) << 43) \
| ((reserved & 0b1111) << 39) \
| ((category & 0xFF) << 31) \
| ((flags & 0b1111) << 27) \
| (sc & 0x7FFFFFF)
return f"X-HM://{b36encode(n)}{setup_id}"
def decode_uri(uri: str) -> dict:
"""Decode an X-HM URI into its components."""
m = re.match(r'^\s*X-HM://([0-9A-Z]+)\s*$', uri.strip().upper())
if not m:
raise ValueError("Not an X-HM URI")
payload = m.group(1)
if len(payload) < 5:
raise ValueError("Payload too short")
setup_id = payload[-4:]
n = b36decode(payload[:-4])
version = (n >> 43) & 0b111
reserved = (n >> 39) & 0b1111
category = (n >> 31) & 0xFF
flags = (n >> 27) & 0b1111
sc = n & 0x7FFFFFF
return {
"version": version,
"reserved": reserved,
"category": category,
"flags_int": flags,
"flags": {
"WAC": bool(flags & 0b1000),
"BLE": bool(flags & 0b0100),
"IP": bool(flags & 0b0010),
"PairedTag": bool(flags & 0b0001),
},
"setup_code_int": sc,
"setup_code": fmt_code(sc),
"setup_id": setup_id
}
def main():
"""Main function to handle command-line arguments."""
p = argparse.ArgumentParser(prog="hkuri", description="Encode/Decode HomeKit X-HM URIs")
sub = p.add_subparsers(dest="cmd", required=True)
pe = sub.add_parser("encode", help="Encode fields → X-HM URI")
pe.add_argument("--code", required=True, help="Setup code (e.g. 123-45-678)")
pe.add_argument("--category", required=True, type=int, help="Accessory category (0..255)")
pe.add_argument("--setup-id", required=True, help="4-char SetupID A–Z/0–9")
pe.add_argument("--version", type=int, default=0)
pe.add_argument("--reserved", type=int, default=0)
g = pe.add_mutually_exclusive_group()
g.add_argument("--flags", type=lambda x: int(x, 0), help="Flags int/0b/0x (WAC=8, BLE=4, IP=2, Paired=1)")
gb = pe.add_argument_group("flag bits (ignored if --flags provided)")
gb.add_argument("--wac", action="store_true")
gb.add_argument("--ble", action="store_true")
gb.add_argument("--ip", action="store_true")
gb.add_argument("--paired", action="store_true")
pd = sub.add_parser("decode", help="Decode X-HM URI → fields")
pd.add_argument("uri", nargs="?", help="X-HM://...")
pd.add_argument("--json", action="store_true", help="Output JSON")
pd.add_argument("--stdin", action="store_true", help="Read URIs from stdin (one per line)")
args = p.parse_args()
if args.cmd == "encode":
uri = encode_uri(
setup_code=args.code,
category=args.category,
setup_id=args.setup_id,
version=args.version,
reserved=args.reserved,
flags=args.flags,
wac=getattr(args, "wac", False),
ble=getattr(args, "ble", False),
ip=getattr(args, "ip", False),
paired=getattr(args, "paired", False),
)
print(uri)
return
if args.cmd == "decode":
def out(obj):
if args.json:
print(json.dumps(obj, separators=(",", ":")))
else:
print(f"version={obj['version']} reserved={obj['reserved']} category={obj['category']} flags={obj['flags_int']}")
print(f"setup_code={obj['setup_code']} setup_id={obj['setup_id']}")
if args.stdin:
for line in sys.stdin:
s = line.strip()
if not s:
continue
try:
out(decode_uri(s))
except Exception as e:
print(f"[error] {s}: {e}", file=sys.stderr)
else:
if not args.uri:
raise SystemExit("decode: provide URI or --stdin")
out(decode_uri(args.uri))
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment