Skip to main content

evmqtt - Linux Input Event to MQTT Gateway

CI PyPI GHCR HACS Custom Python 3.10+ License: MIT

Capture Linux input events (keyboards, IR remotes, gamepads) and publish them to an MQTT broker. Perfect for integrating hardware buttons and remote controls with Home Assistant.

Based on the original gist by James Bulpin.

Which install do you need?

Your setup Install
Home Assistant OS or Supervised Add-on
Home Assistant Container or Core HACS integration
Any other MQTT consumer, or no Home Assistant at all Docker, systemd or pip

Features

  • Home Assistant MQTT device discovery: one HA device per input device, with an event entity for keys and a switch to enable or disable it
  • Stable device ids that survive reboots, eventN renumbering and (with a serial) port moves
  • Grabs only devices you list and have enabled; auto-discovered devices are read without taking them from the system
  • Opt-in by default: an auto-discovered device (which may be your own keyboard) starts disabled, so it is never published until a person enables it
  • Enable state persists across restarts
  • Gateway and per-device availability (LWT), hotplug support
  • Keeps running while the broker is down and reconnects with backoff
  • Home Assistant add-on that uses the Mosquitto add-on's credentials automatically
  • Docker, systemd and plain Python deployment
  • HACS integration for HA Container and Core: native entities, no broker, optional MQTT mirror

Installation

The easiest way to use evmqtt with Home Assistant OS or Supervised is as a Supervisor add-on. Uses the prebuilt image from ghcr.io/odtgit/evmqtt, no local build.

Add Repository to Supervisor

  1. Go to Settings → Add-ons → Add-on Store
  2. Click ⋮ (three dots menu) → Repositories
  3. Add this repository URL: https://github.com/odtgit/evmqtt
  4. Click Add → Close
  5. Find "evmqtt" in the add-on store and click Install
  6. Configure via the add-on's Configuration tab
  7. Start the add-on

Local Add-on Installation

Alternatively, clone directly to your local add-ons folder (config.yaml still points at the prebuilt image, so this does not build locally either):

cd /addons
git clone https://github.com/odtgit/evmqtt

Then restart Home Assistant, go to Settings → Add-ons → evmqtt and configure.

Option 2: HACS Integration

For Home Assistant Container or Core (no Supervisor), install the custom integration through HACS:

  1. HACS → Custom repositories → add https://github.com/odtgit/evmqtt, category Integration
  2. Install "evmqtt"
  3. Restart Home Assistant
  4. Settings → Devices & Services → Add Integration → search "evmqtt"

See HACS integration.

Option 3: Docker Container

# Create your config from the template
cp config.example.json config.json
# Edit config.json with your settings

# Run with access to all input devices, including hotplugged ones
docker run -d \
  --name evmqtt \
  --network host \
  --device-cgroup-rule='c 13:* rw' \
  -v /dev/input:/dev/input:ro \
  -v $(pwd)/config.json:/data/config.json:ro \
  -v evmqtt-state:/var/lib/evmqtt \
  -e STATE_DIRECTORY=/var/lib/evmqtt \
  ghcr.io/odtgit/evmqtt:latest

c 13:* rw gives the container every input device, so auto-discovery also finds the host's own keyboard. It gets entities but starts disabled: it is neither grabbed nor published until a person enables it (in Home Assistant, or by listing it). List the device you want in devices or enabled_devices (see Device selection), or pass only that device instead of the cgroup rule (--device /dev/input/rc; a device passed this way is not seen again after it is replugged).

Or use Docker Compose (also expects a config.json created from config.example.json as above; compose.yaml has a commented build: . if you want to build locally instead of pulling the image):

docker compose up -d

Option 4: Python Package

pip install 'evmqtt[mqtt]'

evmqtt -c config.json -v

Installing from source or in editable mode is under Development.

Option 5: Systemd Service

evmqtt.service runs as a systemd DynamicUser, which has no home directory, so install evmqtt somewhere on the system PATH that a service can see, not with a plain per-user pip/pipx install.

Venv:

sudo python3 -m venv /opt/evmqtt
sudo /opt/evmqtt/bin/pip install 'evmqtt[mqtt]'
sudo ln -s /opt/evmqtt/bin/evmqtt /usr/local/bin/evmqtt

