The Decision Path: Choosing Your Pi and OS for Kodi
Most guides treating kodi software for raspberry pi stop at flashing an SD card and plugging in an HDMI cable. But if you are building a headless, wall-mounted, or custom-enclosure media center with physical hardware controls, your OS choice dictates your hardware access. LibreELEC is a fantastic appliance OS, but its read-only squashfs filesystem makes running custom Python GPIO daemons a containerized nightmare.
Use this decision matrix to select your stack. We terminate on a single concrete pick for hardware hackers.
| Use Case | Board Variant | OS Selection | Verdict |
|---|---|---|---|
| Pure software, 4K60p HDR, no custom hardware | Pi 5 (8GB) | LibreELEC 12 | Best for appliance-style plug-and-play. |
| Low power, 1080p secondary room | Pi Zero 2 W | LibreELEC / OSMC | Good, but thermal throttles on HEVC. |
| Custom GPIO buttons, PWM fan control, Python daemons | Pi 5 (8GB) | Raspberry Pi OS Lite (Bookworm 64-bit) | Required for native systemd & pip access. |
kodi and kodi-standalone packages installed via apt. This gives you native systemd, unrestricted GPIO access, and the ability to run the Python daemon below without Docker overhead.
Hardware Spec Sheet & Pin Mapping
This build integrates three physical arcade-style buttons (Play/Pause, Stop, Safe Shutdown) and a 5V PWM cooling fan driven by a logic-level MOSFET. The Pi 5 operates at 3.3V logic, so we cannot drive a 5V fan PWM line directly from the GPIO header without risking the SoC.
Parts List
- Compute: Raspberry Pi 5 (8GB) - ~$80
- Storage: SanDisk Extreme 128GB microSD (A2 rating) - ~$18
- Power: Official 27W USB-C PD Power Supply - ~$12
- Switches: 3x Sanwa Denshi OBSF-30 arcade buttons (or generic 12mm momentary switches)
- Fan: Noctua NF-A8 5V PWM (or generic 4-pin 5V PWM fan)
- Switching: 2N7000 N-Channel Logic-Level MOSFET (for PWM fan control)
GPIO Pin Mapping Table
| Component | Function | BCM GPIO | Physical Pin | Notes |
|---|---|---|---|---|
| Button 1 | Play / Pause | 17 | 11 | Internal pull-up enabled in code |
| Button 2 | Stop Playback | 27 | 13 | Internal pull-up enabled in code |
| Button 3 | Safe Shutdown | 22 | 15 | Requires 3-second hold to trigger |
| MOSFET Gate | PWM Fan Control | 18 | 12 | Hardware PWM0, 25kHz frequency |
Wiring the Physical Control Interface
- Wire the Buttons: Connect one leg of each momentary button to a common Ground (GND) rail. Connect the other legs to GPIO 17, 27, and 22 respectively. Do not use external pull-up resistors; the
gpiozerolibrary will handle this via software. - Build the Fan Driver: Connect the 2N7000 MOSFET Source to GND. Connect the Drain to the fan's PWM wire (usually blue). Connect the Gate to GPIO 18 through a 1kΩ resistor (to prevent ringing).
- Fan Power: Connect the fan's VCC (red) to the Pi's 5V pin (Physical Pin 2 or 4) and the fan's GND (black) to the common GND rail. Note: Ensure your power supply can handle the fan's startup current; the official 27W Pi 5 PSU has ample headroom.
- Verify with Multimeter: Before powering the Pi, use your multimeter in continuity mode. Press each button to verify continuity to GND. Check for shorts between 5V and the MOSFET Gate.
Python Daemon: Kodi JSON-RPC & Thermal Control
This Python script targets the Raspberry Pi 5 running Bookworm. It uses gpiozero for hardware abstraction and requests to poll Kodi's JSON-RPC API. It runs as a background systemd service.
Prerequisite: In the Kodi GUI, navigate to Settings (gear icon) -> Services -> Control. Enable Allow remote control via HTTP. Set Port to 8080, Username to kodi, and leave the password blank for local-only access.
import requests
import time
import json
import os
import logging
import threading
from gpiozero import Button, PWMOutputDevice
from signal import pause
# --- PIN DEFINITIONS ---
PIN_PLAY_PAUSE = 17
PIN_STOP = 27
PIN_SHUTDOWN = 22
PIN_FAN_PWM = 18
# --- KODI CONFIG ---
KODI_IP = "localhost"
KODI_PORT = 8080
KODI_USER = "kodi"
KODI_PASS = ""
KODI_URL = f"http://{KODI_IP}:{KODI_PORT}/jsonrpc"
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Hardware setup
btn_play = Button(PIN_PLAY_PAUSE, pull_up=True, bounce_time=0.05)
btn_stop = Button(PIN_STOP, pull_up=True, bounce_time=0.05)
btn_shutdown = Button(PIN_SHUTDOWN, pull_up=True, hold_time=3)
fan = PWMOutputDevice(PIN_FAN_PWM, frequency=25000)
def send_kodi_rpc(method, params=None):
payload = {"jsonrpc": "2.0", "method": method, "id": 1}
if params:
payload["params"] = params
try:
response = requests.post(
KODI_URL,
data=json.dumps(payload),
headers={'Content-Type': 'application/json'},
auth=(KODI_USER, KODI_PASS),
timeout=3
)
return response.json()
except requests.exceptions.ConnectionError as e:
# Exact error string caught here for debugging
logging.error(f"ConnectionRefusedError: [Errno 111] Connection refused. Is Kodi running and JSON-RPC enabled? Details: {e}")
except Exception as e:
logging.error(f"RPC Call Failed: {e}")
def get_active_player_id():
rpc_resp = send_kodi_rpc("Player.GetActivePlayers")
if rpc_resp and "result" in rpc_resp and len(rpc_resp["result"]) > 0:
return rpc_resp["result"][0]["playerid"]
return None
def toggle_play_pause():
logging.info("Play/Pause pressed")
player_id = get_active_player_id()
if player_id is not None:
send_kodi_rpc("Player.PlayPause", {"playerid": player_id})
def stop_playback():
logging.info("Stop pressed")
player_id = get_active_player_id()
if player_id is not None:
send_kodi_rpc("Player.Stop", {"playerid": player_id})
def safe_shutdown():
logging.info("Shutdown held for 3s. Halting system.")
send_kodi_rpc("System.Shutdown")
time.sleep(2)
os.system("sudo shutdown -h now")
def thermal_fan_loop():
"""Reads Pi 5 SoC temp and adjusts PWM fan duty cycle."""
while True:
try:
with open("/sys/class/thermal/thermal_zone0/temp", "r") as f:
temp = int(f.read()) / 1000.0
if temp > 65.0:
fan.value = 1.0 # 100% duty
elif temp > 55.0:
fan.value = 0.5 # 50% duty
else:
fan.value = 0 # Off
except Exception:
fan.value = 1.0 # Failsafe: full speed on sensor error
time.sleep(5)
# Start thermal monitoring in background
threading.Thread(target=thermal_fan_loop, daemon=True).start()
# Button assignments
btn_play.when_pressed = toggle_play_pause
btn_stop.when_pressed = stop_playback
btn_shutdown.when_held = safe_shutdown
logging.info("Kodi GPIO Daemon started. Waiting for events...")
pause()
/home/kodi/kodi_gpio.py. Create a systemd service at /etc/systemd/system/kodi-gpio.service with After=kodi.service and User=root (required for GPIO access on Bookworm without complex udev rules). Enable it with sudo systemctl enable --now kodi-gpio.service.
Debugging: Connection Refused & GPIO Edge Failures
When integrating Python with Kodi's event loop, you will inevitably hit race conditions or permission walls. Here is the exact decision path for the two most common fatal errors.
Error 1: ConnectionRefusedError: [Errno 111] Connection refused
This string appears in your journalctl logs when the Python script attempts to POST to localhost:8080 but the OS rejects the TCP handshake.
- Check Kodi GUI Settings (Most Likely): You forgot to enable HTTP control. Go to Settings -> Services -> Control -> Allow remote control via HTTP. Toggle it off and on to force the listener to bind.
- Check the Systemd Race Condition: If this error only happens on boot, your Python script is starting before Kodi's web server initializes. Add
ExecStartPre=/bin/sleep 10to your systemd service file to delay the daemon. - Check Port Conflicts: Run
sudo ss -tulpn | grep 8080. If another service (like a rogue Docker container or OctoPrint) grabbed 8080, change Kodi's port inguisettings.xmland update the Python script.
Error 2: RuntimeError: Failed to add edge detection
This occurs when gpiozero attempts to attach an interrupt to a BCM pin, but the kernel blocks it.
- Check Pin Ownership: The Pi 5's new RP1 southbridge handles GPIO differently than the Pi 4. Ensure you are running the latest
python3-gpiozeroandlgpiobackend packages via apt. The legacyRPi.GPIOlibrary will fail on Pi 5. - Check for Ghost Processes: If a previous instance of your script crashed without releasing the pins, the kernel holds them. Run
sudo systemctl stop kodi-gpio, thensudo pkill -f kodi_gpio.pyto clear ghost processes. - Check config.txt Overlays: If you added
dtoverlay=gpio-fanto/boot/firmware/config.txtto let the firmware control the fan, GPIO 18 is claimed by the kernel. Remove the overlay if your Python script is managing the PWM.
First Three Things to Check When It Fails
If the physical buttons do nothing and the fan stays at 100%, run this triage sequence before rewriting code:
- Verify JSON-RPC Response: From the Pi terminal, run:
curl -d '{"jsonrpc":"2.0","method":"Player.GetActivePlayers","id":1}' -H 'Content-Type: application/json' http://localhost:8080/jsonrpc. If you don't get a JSON payload back, the issue is in Kodi, not your Python script. - Verify Pull-Up Logic: Use a multimeter to measure the voltage at the GPIO pin (e.g., Pin 11). It should read ~3.3V when the button is released, and drop to 0V when pressed. If it floats, your software pull-up isn't engaging.
- Check Systemd Status: Run
sudo systemctl status kodi-gpio.service. Look forActive: active (running). If it showsfailed, read the last 5 lines of the log output provided in the terminal.
Extending or Simplifying the Build
This architecture is modular. You can scale it up for a dedicated home theater or strip it down for a portable setup.
How to Simplify (The Pi Zero 2 W Route)
If you are building a small 1080p bedroom display, swap the Pi 5 for a Pi Zero 2 W. You can drop the MOSFET and PWM fan entirely (the Zero 2 W rarely needs active cooling for HEVC playback). Remove the thermal_fan_loop thread from the Python script, and map the buttons directly to gpiozero. Use a 10A USB power supply instead of the 27W PD brick.
How to Extend (Adding HDMI-CEC & Rotary Encoders)
For a premium living room build, integrate a rotary encoder (like the KY-040) on GPIO 5 and 6 to handle volume control. In the Python script, use the RotaryEncoder class from the gpiozero library to send Application.SetVolume RPC calls. Furthermore, enable HDMI-CEC in Kodi's input settings. This allows your TV's native remote to send power-state signals back to the Pi, allowing you to trigger the Python safe shutdown function automatically when the TV turns off, creating a seamless, single-remote experience.
By treating Kodi not just as an app, but as an API-driven headless service, you unlock the full potential of the Raspberry Pi as a true embedded media appliance.






