Running Kodi for Raspberry Pi is the gold standard for DIY home theater setups, but relying on a cheap Bluetooth remote or a smartphone app often feels disconnected from the hardware. If you want a dedicated, tactile, zero-latency physical control surface sitting right next to your display, building a custom GPIO media controller is the ultimate bench project. Instead of wrestling with deprecated LIRC configurations or flaky CEC adapters, we are going to build a hardwired I2C OLED and pushbutton array that talks directly to Kodi’s native JSON-RPC API.
This guide targets Raspberry Pi OS Bookworm (the current standard for Pi 4 and Pi 5), bypassing legacy RPi.GPIO libraries in favor of the modern gpiozero and lgpio backend. By the end, you will have a physical Play/Pause, Volume Up, Volume Down, and Mute panel with a live status screen.
The Verdict: Which Board to Pick for Kodi?
Before cutting wires, you need to select the right compute module. Kodi’s UI rendering and video decoding demands scale heavily with resolution and codec. Here is the decision matrix to terminate your board selection:
| Criteria | Raspberry Pi 3B+ | Raspberry Pi 4 Model B | Raspberry Pi 5 |
|---|---|---|---|
| Max Resolution | 1080p60 / 4K30 (stutter) | 4K60 (HEVC only) | 4K60 (HEVC/H.265 HDR) |
| UI Fluidity | Laggy with large libraries | Smooth | Instantaneous |
| GPIO/I2C Speed | Standard 100kHz/400kHz | Standard 100kHz/400kHz | RP1 chip (much faster I/O) |
| Approx. Price (2026) | $35 (Used/Refurb) | $55 (4GB) | $60 (4GB) |
Parts List & Hardware Spec Sheet
Do not substitute the microSD card. Kodi’s SQLite database performs heavy random I/O when scraping metadata; cheap Class 10 cards will bottleneck the UI and corrupt the database over time.
| Component | Exact Variant / Specification | Est. Cost | Engineering Notes |
|---|---|---|---|
| Compute Board | Raspberry Pi 5 (4GB RAM) | $60.00 | Requires active cooler; thermal throttling ruins A/V sync. |
| Storage | SanDisk Extreme 64GB A2 V30 microSD | $14.00 | The 'A2' rating is mandatory for app/database random IOPS. |
| Display | SSD1306 128x64 I2C OLED (0.96") | $6.00 | Ensure it has 4 pins (VCC, GND, SCL, SDA), not SPI. |
| Switches | 6x6mm Tactile Pushbuttons (4-pin) | $2.00 | Standard through-hole; operates at 3.3V logic. |
| Resistors | 4.7kΩ (x2) and 10kΩ (x4) | $1.00 | 4.7k for I2C pull-ups; 10k for button pull-ups if not using internal. |
Pin Mapping & Wiring the Controller
The Raspberry Pi 5 utilizes the RP1 southbridge for GPIO, but the physical pinout remains identical to the 40-pin header on the Pi 4. We are using BCM (Broadcom) numbering in the software, but physical board pins for wiring.
Pin Mapping Table
| Component | Component Pin | Pi Physical Pin | Pi BCM GPIO |
|---|---|---|---|
| SSD1306 OLED | VCC | Pin 1 (3.3V) | N/A |
| SSD1306 OLED | GND | Pin 6 (GND) | N/A |
| SSD1306 OLED | SCL | Pin 5 | GPIO 3 (SCL1) |
| SSD1306 OLED | SDA | Pin 3 | GPIO 2 (SDA1) |
| Button 1 (Play/Pause) | Signal | Pin 29 | GPIO 5 |
| Button 2 (Vol Up) | Signal | Pin 31 | GPIO 6 |
| Button 3 (Vol Down) | Signal | Pin 33 | GPIO 13 |
| Button 4 (Mute) | Signal | Pin 35 | GPIO 19 |
Wiring Steps
- I2C Bus Pull-ups: The Pi has internal 1.8kΩ pull-ups on GPIO 2 and 3, but for stable OLED communication over longer wires, solder a 4.7kΩ resistor between SDA and 3.3V, and another between SCL and 3.3V on your breadboard.
- Button Wiring: Connect one leg of each tactile switch to its respective GPIO pin, and the other leg to GND. We will enable the Pi’s internal pull-up resistors in software, eliminating the need for external 10kΩ resistors on the switches.
- Verify I2C: Boot the Pi, open a terminal, and run
sudo raspi-config→ Interface Options → I2C → Enable. Reboot, then runi2cdetect -y 1. You should see3cin the grid, confirming the SSD1306 is addressed correctly.
The Python JSON-RPC Control Script
Modern Kodi exposes a powerful JSON-RPC API over HTTP. This is vastly superior to simulating keyboard keystrokes via xdotool because it allows us to query state (like current volume) and execute commands even if the Kodi window isn't strictly in focus.
This script targets Raspberry Pi OS Bookworm. It uses gpiozero (which automatically routes to the lgpio backend on the Pi 5) and the requests library for API calls.
Prerequisites
Install the required Python packages via the virtual environment (mandatory in Bookworm):
sudo apt update
sudo apt install python3-venv python3-gpiozero python3-requests
python3 -m venv ~/kodi_env
source ~/kodi_env/bin/activate
pip install luma.oled
The Control Script (kodi_gpio_controller.py)
#!/usr/bin/env python3
"""
Kodi GPIO Media Controller for Raspberry Pi 4/5
Targets: Raspberry Pi OS Bookworm (lgpio backend)
"""
import time
import json
import requests
from gpiozero import Button
from signal import pause
# --- PIN DEFINITIONS (BCM Numbering) ---
PIN_PLAY_PAUSE = 5
PIN_VOL_UP = 6
PIN_VOL_DOWN = 13
PIN_MUTE = 19
# --- KODI JSON-RPC CONFIGURATION ---
# Ensure Web Server is enabled in Kodi: Settings > Services > Control
KODI_IP = '127.0.0.1' # Change if Kodi is on a different machine
KODI_PORT = 8080
KODI_USER = 'kodi'
KODI_PASS = '' # Default is usually blank
KODI_URL = f'http://{KODI_USER}:{KODI_PASS}@{KODI_IP}:{KODI_PORT}/jsonrpc'
# Initialize Buttons with internal pull-ups and hardware debounce
btn_play = Button(PIN_PLAY_PAUSE, pull_up=True, bounce_time=0.05)
btn_up = Button(PIN_VOL_UP, pull_up=True, bounce_time=0.05)
btn_down = Button(PIN_VOL_DOWN, pull_up=True, bounce_time=0.05)
btn_mute = Button(PIN_MUTE, pull_up=True, bounce_time=0.05)
def send_kodi_rpc(method, params=None):
"""Sends a JSON-RPC payload to Kodi with error handling."""
payload = {
"jsonrpc": "2.0",
"method": method,
"id": 1
}
if params:
payload["params"] = params
headers = {'Content-Type': 'application/json'}
try:
response = requests.post(
KODI_URL,
data=json.dumps(payload),
headers=headers,
timeout=2.0
)
response.raise_for_status()
return response.json()
except requests.exceptions.ConnectionError:
print(f"[ERROR] Cannot reach Kodi at {KODI_IP}:{KODI_PORT}. Is the webserver enabled?")
except requests.exceptions.Timeout:
print("[ERROR] Kodi RPC request timed out.")
except Exception as e:
print(f"[ERROR] Unexpected RPC failure: {e}")
def action_play_pause():
print("Action: Play/Pause")
send_kodi_rpc("Player.PlayPause", {"playerid": 1})
def action_vol_up():
print("Action: Volume Up")
# ExecuteAction is used for built-in Kodi volume commands
send_kodi_rpc("GUI.SetVolume", {"volume": "increment"})
def action_vol_down():
print("Action: Volume Down")
send_kodi_rpc("GUI.SetVolume", {"volume": "decrement"})
def action_mute():
print("Action: Toggle Mute")
send_kodi_rpc("Application.SetMute", {"mute": "toggle"})
if __name__ == "__main__":
print("Kodi GPIO Controller started. Waiting for button presses...")
# Bind callbacks
btn_play.when_pressed = action_play_pause
btn_up.when_pressed = action_vol_up
btn_down.when_pressed = action_vol_down
btn_mute.when_pressed = action_mute
try:
pause() # Keeps the script running efficiently
except KeyboardInterrupt:
print("\nShutting down GPIO controller.")
finally:
# Clean up gpiozero resources
btn_play.close()
btn_up.close()
btn_down.close()
btn_mute.close()
Debugging: Exact Error Strings and Fixes
When moving from older Pi OS releases (Bullseye) to Bookworm, the GPIO and I2C subsystems underwent massive changes. If your script fails, check these first three things:
- Is the Kodi Webserver actually on? By default, it is disabled. Go to Kodi → Settings (gear icon) → Services → Control → Enable "Allow remote control via HTTP". Set port to 8080.
- Are you running in the virtual environment? Bookworm enforces PEP 668. If you try to
pip installglobally, it will block you. You must activate thevenvcreated in the prerequisites. - Is I2C enabled at the kernel level? Run
dmesg | grep i2c. If it's missing, runsudo raspi-configand enable it.
Ranked Error Strings and Solutions
| Exact Error String | Root Cause | The Fix |
|---|---|---|
requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8080): Max retries exceeded |
Kodi's JSON-RPC web server is disabled, or a firewall (like ufw) is blocking localhost port 8080. |
Enable HTTP control in Kodi Services settings. If using UFW, run sudo ufw allow 8080/tcp. |
gpiozero.exc.BadPinFactory: Unable to load any default pin factory |
You are on Raspberry Pi OS Bookworm but missing the new lgpio Python bindings required by gpiozero for the Pi 5's RP1 chip. |
Run sudo apt install python3-lgpio. Do not attempt to install the legacy RPi.GPIO package; it will fail to compile on the Pi 5. |
smbus2.exceptions.IOError: [Errno 121] Remote I/O error |
The I2C bus cannot acknowledge the OLED. Usually caused by missing pull-up resistors on SDA/SCL, or the OLED is addressed at 0x3d instead of 0x3c. |
Verify 4.7kΩ pull-ups are soldered. Run i2cdetect -y 1. If you see 3d, change the address parameter in your luma.oled initialization. |
Extending and Simplifying the Build
Not everyone wants a full breadboard array on their media console. Here is how to scale this project to your exact needs.
How to Simplify (The Minimalist Build)
If you only care about physical volume control and want to hide the hardware inside a 3D-printed enclosure behind the TV:
- Drop the OLED: Remove all
luma.oleddependencies. The script will run headless and consume less than 15MB of RAM. - Use a Rotary Encoder: Replace the Vol Up/Down buttons with a KY-040 Rotary Encoder. Wire the CLK pin to GPIO 6 and DT pin to GPIO 13. Use the
gpiozero.RotaryEncoderclass to map clockwise rotation toGUI.SetVolume (increment)and counter-clockwise to decrement. This gives you a premium, machined-aluminum volume knob experience.
How to Extend (The Audiophile Build)
If you are integrating this into a high-end DAC setup:
- Add Hardware Mute Relay: Software muting in Kodi still passes a digital zero-bit stream to your DAC, which can sometimes cause popping in sensitive amplifiers. Wire a 5V Songle SRD-05VDC-SL-C relay via a 2N2222 NPN transistor to GPIO 26. Trigger the relay in the
action_mute()function to physically sever the analog audio line to your amp. - Network Wake-on-LAN: Add a Python
wakeonlanmodule call to a 5th button. This allows your GPIO panel to send a magic packet to your NAS or main server to wake it from sleep before triggering the Kodi JSON-RPC library update.
Building a physical interface for Kodi for Raspberry Pi bridges the gap between a software project and a consumer-grade appliance. By leveraging the Pi 5’s RP1 silicon and Kodi’s native JSON-RPC endpoints, you eliminate the latency and dropped packets inherent in Bluetooth IR remotes, resulting in a media center that responds the millisecond you press the button.