Or pipx (>= 1.4) in global mode, which also lands in /usr/local/bin:

sudo pipx install --global 'evmqtt[mqtt]'

Either way evmqtt ends up on /usr/local/bin, which is on the PATH that ExecStart=/usr/bin/env evmqtt ... in evmqtt.service resolves against. Then configure and install the unit (both files are in this repo):

EVMQTT_VERSION=2.1.0  # the release you installed: pip show evmqtt
EVMQTT_RAW=https://raw.githubusercontent.com/odtgit/evmqtt/refs/tags/v$EVMQTT_VERSION
sudo mkdir -p /etc/evmqtt
sudo curl -fsSL -o /etc/evmqtt/config.json "$EVMQTT_RAW/config.example.json"
sudo chmod 644 /etc/evmqtt/config.json
# Edit /etc/evmqtt/config.json with your settings

sudo curl -fsSL -o /etc/systemd/system/evmqtt.service "$EVMQTT_RAW/evmqtt.service"
sudo systemctl daemon-reload
sudo systemctl enable --now evmqtt

evmqtt.service runs in the input group, so /etc/evmqtt/config.json must stay world-readable (mode 644) for the service to read it.

Configuration

The same keys work in config.json and in the add-on options.

Key Default Description
mqtt_host add-on: provided broker Broker host. Required outside the add-on.
mqtt_port 1883, 8883 with TLS Broker port
mqtt_username / mqtt_password none Broker credentials
mqtt_tls false Connect with TLS
mqtt_tls_ca system CAs CA file for TLS (implies TLS)
name evmqtt <hostname> Name of the gateway device in HA
discovery_prefix homeassistant HA discovery prefix
base_topic evmqtt/<hostname> Root of all state, event and command topics. Must not be under discovery_prefix.
auto_discover true Select keyboard-like devices automatically. When false, only devices are used.
devices [] Extra devices by stable id, path or name. Listed devices are used even if virtual or not keyboard-like, start enabled, and are grabbed while enabled.
enabled_devices [] Devices that should start enabled, by id, path or name, for the first time they are seen. Everything else (including every auto-discovered device) starts disabled; see Enable, grab and persistence.
keystates ["PRESS"] Any of PRESS, REPEAT, RELEASE
rescan_interval 5 Seconds between hotplug scans, 0 disables
state_file see below Where the enable state is kept
cleanup_legacy true Remove retained 1.x discovery on start
log_level info debug, info, warning, error. -v, -d and --log-level override it.

Deprecated 1.x keys still load with a warning: serverip, port, username, password, tls, tls_ca map to the mqtt_* keys; topic and filter_keys_only are described in Upgrading from 1.x.

Configuration is read from, in order: -c FILE, $EVMQTT_CONFIG, /data/options.json (add-on), ./config.local.json, ./config.json.

{
  "mqtt_host": "192.168.1.10",
  "mqtt_username": "mqtt_user",
  "mqtt_password": "mqtt_password",
  "name": "Living room remote",
  "keystates": ["PRESS", "RELEASE"],
  "enabled_devices": ["gpio-ir-recv-1a2b3c4d"]
}

Home Assistant add-on

Leave MQTT Host empty: the add-on declares services: mqtt:need and reads host, port, credentials and TLS of the broker Home Assistant provides (the Mosquitto add-on) from the Supervisor. Any mqtt_* option you set overrides the provided value.

Device selection

