Fire Remote Remap Script

I’ve put together a helpful little script that let me take full control of my Fire TV remote by remapping the keys. Before this, several buttons were just sitting useless, especially when I tried using them with Kodi. I gave Kodi’s key remapper addon a shot, but it didn’t quite cut it. That’s when I decided to roll up my sleeves and make something myself.

This script has been a game changer for me. It might not be the textbook way to handle things, but guess what? It works great for what I need. If you’ve been in the same boat, wanting all your remote’s keys to actually do something, you might want to give this a try.

What’s the Deal?

  • Device Discovery: Automatically finds your remote by name. No manual setup required.
  • Key Remapping: Converts unused or unsupported remote buttons into functional keys for your system.
  • MQTT Integration: This allows some remote to integrate with MQTT to allow you to trigger other automations.
  • Prevent Accidental Repeats: Adds a simple mechanism to stop the same button from sending multiple commands too quickly.
  • Debug Insights: If you like to see what’s happening under the hood, switch on the debug mode for detailed logs.

Getting Started

  • Python 3.x
  • The evdev library
  • xdotool for simulating keypresses on Linux

How to Set It Up

  • Install the Essentials:
pip install evdev 
sudo apt-get install xdotool
  • Download and Prep the Script:
    • Grab the script and save it where you prefer.
    • Make it executable with
chmod +x <script_name>.py
  • Customize Your Controls:
    • Open up the script and tweak the key_mapping to set up what each button should do.
  • Run It and Enjoy:
    • Fire up the script and see your remote come to life.
 python <script_name>.py

How It Works

Once you kick things off, the script:

  • Sets up the environment.
  • Searches for the remote and adjusts permissions if needed.
  • Listens for button presses and maps them to the corresponding actions you’ve set up.

A Little Troubleshooting

  • Can’t Find the Remote? Make sure the remote’s name in the script matches its system name exactly. You can use the command below to check for your remote.
sudo evtest
  • Permissions Issues? You might need to run the script with more privileges or adjust the device file permissions.
  • Keys Not Behaving? Double-check your key_mapping to make sure the commands are set up right.

If you’d like to remap your keys, you can use the sudo evtest command. Start by selecting the input device from the list presented. When you press a key, evtest will output information similar to the example below. The value provided is your key code, which you’ll need to define in your script. For instance, in this example, c008d is the key code. In the script, you should refer to it as x0c008d.

Event: time 1722150873.677004, type 4 (EV_MSC), code 4 (MSC_SCAN), value c008d
Event: time 1722150873.677004, type 1 (EV_KEY), code 362 (KEY_PROGRAM), value 1
Event: time 1722150873.677004, -------------- SYN_REPORT ------------

In my setup, I’ve configured the MQTT buttons to interface with Home Assistant, allowing for seamless control of my TV and voice assistant. When I press the “Power” button, an MQTT message (power_pressed) is sent, which is picked up Home Assistant then send a command to my Broadlink RM Pro to power my TV on or off. On the other hand, the “Voice” button is integrated with another script that enables voice control functionality. When this button is pressed, an MQTT message (voice_pressed) triggers the script, allowing me to use voice commands to control various smart home devices or perform specific actions. To ensure accurate feedback, both buttons also send an idle message after a brief period, indicating that they are no longer active. This setup not only simplifies the control of my TV and voice assistant but also ensures that every action is precisely tracked through MQTT messaging.

That’s about it! This setup has been rock solid for me, especially for my Kodi setup. If you’re looking to get more out of your remote, why not give this a shot? Maybe it’ll be just what you need. Cheers! 🚀

Script:

import evdev
from evdev import InputDevice, categorize, ecodes
import subprocess
import os
import logging
import threading
import time
import paho.mqtt.client as mqtt

# Ensure the DISPLAY environment variable is set
os.environ['DISPLAY'] = ':0'

# Debug toggle
debug = True

# Set up logging to console
logging.basicConfig(level=logging.DEBUG if debug else logging.INFO, format='%(asctime)s %(message)s')

# MQTT settings
mqtt_broker = "192.168.1.110"
mqtt_port = 1883
mqtt_topic_voice = "minipc/fireremote/voice"
mqtt_topic_power = "minipc/fireremote/power"
mqtt_username = "user"
mqtt_password = "password"

# Idle times for voice and power buttons (in seconds)
voice_idle_timeout = 1  # User-configurable idle time for voice button
power_idle_timeout = 1  # User-configurable idle time for power button

# Initialize MQTT client
mqtt_client = mqtt.Client()

# Set username and password
mqtt_client.username_pw_set(mqtt_username, mqtt_password)

# Define on_connect callback
def on_connect(client, userdata, flags, rc):
    if rc == 0:
        logging.info("Connected to MQTT Broker!")
    else:
        logging.error(f"Failed to connect, return code {rc}")

# Define on_publish callback
def on_publish(client, userdata, mid):
    logging.info("Message Published!")

mqtt_client.on_connect = on_connect
mqtt_client.on_publish = on_publish

# Connect to the broker
mqtt_client.connect(mqtt_broker, mqtt_port, 60)

# Start the loop
mqtt_client.loop_start()

