When builders search for a kodi xbmc raspberry pi setup, they usually stop at flashing an SD card and plugging in an HDMI cable. But treating the Raspberry Pi as a true embedded system unlocks hardware-level integrations that off-the-shelf streaming sticks cannot match. By wiring an I2C OLED display and a custom IR receiver directly to the GPIO header, you can build a media center with physical telemetry and independent remote control, bypassing the need for HDMI-CEC handshakes.
This guide walks through building a Kodi-integrated hardware telemetry daemon. We will wire an SSD1306 OLED to display real-time playback states via Kodi’s JSON-RPC API, integrate a TSOP38238 IR sensor for custom macro buttons, and write a robust Python service to tie it all together.
Estimated Time: 3 hours (Hardware assembly + software configuration).
Target Board: Raspberry Pi 4 Model B (4GB RAM) running LibreELEC 12 (Kodi 21 Omega).
Hardware BOM & Pin Mapping
Before writing any code, we need to establish the physical layer. The Raspberry Pi 4 Model B is the target here because its hardware video decoder (via the updated firmware in LibreELEC 12) handles 4K HEVC smoothly, and its 3.3V logic is strictly regulated compared to older Pi 3 boards. Below is the exact bill of materials and the GPIO mapping required for this build.
Bill of Materials (BOM)
| Component | Exact Model / Variant | Specs & Notes | Est. Cost |
|---|---|---|---|
| Microcontroller | Raspberry Pi 4 Model B | 4GB RAM variant. 2GB is insufficient for Kodi 21 Omega's Python add-on overhead. | $55.00 |
| Display Module | SSD1306 I2C OLED | 128x64 pixels, 0.96", 3.3V/5V tolerant. Must have 4 pins (VCC, GND, SCL, SDA). | $6.50 |
| IR Receiver | TSOP38238 | 38kHz carrier frequency. Do not use TSOP4838 (different pinout). | $1.20 |
| Proto Board | Pi GPIO Proto HAT | Includes 40-pin header and ground plane. Avoid breadboards for permanent TV setups. | $8.00 |
| Passives | 100Ω and 10kΩ Resistors | 100Ω for IR VCC current limiting; 10kΩ for IR data line pull-up. | $0.50 |
GPIO Pin Mapping
The I2C bus on the Pi uses dedicated hardware pins. The IR receiver is mapped to GPIO 17, which supports hardware PWM if you later decide to drive an IR blaster instead of a receiver.
| Component Pin | Pi GPIO / Bus | Physical Pin # | Wiring Note |
|---|---|---|---|
| SSD1306 VCC | 3.3V Power | Pin 1 | Use 3.3V to avoid frying the OLED logic if it lacks a 5V regulator. |
| SSD1306 GND | Ground | Pin 6 | Keep I2C ground return short to prevent signal ringing. |
| SSD1306 SCL | GPIO 3 (SCL1) | Pin 5 | Internal Pi pull-up is 1.8kΩ; sufficient for 400kHz Fast Mode. |
| SSD1306 SDA | GPIO 2 (SDA1) | Pin 3 | Do not swap SDA/SCL; hardware I2C will fail silently. |
| TSOP38238 VCC | 3.3V Power | Pin 17 | Place 100Ω resistor in series to limit inrush current. |
| TSOP38238 GND | Ground | Pin 14 | Shared ground plane with OLED. |
| TSOP38238 OUT | GPIO 17 | Pin 11 | Add 10kΩ pull-up to 3.3V to prevent floating logic when idle. |
Base OS & Kodi JSON-RPC Configuration
We are using LibreELEC 12 because it strips away the desktop environment overhead, dedicating maximum RAM and CPU cycles to Kodi. However, LibreELEC runs a read-only root filesystem, which dictates where we place our custom scripts.
- Flash the OS: Use the Raspberry Pi Imager to flash LibreELEC 12 onto a high-endurance microSD card (e.g., SanDisk High Endurance 32GB). Standard cards will fail within months due to Kodi's constant database writes.
- Enable SSH: Boot the Pi, navigate to Settings → System → LibreELEC → Services, and toggle SSH on. Set a custom root password.
- Enable Kodi HTTP Control: This is the bridge between our Python script and Kodi. Go to Settings → Services → Control. Enable Allow remote control via HTTP. Set the port to
8080, username tokodi, and leave the password blank for local-only JSON-RPC access. - Enable I2C: SSH into the Pi and mount the config partition:
mount -o remount,rw /flash. Edit/flash/config.txtand ensuredtparam=i2c_arm=onis present and uncommented. Reboot.
i2cdetect -y 1. You should see 3c in the grid. If the grid is empty, check your SDA/SCL physical pin orientation.
Python Telemetry Daemon (Targeting Pi 4 & LibreELEC 12)
The following Python script polls Kodi's JSON-RPC API every 500ms. If media is playing, it extracts the title and progress percentage and renders it to the SSD1306 OLED. If playback stops, it displays the system IP and CPU temperature. This code targets Python 3.11+ and requires the luma.oled and requests libraries.
Note: In LibreELEC, install these via the Entware package manager or run the script inside a Docker container mapped to the host network. For this build, we assume a standard Python virtual environment in /storage/scripts/kodi-telemetry/venv.
#!/usr/bin/env python3
"""
Kodi GPIO Telemetry Daemon
Target: Raspberry Pi 4 Model B (LibreELEC 12 / Kodi 21)
Hardware: SSD1306 I2C OLED (Addr 0x3C), TSOP38238 IR (GPIO 17)
"""
import time
import json
import requests
import subprocess
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from gpiozero import Button
# --- PIN & HARDWARE DEFINITIONS ---
I2C_PORT = 1
I2C_ADDRESS = 0x3C
IR_SENSOR_PIN = 17 # Physical Pin 11
# Kodi JSON-RPC Configuration
KODI_IP = 'localhost'
KODI_PORT = '8080'
KODI_URL = f'http://{KODI_IP}:{KODI_PORT}/jsonrpc'
HEADERS = {'Content-Type': 'application/json'}
# Initialize Hardware
serial = i2c(port=I2C_PORT, address=I2C_ADDRESS)
display = ssd1306(serial, width=128, height=64)
ir_sensor = Button(IR_SENSOR_PIN, pull_up=True, bounce_time=0.1)
def get_kodi_status():
"""Polls Kodi JSON-RPC for active player properties."""
payload = {
"jsonrpc": "2.0",
"method": "Player.GetActivePlayers",
"id": 1
}
try:
response = requests.post(KODI_URL, headers=HEADERS, data=json.dumps(payload), timeout=2)
data = response.json()
if data.get('result'):
player_id = data['result'][0]['playerid']
return get_player_info(player_id)
return None
except requests.exceptions.ConnectionError as e:
raise e
def get_player_info(player_id):
"""Fetches playback percentage and title."""
payload = {
"jsonrpc": "2.0",
"method": "Player.GetProperties",
"params": {"playerid": player_id, "properties": ["percentage", "time", "totaltime"]},
"id": 1
}
res = requests.post(KODI_URL, headers=HEADERS, data=json.dumps(payload), timeout=2).json()
item_payload = {
"jsonrpc": "2.0",
"method": "Player.GetItem",
"params": {"playerid": player_id, "properties": ["title"]},
"id": 2
}
item_res = requests.post(KODI_URL, headers=HEADERS, data=json.dumps(item_payload), timeout=2).json()
title = item_res.get('result', {}).get('item', {}).get('title', 'Unknown Media')
pct = res.get('result', {}).get('percentage', 0)
return title, pct
def get_cpu_temp():
"""Reads Pi 4 thermal sensor via vcgencmd."""
try:
output = subprocess.check_output(['vcgencmd', 'measure_temp']).decode()
return output.replace('temp=', '').replace("'C\n", '')
except Exception:
return "N/A"
def ir_macro_triggered():
"""Hardware interrupt for IR sensor (custom macro)."""
print("[IR] Sensor triggered - executing macro.")
# Example: Send Play/Pause toggle to Kodi
payload = {"jsonrpc": "2.0", "method": "Input.ExecuteAction", "params": {"action": "playpause"}, "id": 1}
try:
requests.post(KODI_URL, headers=HEADERS, data=json.dumps(payload), timeout=2)
except Exception:
pass
ir_sensor.when_pressed = ir_macro_triggered
def main():
print("Starting Kodi Telemetry Daemon...")
while True:
try:
status = get_kodi_status()
with canvas(display) as draw:
if status:
title, pct = status
# Truncate title to fit 128px width
draw.text((0, 0), title[:20], fill="white")
draw.text((0, 20), f"Progress: {pct:.1f}%", fill="white")
# Draw progress bar
draw.rectangle((0, 40, 127, 50), outline="white")
draw.rectangle((0, 40, int(127 * (pct / 100)), 50), fill="white")
else:
draw.text((0, 0), "KODI IDLE", fill="white")
draw.text((0, 20), f"CPU: {get_cpu_temp()}C", fill="white")
draw.text((0, 40), "IR: Armed (GPIO17)", fill="white")
except requests.exceptions.ConnectionError:
# Kodi is likely restarting or JSON-RPC is disabled
with canvas(display) as draw:
draw.text((0, 20), "KODI OFFLINE", fill="white")
draw.text((0, 40), "Check Port 8080", fill="white")
time.sleep(0.5)
if __name__ == "__main__":
main()
Debugging: Connection Refused & I2C Faults
When integrating software with embedded hardware, the physical layer and the application layer frequently clash. If your script crashes or the OLED remains blank, you are likely hitting one of the following failure modes.
The "Connection Refused" Error
If your terminal outputs the following exact error string, the Python script cannot reach Kodi's internal web server:
requests.exceptions.ConnectionError: HTTPConnectionPool(host='localhost', port=8080): Max retries exceeded with url: /jsonrpc (Caused by NewConnectionError('... Failed to establish a new connection: [Errno 111] Connection refused'))
Ranked Causes & Fixes:
- HTTP Control Disabled in Kodi: The most common cause. Navigate the Kodi UI and verify Allow remote control via HTTP is toggled ON. A reboot of the Pi does not save this setting if it wasn't applied in the GUI.
- Systemd Race Condition: If your Python script is set to run at boot via
/storage/.config/autostart.sh, it may execute before Kodi's network stack binds to port 8080. Fix: Wrap the main loop in atry/exceptblock (as shown in the code above) so it fails gracefully and retries on the next 500ms tick, rather than crashing the script. - Port Collision or Change: Another service (like a Docker container running Home Assistant) might have claimed port 8080. Check via SSH using
netstat -tulpn | grep 8080. If Kodi is on a different port, update theKODI_PORTvariable in the script.
The First Three Things to Check When It Fails
If the script runs but the OLED is blank and the IR sensor does nothing, run through this diagnostic triage:
- Verify I2C Bus State: Run
i2cdetect -y 1. If3cis missing, your SDA/SCL wires are swapped, or the OLED's 3.3V regulator is dead. Measure voltage across the OLED VCC/GND pins with a multimeter; it must read 3.2V to 3.4V. - Check GPIO Pull-up Logic: The TSOP38238 output pin is active-low. If you omitted the 10kΩ physical pull-up resistor, GPIO 17 will float, causing the
gpiozeroButton class to register phantom IR presses. Measure the voltage on GPIO 17; it should sit steadily at 3.3V when no IR light is hitting the sensor. - Inspect LibreELEC Logs: Run
journalctl -u kodito see if Kodi itself is crashing and restarting, which would sever the JSON-RPC connection repeatedly.
Extending or Simplifying the Build
Not every setup requires a custom telemetry screen, and some builders prefer to lean entirely on their TV's native remote. Here is how to scale this project up or down based on your bench capabilities.
Simplifying: Strip to HDMI-CEC Only
If you want to eliminate the OLED and IR sensor entirely, you can rely on HDMI-CEC. CEC allows your TV remote to send infrared commands to the TV, which then forwards them down the HDMI cable to the Pi.
Trade-off: CEC is notoriously inconsistent across TV brands (Samsung calls it Anynet+, LG calls it SimpLink). If your Pi 4 fails to wake the TV, you will need to add a HDMI-CEC adapter or revert to the TSOP38238 IR hardware built in this guide.
Extending: Adding an RF Macro Keypad
To extend the build, replace the single TSOP38238 with an nRF24L01+ 2.4GHz transceiver wired to the SPI bus (GPIO 10/9/11). This allows you to build a custom, battery-powered physical macro keypad (using an Arduino Pro Micro and mechanical switches) that communicates with the Pi without line-of-sight limitations. You would modify the Python script to listen to the SPI buffer instead of gpiozero interrupts, mapping physical rotary encoders to Kodi volume and seek commands via the JSON-RPC Application.SetVolume method.
By treating your kodi xbmc raspberry pi deployment as an embedded project rather than a simple software installation, you gain total control over the physical interface. The JSON-RPC API combined with direct GPIO access turns a standard media player into a highly responsive, custom-tailored hardware appliance.