By default evmqtt uses every device that has at least one real keyboard key, so mice, power buttons and the video bus are left alone. Virtual devices (bus VIRTUAL or created through uinput, like keyd's keyd virtual keyboard or ydotool) are always skipped unless listed in devices or enabled_devices: grabbing keyd's output device takes away all keyboard input on a desktop. Bluetooth LE keyboards and remotes, which BlueZ creates through uhid, are not treated as virtual.

In devices and enabled_devices a path may also be a symlink to the event node, such as a udev rule's /dev/input/rc or /dev/input/by-id/....

Selection only decides which devices get entities; it does not enable them. An auto-discovered device (not named in devices or enabled_devices) always starts disabled, see below.

evmqtt --list-devices prints every device with its stable id and whether it is selected by default:

  /dev/input/event3    razer-razer-huntsman-mini-048d6e11           "Razer Razer Huntsman Mini"  [keyboard] (default)
  /dev/input/event10   keyd-virtual-keyboard-271f969c               "keyd virtual keyboard"  [keyboard, virtual]

The id is also in the log and in every event payload (deviceId).

Enable, grab and persistence

Privacy rationale: an auto-discovered device may be the keyboard you are typing this config on, so evmqtt never publishes its keys until a person opts it in.

A device listed in devices or enabled_devices starts enabled. Every other device, including every auto-discovered one, starts disabled: it gets discovery entities, but nothing is read into events, nothing is published, and it is not grabbed until it is enabled. Enable it with the switch in Home Assistant, or by adding it to devices or enabled_devices.

A listed device is grabbed (EVIOCGRAB) while it is enabled: its keys reach evmqtt only, not the console or desktop. Turning the switch off releases the grab and stops events; on turns both back on. A device that cannot be grabbed (for example because another program holds it) is reported unavailable and retried on the next rescan.

A device found only by auto-discovery is never grabbed, since it may be the keyboard you use on that machine: once enabled its keys are published and still reach the system. List a remote to grab it, so that keys like KEY_POWER or KEY_SLEEP on it do not also act on the host.

The switch state is saved to a state file, keyed by device id:

Deployment State file
add-on /data/evmqtt-state.json
systemd (StateDirectory=evmqtt) /var/lib/evmqtt/state.json
compose (STATE_DIRECTORY) /var/lib/evmqtt/state.json in the evmqtt-state volume
otherwise $XDG_STATE_HOME/evmqtt/state.json, or ~/.local/state/evmqtt/state.json

devices and enabled_devices only decide the starting state the first time a device is seen; after that the switch decides, and the choice is kept in the state file across restarts.

MQTT over TLS

Set mqtt_tls to use the system CA certificates, or mqtt_tls_ca to a CA file. The default port becomes 8883. In a container, mount the CA file:

    volumes:
      - "/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro"

Usage

evmqtt [-h] [-c CONFIG] [--log-level {debug,info,warning,error}] [-v] [-d]
       [--list-devices] [--auto-discover]

evmqtt keeps running when the broker is unreachable or refuses the connection, and reconnects with backoff (1 s up to 60 s). It keeps running with no devices and picks them up when they are plugged in. It exits with 1 only for configuration errors (bad option, missing CA file, no broker configured, Supervisor refusing access).

MQTT contract

<base> is base_topic, <id> the stable device id, <node> the gateway id derived from base_topic (evmqtt/pi gives pi).

Topic Retained Payload
<base>/status yes online / offline (last will)
<base>/<id>/availability yes online / offline
<base>/<id>/event no key event JSON
<base>/<id>/switch/state yes ON / OFF
<base>/<id>/switch/set ON / OFF (command)
<prefix>/device/evmqtt_<node>/config yes gateway discovery
<prefix>/device/evmqtt_<node>_<id>/config yes device discovery

evmqtt also listens to <prefix>/status and republishes discovery when Home Assistant comes online.

Key event, one message per configured key state:

{
  "event_type": "press",
  "key": "KEY_VOLUMEUP",
  "modifiers": ["KEY_LEFTSHIFT"],
  "state": "PRESS",
  "deviceId": "gpio-ir-recv-1a2b3c4d",
  "deviceName": "gpio_ir_recv",
  "devicePath": "/dev/input/event3"
}

key is the kernel name of the key, modifiers the modifier keys held on the same device, sorted. Modifier keys and KEY_NUMLOCK produce no events of their own.

Device discovery (homeassistant/device/evmqtt_pi_gpio-ir-recv-1a2b3c4d/config):

{
  "device": {
    "identifiers": ["evmqtt_pi_gpio-ir-recv-1a2b3c4d"],
    "name": "gpio_ir_recv",
    "manufacturer": "Logitech",
    "model": "USB Receiver",
    "model_id": "046d:c52b",
    "via_device": "evmqtt_pi"
  },
  "origin": {"name": "evmqtt", "sw_version": "2.1.0", "support_url": "https://github.com/odtgit/evmqtt"},
  "availability": [
    {"topic": "evmqtt/pi/status", "payload_available": "online", "payload_not_available": "offline"},
    {"topic": "evmqtt/pi/gpio-ir-recv-1a2b3c4d/availability", "payload_available": "online", "payload_not_available": "offline"}
  ],
  "availability_mode": "all",
  "components": {
    "event": {
      "platform": "event",
      "unique_id": "evmqtt_pi_gpio-ir-recv-1a2b3c4d_event",
      "name": "Key",
      "icon": "mdi:keyboard",
      "device_class": "button",
      "state_topic": "evmqtt/pi/gpio-ir-recv-1a2b3c4d/event",
      "event_types": ["press"]
    },
    "switch": {
      "platform": "switch",
      "unique_id": "evmqtt_pi_gpio-ir-recv-1a2b3c4d_switch",
      "name": "Enabled",
      "icon": "mdi:keyboard-settings",
      "entity_category": "config",
      "state_topic": "evmqtt/pi/gpio-ir-recv-1a2b3c4d/switch/state",
      "command_topic": "evmqtt/pi/gpio-ir-recv-1a2b3c4d/switch/set",
      "payload_on": "ON",
      "payload_off": "OFF",
      "state_on": "ON",
      "state_off": "OFF"
    }
  }
}

manufacturer and model come from the USB descriptors in sysfs and are left out when unknown, model_id is vendor:product. The gateway device has a Status connectivity binary_sensor on <base>/status. Discovery needs Home Assistant 2024.12 or later.

A device that is unplugged goes unavailable and keeps its entities; it comes back when plugged in again.

Home Assistant

Each input device shows up as a device with event.<device>_key and switch.<device>_enabled. Automation on a key:

automation:
  - alias: "Remote volume up"
    triggers:
      - trigger: state
        entity_id: event.gpio_ir_recv_key
    conditions:
      - condition: template
        value_template: >
          {{ trigger.to_state.attributes.event_type == 'press'
             and trigger.to_state.attributes.key == 'KEY_VOLUMEUP' }}
    actions:
      - action: media_player.volume_up
        target:
          entity_id: media_player.living_room

Node-RED and other MQTT consumers subscribe to <base>/+/event for the JSON stream.

HACS integration

Native Home Assistant integration for HA Container and Core, where add-ons are not available. No broker needed. It runs the same core as the daemon (evmqtt from PyPI, installed by HA from the manifest). Needs HA 2026.3 or newer.

Install through HACS (Option 2), or copy custom_components/evmqtt into <config>/custom_components/ and restart.

Device access: HA Container

services:
  homeassistant:
    image: ghcr.io/home-assistant/home-assistant:stable
    network_mode: host
    volumes:
      - ./config:/config
      - /dev/input:/dev/input:ro
    device_cgroup_rules:
      - "c 13:* rw"
  • c 13:* rw allows every input device node (major 13), including ones plugged in later; the bind mount shows new nodes without a restart (CI checks this). python-evdev only lists nodes it may open read-write, so c 13:* r finds no devices. :ro keeps the container from creating or removing nodes, it does not stop writes to them.
  • devices: [/dev/input/event3] works for one fixed device but breaks on hotplug and eventN renumbering.
  • The image runs as root. With user: set, add group_add: ["<gid>"] using the host's input group id (getent group input | cut -d: -f3).

Device access: HA Core (venv)

sudo usermod -aG input homeassistant
sudo systemctl restart home-assistant@homeassistant

evdev 1.9 has no wheels on PyPI, so the first install builds it: the host needs a C compiler, Python headers and kernel headers (Debian: build-essential python3-dev linux-libc-dev). HA Container uses HA's prebuilt wheel.

Configuration

Settings → Devices & Services → Add Integration → evmqtt. The form lists keyboard-like devices (no mice, power buttons or video bus). Tick the ones to enable. Every listed device gets entities, unticked ones start disabled. Include virtual devices adds uinput devices (keyd, kanata, ydotool); Bluetooth LE remotes (BlueZ uhid) are not virtual and are always listed. If nothing is readable the form says why: /dev/input not mapped, or no permission.

Configure on the integration:

Option Default
Enabled devices from setup Grabbed devices
Key states press Which of press/repeat/release fire events
Rescan interval 5 s Hotplug scan, 0 disables
Include virtual devices off
MQTT mirror off Only shown when the MQTT integration is set up
MQTT base topic evmqtt/<hostname> Same default as the daemon

Changing only the enabled devices applies live, anything else reloads the entry.

Grabbing follows the daemon's 2.1.0 rule: only devices you chose are grabbed. Enabling a device here, in the options or with its switch counts as listing it, so it is grabbed while enabled. Nothing is enabled or grabbed automatically. Disabled devices stay open to track modifiers but fire no events.

Entities

One HA device per input device, keyed by the core's stable id; manufacturer and model from the USB descriptors, model id vendor:product.

  • event.<device>_key: device class button, event types from the key states. Attributes key, modifiers, state (PRESS/REPEAT/RELEASE), device_id, device_name, device_path. Modifier keys do not fire on their own, they show up in modifiers.
  • switch.<device>_enabled (config): on grabs the device, so its keys reach only HA. Off releases it. Stored in the entry options, survives restarts.
  • Unplugged: both entities unavailable, kept, back on replug. Devices that are not plugged in can be deleted from their device page.
  • Newly seen devices get entities with the switch off: not grabbed, no events. A new keyboard on the HA host keeps typing locally.
  • Privacy: an enabled full keyboard sends every keystroke to HA. Any HA user or access token can read them live from the event entity (/api/states, websocket). key, modifiers, state and the device_* attributes are excluded from the recorder, so history keeps only when a press happened and its type. The MQTT mirror publishes them to the broker too. Enable remotes and macro pads, not the keyboard people type passwords on.
automation:
  - alias: "Remote: Ctrl+P toggles the lamp"
    triggers:
      - trigger: state
        entity_id: event.ir_remote_key
    conditions:
      - condition: template
        value_template: >
          {{ trigger.to_state.attributes.event_type == 'press'
             and trigger.to_state.attributes.key == 'KEY_P'
             and 'KEY_LEFTCTRL' in trigger.to_state.attributes.modifiers }}
    actions:
      - action: light.toggle
        target:
          entity_id: light.lamp

MQTT mirror

Publishes each event to <base>/<device id>/event with the daemon's JSON payload (QoS 0, not retained), so flows built on the daemon keep working. No discovery: the entities are native.

Coexistence

  • Only one process can grab a device. With the add-on or daemon and this integration on the same device, the second grab fails with EBUSY. The integration logs one warning, marks the event entity unavailable and retries on every rescan; the switch stays usable. Switch off reads the device without grabbing. Use one of them per device.
  • HA's built-in keyboard_remote is YAML only, always grabs, and fires keyboard_remote_command_received bus events with numeric key codes. This integration adds a config flow, entities per device, stable ids, modifiers, runtime grab on/off, hotplug of new devices and the MQTT mirror. Do not point both at the same device.

Upgrading to 3.0

Auto-discovered devices now start disabled (opt-in). In 2.0 and 2.1, an empty enabled_devices meant "enable all", so every auto-discovered keyboard-like device (which can include the host's own keyboard) was enabled and publishing keys by default. 3.0 closes that: only devices listed in devices or enabled_devices start enabled; everything else, including every auto-discovered device, starts disabled and must be turned on with the Home Assistant switch (or added to devices/enabled_devices).

On first start with a 2.x state file, evmqtt migrates it: devices that were "on" only because of the old default are switched to "off" (devices that match devices/enabled_devices, or that a person already toggled and got persisted, are unaffected). It logs one WARNING naming every device it disabled and how to re-enable it, then rewrites the state file with the new schema version. After that first migration the file is trusted as-is.

If you rely on an auto-discovered device (for example a remote that was never listed), add it to devices or enabled_devices, or re-enable it in Home Assistant, after upgrading.

Upgrading from 1.x

2.0 changes topics, entities, payloads and some config keys. Old entities are removed automatically; automations on them have to be rewritten.

Topics

1.x 2.0
<topic>/<slug>/state <base>/<id>/event
<topic>/<slug>/config, homeassistant/switch/<uid>/config homeassistant/device/evmqtt_<node>_<id>/config
<topic>/<slug>/switch/state, /switch/set <base>/<id>/switch/state, /switch/set
none <base>/status, <base>/<id>/availability

<slug> was the name slug (plus -2 for duplicates, eventN in manual mode); <id> is the stable id (name slug plus a hash), so topics no longer move when eventN changes.

Entities

  • sensor.<name>_<device> (last key as state) becomes event.<device>_key. The key is in the key attribute, the state is the event time.
  • switch.<device>_enable becomes switch.<device>_enabled, in the device's configuration section.
  • Every input device is its own HA device, linked to a new gateway device.

Payload

  • New: event_type (lowercase key state), modifiers (list), deviceId.
  • key is the plain key name. 1.x appended held modifiers (KEY_A_KEY_LEFTSHIFT) and joined aliased names (KEY_MIN_INTERESTING|KEY_MUTE); 2.0 sends KEY_A with "modifiers": ["KEY_LEFTSHIFT"], and KEY_MUTE.
  • state, devicePath and deviceName are unchanged.

Config

  • serverip, port, username, password, tls, tls_ca: renamed to mqtt_host, mqtt_port, mqtt_username, mqtt_password, mqtt_tls, mqtt_tls_ca. The old names still work and log a warning.
  • topic: deprecated. If it is under discovery_prefix (the 1.x default homeassistant/sensor/evmqtt), it is ignored for state topics, which move to base_topic. If it is elsewhere and base_topic is not set, it becomes base_topic. In both cases it tells the cleanup where the 1.x discovery is.
  • filter_keys_only: ignored. The default filter is stricter (keyboard-like, no virtual devices); list anything else in devices.
  • devices and enabled_devices accept ids and names as well as paths, and devices no longer requires auto_discover: false.
  • auto_discover now defaults to true in config.json too.
  • Since 2.1.0, only devices listed in devices or enabled_devices are grabbed. 1.x and 2.0.0 grabbed every device they used; list your devices to keep that.
  • Add-on: mqtt_host can be left empty to use the Mosquitto add-on.
  • Enable/disable is now kept in a state file instead of the retained switch topic; the first 2.0 start seeds it from enabled_devices.

Automations

  • Replace state triggers on sensor.* with a state trigger on the event.* entity and a condition on trigger.to_state.attributes.key (see the example above). A to: on the key no longer works: the state of an event entity is a timestamp.
  • Keys with modifiers: check attributes.modifiers instead of matching KEY_A_KEY_LEFTSHIFT.
  • MQTT triggers and Node-RED flows: subscribe to <base>/+/event.
  • Switches: update entity ids.

Cleanup of old entities

On the first connect evmqtt subscribes for a few seconds to <prefix>/+/+/config and <topic>/+/config, and clears (empty retained message) only configs whose unique_id starts with evmqtt_ and whose state_topic is under the 1.x topic, plus the retained 1.x switch state. Home Assistant then removes the old sensor and switch entities. Nothing else is touched: other integrations' configs, unparseable payloads and 2.0 device configs are left alone. Set cleanup_legacy: false to skip it.

If several 1.x gateways shared one broker and topic, the first upgraded one removes the 1.x entities of all of them; the others recreate theirs on their next 1.x start. Upgrade them together, or set cleanup_legacy: false until the last one is upgraded.

Core Library

evmqtt.core is the evdev-only asyncio layer the daemon runs on, usable without MQTT (pip install evmqtt):

import asyncio
from evmqtt.core import (
    DeviceReader,
    GrabMode,
    KeyEvent,
    KeyState,
    is_keyboard_like,
    list_devices,
    open_device,
)


def on_event(event: KeyEvent) -> None:
    if event.state is KeyState.PRESS:
        print(event.key, event.modifiers)


async def main() -> None:
    info = list_devices(is_keyboard_like)[0]
    reader = DeviceReader(
        open_device(info.path), on_event, info=info, grab=GrabMode.WHILE_ENABLED
    )
    await reader.run()


asyncio.run(main())

info.id is stable across reboots and eventN renumbering: name slug plus a hash of bus, vendor, product, name and either the serial (uniq, plus the interface number) when the device has a real one, so it survives a port move, or the port path (phys) when it does not. The MQTT daemon keys its topics and Home Assistant ids on it.

DeviceWatcher runs the same scan on an interval and reports added/removed devices, for hotplug without an event loop of your own:

from evmqtt.core import DeviceWatcher

watcher = DeviceWatcher(
    on_added=lambda info: print("added", info.id),
    on_removed=lambda info: print("removed", info.id),
    predicate=is_keyboard_like,
    interval=5.0,
)
asyncio.run(watcher.run())

Development

Clone and install in editable mode, with the mqtt and dev extras:

git clone https://github.com/odtgit/evmqtt
cd evmqtt
pip install -e ".[mqtt,dev]"

Running Tests

# Run tests (see tests/README.md for the broker and uinput tiers)
pytest -m "not broker and not uinput"

# Run with coverage
pytest tests/ -v --cov=evmqtt --cov-report=html

HACS integration tests (Python 3.14, own venv, fake devices only):

python3.14 -m venv .venv-ha
.venv-ha/bin/pip install -r tests_ha/requirements.txt -e ".[mqtt]"
cd tests_ha && ../.venv-ha/bin/pytest -q

scripts/ha_integration_validate.py runs the integration in a real HA container. Locally it only checks install and the config flow; CI adds a uinput remote (--uinput, root).

Project Structure

evmqtt/
├── src/evmqtt/             # Main package
│   ├── __init__.py
│   ├── core/               # evdev-only asyncio library (no MQTT)
│   ├── __main__.py         # CLI entry point
│   ├── config.py           # Configuration
│   ├── gateway.py          # Daemon: readers, hotplug, persistence, MQTT
│   ├── ha.py               # Topics and HA discovery payloads
│   ├── mqtt_client.py      # paho wrapper
│   ├── state.py            # Enable state file
│   ├── supervisor.py       # Add-on broker lookup
│   └── sysinfo.py          # sysfs: virtual devices, vendor/model
├── tests/                  # Test suite
├── custom_components/evmqtt/  # HACS integration
├── tests_ha/               # HACS integration tests
├── hacs.json               # HACS manifest
├── config.yaml             # HA add-on manifest
├── repository.yaml         # HA add-on repository manifest
├── Dockerfile              # Container build
├── pyproject.toml          # Python packaging
└── run.sh                  # Container entrypoint

Type Checking

mypy src/evmqtt/core

Linting

ruff check src/ tests/
ruff format src/ tests/

Requirements

  • Python 3.10+
  • evdev >= 1.6.0
  • paho-mqtt >= 2.0.0 for the daemon (evmqtt[mqtt])
  • Linux with input device access

Troubleshooting

Permission Denied for Input Device

Add your user to the input group:

sudo usermod -a -G input $USER
# Log out and back in

Or run with sudo (not recommended for production).

Device Not Found

  1. Check the device exists: ls -la /dev/input/
  2. Verify permissions: groups should include input
  3. For Docker/add-on, ensure the device is passed through

MQTT Connection Failed

evmqtt logs MQTT broker ... unreachable or refused the connection and keeps retrying.

  1. Verify mqtt_host and mqtt_port
  2. Check username/password (refused ... Not authorized)
  3. Check the broker: mosquitto_sub -h <broker> -t 'evmqtt/#' -v

Devices Not Appearing in Home Assistant

  1. Check MQTT discovery is enabled in Home Assistant and discovery_prefix matches it
  2. Check the device is selected: evmqtt --list-devices, and the log at startup
  3. Look in Settings → Devices & Services → MQTT → Devices

License

MIT License - see LICENSE file for details.

Credits

Release files for evmqtt 3.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for evmqtt 3.0.0
File Size Uploaded
evmqtt-3.0.0.tar.gz 71.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for evmqtt 3.0.0
File Interpreter ABI Platform
evmqtt-3.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 114.0 kB

Release files / evmqtt-3.0.0.tar.gz

Download URL evmqtt-3.0.0.tar.gz
Size 71.9 kB
Tags Source
SHA-256 checksum
How to use checksums
00c522ff786f324d6ba8b6823e19185769a1a528ef3716e3d0fd8b16a6a9f3c2
BLAKE2b-256 checksum
How to use checksums
943f15ae7e558ee0d8162404fb87b0468a8ed2f5bc1ee0bbaa86d10d1a674370
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / evmqtt-3.0.0-py3-none-any.whl

Download URL evmqtt-3.0.0-py3-none-any.whl
Size 42.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7a4cdcee9d8027afb254c7f589218cc2653c84121fbba126543511a4a55625bb
BLAKE2b-256 checksum
How to use checksums
866ec6a0e7a825bcde2ef53f88fce319cc1f998d855b0bd0e90765473c39ddca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

3.0.0 This release

2 release files

2.1.0

2 release files

2.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page