# Function to find the device path by name
def find_device_by_name(name):
    devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
    for device in devices:
        if device.name == name:
            return device.path
    return None

# Path to the input device
device_name = 'Amazon Remote Keyboard'
device_path = find_device_by_name(device_name)
if device_path is None:
    logging.error(f'Device with name {device_name} not found.')
    exit(1)

# Function to set permissions to the input device
def set_device_permissions(device_path):
    try:
        subprocess.run(['sudo', 'chmod', 'a+rw', device_path], check=True)
    except subprocess.CalledProcessError as e:
        logging.error(f'Error setting permissions: {e}')
        exit(1)

set_device_permissions(device_path)

device = InputDevice(device_path)
logging.info(f'Listening on {device_path}')

key_mapping = {
    0xc0040: 'm',  # Menu Key
    0xc008d: 'h',  # Guide Key mapped to 'h'
    0xc009c: 'Up',  # Channel Up
    0xc009d: 'Down',  # Channel Down
    0xc00b4: 'r',  # Rewind
    0xc00b3: 'f',  # Fast Forward
    0xc0033: 'c',  # Settings Key
    0xc027e: 'i',  # Button 1
    0xc027f: 't',  # Button 2
    0xc0221: 'MQTT_POWER',  # Power button mapped to MQTT "power_pressed"
    0xc00cd: 'space',  # Play/Pause
    0xc0280: 'MQTT_VOICE'  # Voice button mapped to MQTT "voice_pressed"
}

last_key = None  # To track the last processed key
timeout = 0.5  # Timeout in seconds (same as original script)
lock = threading.Lock()

# Keys for which we want to bypass the last_key logic
bypass_keys = {0xc00b4, 0xc00b3, 0xc00cd}  # Rewind, Fast Forward, Play/Pause

def send_idle_message(topic):
    logging.debug(f"Sending idle message to {topic}")
    result = mqtt_client.publish(topic, "idle")
    if result.rc != mqtt.MQTT_ERR_SUCCESS:
        logging.error(f"Failed to send MQTT idle message: {result.rc}")
    else:
        logging.debug("MQTT idle message sent successfully")

def reset_last_key():
    global last_key
    with lock:
        if debug:
            logging.debug(f'Resetting last_key from {last_key} to None')
        last_key = None

def remap_key(scan_code):
    global last_key
    if debug:
        logging.debug(f'Attempting to remap scan code: {scan_code}')
    with lock:
        if scan_code in key_mapping:
            new_key = key_mapping[scan_code]
            if debug:
                logging.debug(f'Scan code {scan_code} maps to {new_key}')
            if scan_code in bypass_keys or last_key != scan_code:
                if new_key == 'MQTT_VOICE':
                    logging.debug("MQTT_VOICE action triggered")
                    result = mqtt_client.publish(mqtt_topic_voice, "voice_pressed")
                    if result.rc != mqtt.MQTT_ERR_SUCCESS:
                        logging.error(f"Failed to send MQTT voice pressed message: {result.rc}")
                    else:
                        logging.debug("MQTT voice pressed message sent successfully")
                    threading.Timer(voice_idle_timeout, send_idle_message, args=[mqtt_topic_voice]).start()

                elif new_key == 'MQTT_POWER':
                    logging.debug("MQTT_POWER action triggered")
                    result = mqtt_client.publish(mqtt_topic_power, "power_pressed")
                    if result.rc != mqtt.MQTT_ERR_SUCCESS:
                        logging.error(f"Failed to send MQTT power pressed message: {result.rc}")
                    else:
                        logging.debug("MQTT power pressed message sent successfully")
                    threading.Timer(power_idle_timeout, send_idle_message, args=[mqtt_topic_power]).start()

                else:
                    if debug:
                        logging.debug(f'Executing xdotool for key: {new_key}')
                    subprocess.run(['xdotool', 'key', new_key])

                if scan_code not in bypass_keys:
                    last_key = scan_code  # Update the last processed key only for non-bypass keys
                    threading.Timer(timeout, reset_last_key).start()
            else:
                if debug:
                    logging.debug(f'Scan code {scan_code} is the same as the last key processed. Skipping.')
        else:
            if debug:
                logging.warning(f'No mapping found for scan code: {scan_code}')

try:
    for event in device.read_loop():
        if event.type == evdev.ecodes.EV_MSC and event.code == evdev.ecodes.MSC_SCAN:
            if debug:
                logging.debug(f'Detected MSC event: {event}')
            remap_key(event.value)
        elif event.type == evdev.ecodes.EV_KEY and event.value == 1:  # Only key press events
            key_event = categorize(event)
            if debug:
                logging.debug(f'Detected key down: {key_event.scancode}, key code: {key_event.keycode}')
        time.sleep(0.01)  # Add a small delay to ensure events are processed properly
except KeyboardInterrupt:
    logging.info('Exiting...')
    mqtt_client.loop_stop()
except Exception as e:
    logging.error(f'Unexpected error: {e}', exc_info=True)
    mqtt_client.loop_stop()
    exit(1)

Leave a Reply

Your email address will not be published. Required fields are marked *