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 CaseBoard VariantOS SelectionVerdict
Pure software, 4K60p HDR, no custom hardwarePi 5 (8GB)LibreELEC 12Best for appliance-style plug-and-play.
Low power, 1080p secondary roomPi Zero 2 WLibreELEC / OSMCGood, but thermal throttles on HEVC.
Custom GPIO buttons, PWM fan control, Python daemonsPi 5 (8GB)Raspberry Pi OS Lite (Bookworm 64-bit)Required for native systemd & pip access.
The Concrete Pick: For a hardware-integrated media center in 2026, use the Raspberry Pi 5 (8GB) running Raspberry Pi OS Lite (64-bit, Bookworm) with the 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

ComponentFunctionBCM GPIOPhysical PinNotes
Button 1Play / Pause1711Internal pull-up enabled in code
Button 2Stop Playback2713Internal pull-up enabled in code
Button 3Safe Shutdown2215Requires 3-second hold to trigger
MOSFET GatePWM Fan Control1812Hardware PWM0, 25kHz frequency

Wiring the Physical Control Interface

  1. 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 gpiozero library will handle this via software.
  2. 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).
  3. 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.
  4. 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()
Systemd Integration: Save this as /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.

  1. 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.
  2. 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 10 to your systemd service file to delay the daemon.
  3. 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 in guisettings.xml and 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.

  1. Check Pin Ownership: The Pi 5's new RP1 southbridge handles GPIO differently than the Pi 4. Ensure you are running the latest python3-gpiozero and lgpio backend packages via apt. The legacy RPi.GPIO library will fail on Pi 5.
  2. 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, then sudo pkill -f kodi_gpio.py to clear ghost processes.
  3. Check config.txt Overlays: If you added dtoverlay=gpio-fan to /boot/firmware/config.txt to 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:

  1. 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.
  2. 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.
  3. Check Systemd Status: Run sudo systemctl status kodi-gpio.service. Look for Active: active (running). If it shows failed, 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.