Running Kodi on a Raspberry Pi is a solved problem if you only care about software. But if you are building a headless, 24/7 embedded media center, the real challenge is hardware integration and graceful power management. Yanking the power cord on a Pi corrupts the SD card and destroys the Kodi database. Relying solely on a TV remote via HDMI-CEC fails when the TV drops the CEC bus.
This guide walks through building a hardware-integrated Kodi on a Raspberry Pi 5, complete with a physical GPIO watchdog, a status LED tied to Kodi's JSON-RPC API, and a safe-shutdown button to protect your filesystem.
The 2026 Decision Matrix: Which Pi and OS for Kodi?
Before ordering parts, you must decide on the operating system. The embedded community usually defaults to LibreELEC, but that limits custom GPIO daemons due to its read-only squashfs filesystem. Here is the decision path to select your stack.
| Criteria | LibreELEC (Appliance OS) | Raspberry Pi OS (Bookworm) + Kodi |
|---|---|---|
| Setup Complexity | Low (Flash and boot) | Medium (Requires desktop environment config) |
| Custom GPIO Daemons | Difficult (Requires Docker/Entware) | Native (Full systemd & Python access) |
| 4K60 HDR Performance | Excellent (Optimized kernel) | Good (Requires fkms/kms tweaking) |
| Background Tasks | Restricted | Unrestricted (Plex, Pi-hole, MQTT) |
apt. This gives us native systemd control and unrestricted access to the gpiozero library for our watchdog script.
Parts List and Spec Sheet
Do not cheap out on the power supply or the SD card. 90% of Pi Kodi crashes are caused by voltage brownouts or failing flash memory.
- Compute: Raspberry Pi 5 (8GB variant) — $80. The 8GB model prevents OOM kills when scraping large local media libraries.
- Power: Official Raspberry Pi 27W USB-C PD Power Supply — $12. Mandatory to prevent peripheral brownouts.
- Storage: Samsung PRO Endurance 128GB microSD — $18. Standard EVO cards die within a year under Kodi's constant SQLite logging.
- Cooling: Official Pi 5 Active Cooler — $5. The Pi 5 will thermal throttle at 85°C during 4K HEVC decoding without it.
- GPIO Components: 5mm diffused LED, 220Ω through-hole resistor, 6x6mm tactile momentary switch, breadboard, and jumper wires.
Pin Mapping and Hardware Wiring
We are wiring two circuits: a status indicator and a hardware interrupt for safe shutdown. The code targets the BCM pin numbering scheme native to the Pi 5's RP1 I/O controller.
| Component | BCM Pin | Physical Pin | Wiring Notes |
|---|---|---|---|
| Status LED (Anode) | GPIO 17 | 11 | Wire through 220Ω resistor to limit current to ~15mA. |
| Status LED (Cathode) | GND | 9 | Connect to any ground pin. |
| Shutdown Button | GPIO 27 | 13 | Connect one leg to GPIO 27, the other to GND (Pin 14). Uses internal pull-up. |
The Code: GPIO Watchdog and JSON-RPC Health Check
This Python script runs as a systemd service. It monitors Kodi's local JSON-RPC API. If Kodi is running, the LED stays solid. If media is playing, it pulses. If Kodi crashes, it blinks. Pressing the button sends a quit command to Kodi, waits for the process to release the database lock, and then halts the Pi.
#!/usr/bin/env python3
import time
import signal
import sys
import os
import requests
from gpiozero import LED, Button
# --- PIN DEFINITIONS ---
STATUS_LED = LED(17)
SHUTDOWN_BTN = Button(27, pull_up=True, bounce_time=0.1)
# --- KODI JSON-RPC CONFIG ---
KODI_URL = "http://127.0.0.1:8080/jsonrpc"
HEADERS = {'content-type': 'application/json'}
TIMEOUT = 2
def check_kodi_status():
"""Queries Kodi JSON-RPC to determine if app is running and playing media."""
payload = {
"jsonrpc": "2.0",
"method": "Player.GetActivePlayers",
"id": 1
}
try:
response = requests.post(KODI_URL, json=payload, headers=HEADERS, timeout=TIMEOUT)
data = response.json()
if 'result' in data and len(data['result']) > 0:
return 'playing'
return 'idle'
except requests.exceptions.RequestException:
return 'offline'
def graceful_shutdown():
"""Sends quit command to Kodi, then halts the OS to prevent SD corruption."""
STATUS_LED.blink(0.2, 0.2)
payload = {"jsonrpc": "2.0", "method": "Application.Quit", "id": 1}
try:
requests.post(KODI_URL, json=payload, headers=HEADERS, timeout=TIMEOUT)
time.sleep(3) # Allow SQLite DB to flush and close
except Exception:
pass # If Kodi is already dead, proceed to OS shutdown
os.system("sudo shutdown -h now")
def signal_handler(sig, frame):
"""Handles systemd service stop signals gracefully."""
STATUS_LED.off()
sys.exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
SHUTDOWN_BTN.when_pressed = graceful_shutdown
while True:
state = check_kodi_status()
if state == 'playing':
STATUS_LED.blink(1, 1) # Breathing effect for active playback
elif state == 'idle':
STATUS_LED.on() # Solid on when Kodi is at the home screen
else:
STATUS_LED.blink(0.1, 0.1) # Fast blink when Kodi is offline/crashed
time.sleep(2)
Debugging: Connection Refused and Boot Failures
When integrating software APIs with hardware daemons, timing and permissions are your primary failure points. If your LED is stuck in the fast-blink "offline" state, check the console logs.
The Exact Error String
If you run the script manually and see this exact traceback:
requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8080): Max retries exceeded with url: /jsonrpc (Caused by NewConnectionError('Failed to establish a new connection: [Errno 111] Connection refused'))
Ranked Causes and Fixes
- Kodi Web Server is Disabled (Most Likely): Kodi does not enable JSON-RPC by default. Boot into Kodi, go to Settings > Services > Control, enable "Allow remote control via HTTP", and ensure the port is set to 8080 with no password (or update the script with basic auth).
- Race Condition on Boot: Your
systemdservice is launching before Kodi finishes initializing its web server. Fix this by addingAfter=graphical.targetand a 10-secondExecStartPre=/bin/sleep 10in your service file. - Kodi Bound to Wrong Interface: If Kodi is configured to listen only on the Ethernet MAC but you are querying localhost, it will refuse the connection. Set Kodi's web server to listen on
0.0.0.0or127.0.0.1.
The First Three Things to Check When It Fails
- Verify the API manually: SSH into the Pi and run
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"Application.GetProperties","params":{"properties":["volume"]},"id":1}' http://127.0.0.1:8080/jsonrpc. If this times out, the issue is in Kodi, not your Python code. - Check GPIO Pin Conflicts: Ensure no other service (like
lircor an I2C audio DAC overlay in/boot/firmware/config.txt) has claimed BCM 17 or 27. - Inspect User Permissions: If running via
systemd, ensure the service user is part of thegpioandvideogroups, otherwisegpiozerowill throw a permissions error on initialization.
Extending or Simplifying the Build
Not every project needs a custom daemon. Evaluate your actual requirements before over-engineering the hardware.
How to Simplify
If you only want safe shutdown and do not care about status LEDs or API polling, delete the Python script entirely. Instead, enable HDMI-CEC in Kodi. Most modern TVs will send a CEC standby signal when turned off, which Kodi can map to a graceful quit. Alternatively, wire the tactile button directly to the Pi's physical reset/run headers (if using a custom HAT) or use the standard gpio-shutdown device tree overlay in config.txt:
dtoverlay=gpio-shutdown,gpio_pin=27,active_low=1,gpio_pull=up
This handles the shutdown at the kernel level, requiring zero Python overhead.
How to Extend
If you want to eliminate the TV remote entirely, add an IR Receiver. Wire a TSOP38238 IR receiver to BCM 18 (Physical Pin 12). Install lirc and map the IR hex codes to Kodi's keymaps XML file. For advanced telemetry, swap the single 5mm LED for an SSD1306 I2C OLED display (SDA to BCM 2, SCL to BCM 3) and use the luma.oled library to render the currently playing track, CPU temperature, and network bitrate directly on the media center chassis.






