evmqtt 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. evmqtt-2.0.0/LICENSE +21 -0
  2. evmqtt-2.0.0/PKG-INFO +577 -0
  3. evmqtt-2.0.0/README.md +543 -0
  4. evmqtt-2.0.0/pyproject.toml +89 -0
  5. evmqtt-2.0.0/setup.cfg +4 -0
  6. evmqtt-2.0.0/src/evmqtt/__init__.py +39 -0
  7. evmqtt-2.0.0/src/evmqtt/__main__.py +163 -0
  8. evmqtt-2.0.0/src/evmqtt/config.py +304 -0
  9. evmqtt-2.0.0/src/evmqtt/core/__init__.py +65 -0
  10. evmqtt-2.0.0/src/evmqtt/core/devices.py +238 -0
  11. evmqtt-2.0.0/src/evmqtt/core/events.py +59 -0
  12. evmqtt-2.0.0/src/evmqtt/core/keys.py +104 -0
  13. evmqtt-2.0.0/src/evmqtt/core/reader.py +222 -0
  14. evmqtt-2.0.0/src/evmqtt/core/watcher.py +67 -0
  15. evmqtt-2.0.0/src/evmqtt/gateway.py +435 -0
  16. evmqtt-2.0.0/src/evmqtt/ha.py +227 -0
  17. evmqtt-2.0.0/src/evmqtt/mqtt_client.py +232 -0
  18. evmqtt-2.0.0/src/evmqtt/state.py +79 -0
  19. evmqtt-2.0.0/src/evmqtt/supervisor.py +118 -0
  20. evmqtt-2.0.0/src/evmqtt/sysinfo.py +61 -0
  21. evmqtt-2.0.0/src/evmqtt.egg-info/PKG-INFO +577 -0
  22. evmqtt-2.0.0/src/evmqtt.egg-info/SOURCES.txt +32 -0
  23. evmqtt-2.0.0/src/evmqtt.egg-info/dependency_links.txt +1 -0
  24. evmqtt-2.0.0/src/evmqtt.egg-info/entry_points.txt +2 -0
  25. evmqtt-2.0.0/src/evmqtt.egg-info/requires.txt +11 -0
  26. evmqtt-2.0.0/src/evmqtt.egg-info/top_level.txt +1 -0
  27. evmqtt-2.0.0/tests/test_cli.py +160 -0
  28. evmqtt-2.0.0/tests/test_config.py +230 -0
  29. evmqtt-2.0.0/tests/test_gateway.py +769 -0
  30. evmqtt-2.0.0/tests/test_mqtt_wrapper.py +132 -0
  31. evmqtt-2.0.0/tests/test_package.py +25 -0
  32. evmqtt-2.0.0/tests/test_slugs.py +24 -0
  33. evmqtt-2.0.0/tests/test_supervisor.py +184 -0
  34. evmqtt-2.0.0/tests/test_uinput_guard.py +23 -0
evmqtt-2.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 odtgit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
evmqtt-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,577 @@
1
+ Metadata-Version: 2.4
2
+ Name: evmqtt
3
+ Version: 2.0.0
4
+ Summary: Linux input event to MQTT gateway for home automation
5
+ Author: odtgit
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/odtgit/evmqtt
8
+ Project-URL: Repository, https://github.com/odtgit/evmqtt
9
+ Project-URL: Issues, https://github.com/odtgit/evmqtt/issues
10
+ Keywords: mqtt,home-assistant,linux,input,evdev,home-automation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Home Automation
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: evdev<2.0.0,>=1.6.0
25
+ Provides-Extra: mqtt
26
+ Requires-Dist: paho-mqtt<3.0.0,>=2.0.0; extra == "mqtt"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
32
+ Requires-Dist: trustme>=1.1.0; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # evmqtt - Linux Input Event to MQTT Gateway
36
+
37
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
+
40
+ 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.
41
+
42
+ Based on the original [gist](https://gist.github.com/jamesbulpin/b940e7d81e2e65158f12e59b4d6a0c3c) by James Bulpin.
43
+
44
+ ## Features
45
+
46
+ - 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
47
+ - Stable device ids that survive reboots, `eventN` renumbering and (with a serial) port moves
48
+ - Grabs only enabled devices; disabling a device releases it back to the system
49
+ - Enable state persists across restarts
50
+ - Gateway and per-device availability (LWT), hotplug support
51
+ - Keeps running while the broker is down and reconnects with backoff
52
+ - Home Assistant add-on that uses the Mosquitto add-on's credentials automatically
53
+ - Docker, systemd and plain Python deployment
54
+
55
+ ## Installation
56
+
57
+ ### Option 1: Home Assistant Add-on (Recommended)
58
+
59
+ The easiest way to use evmqtt with Home Assistant is as a Supervisor add-on.
60
+
61
+ > **Note:** This is an add-on, not a HACS integration. Add-ons require direct hardware access and run as separate Docker containers, which HACS does not support. Install via the Supervisor Add-on Store instead.
62
+
63
+ #### Add Repository to Supervisor
64
+
65
+ 1. Go to **Settings** → **Add-ons** → **Add-on Store**
66
+ 2. Click **⋮** (three dots menu) → **Repositories**
67
+ 3. Add this repository URL: `https://github.com/odtgit/evmqtt`
68
+ 4. Click **Add** → **Close**
69
+ 5. Find "evmqtt" in the add-on store and click **Install**
70
+ 6. Configure via the add-on's **Configuration** tab
71
+ 7. Start the add-on
72
+
73
+ #### Local Add-on Installation
74
+
75
+ Alternatively, clone directly to your local add-ons folder:
76
+
77
+ ```bash
78
+ cd /addons
79
+ git clone https://github.com/odtgit/evmqtt
80
+ ```
81
+
82
+ Then restart Home Assistant, go to **Settings** → **Add-ons** → **evmqtt** and configure.
83
+
84
+ ### Option 2: Docker Container
85
+
86
+ ```bash
87
+ # Build the image (use standard Python base for standalone deployment)
88
+ docker build --build-arg -t evmqtt .
89
+
90
+ # Create your config from the template
91
+ cp config.example.json config.json
92
+ # Edit config.json with your settings
93
+
94
+ # Run with access to all input devices, including hotplugged ones
95
+ docker run -d \
96
+ --name evmqtt \
97
+ --network host \
98
+ --device-cgroup-rule='c 13:* rw' \
99
+ -v /dev/input:/dev/input:ro \
100
+ -v $(pwd)/config.json:/data/config.json:ro \
101
+ -v evmqtt-state:/var/lib/evmqtt \
102
+ -e STATE_DIRECTORY=/var/lib/evmqtt \
103
+ evmqtt
104
+ ```
105
+
106
+ Or use Docker Compose (also expects a `config.json` created from `config.example.json` as above):
107
+
108
+ ```bash
109
+ docker compose up -d
110
+ ```
111
+
112
+ ### Option 3: Python Package
113
+
114
+ ```bash
115
+ # Install from source (the daemon needs the mqtt extra)
116
+ pip install ".[mqtt]"
117
+
118
+ # Or install in development mode
119
+ pip install -e ".[mqtt,dev]"
120
+
121
+ # Run
122
+ evmqtt -c config.json -v
123
+ ```
124
+
125
+ ### Option 4: Systemd Service
126
+
127
+ ```bash
128
+ # Clone and install the package
129
+ git clone https://github.com/odtgit/evmqtt
130
+ cd evmqtt
131
+ pip install ".[mqtt]"
132
+
133
+ # Configure
134
+ sudo mkdir -p /etc/evmqtt
135
+ sudo cp config.example.json /etc/evmqtt/config.json
136
+ sudo chmod 644 /etc/evmqtt/config.json
137
+ # Edit /etc/evmqtt/config.json with your settings
138
+
139
+ # Install service
140
+ sudo cp evmqtt.service /etc/systemd/system/
141
+ sudo systemctl daemon-reload
142
+ sudo systemctl enable --now evmqtt
143
+ ```
144
+
145
+ `evmqtt.service` runs as a systemd `DynamicUser` in the `input` group, so
146
+ `/etc/evmqtt/config.json` must stay world-readable (mode 644) for the
147
+ service to read it.
148
+
149
+ ## Configuration
150
+
151
+ The same keys work in `config.json` and in the add-on options.
152
+
153
+ | Key | Default | Description |
154
+ |-----|---------|-------------|
155
+ | `mqtt_host` | add-on: provided broker | Broker host. Required outside the add-on. |
156
+ | `mqtt_port` | `1883`, `8883` with TLS | Broker port |
157
+ | `mqtt_username` / `mqtt_password` | none | Broker credentials |
158
+ | `mqtt_tls` | `false` | Connect with TLS |
159
+ | `mqtt_tls_ca` | system CAs | CA file for TLS (implies TLS) |
160
+ | `name` | `evmqtt <hostname>` | Name of the gateway device in HA |
161
+ | `discovery_prefix` | `homeassistant` | HA discovery prefix |
162
+ | `base_topic` | `evmqtt/<hostname>` | Root of all state, event and command topics. Must not be under `discovery_prefix`. |
163
+ | `auto_discover` | `true` | Select keyboard-like devices automatically. When `false`, only `devices` are used. |
164
+ | `devices` | `[]` | Extra devices by stable id, path or name. Listed devices are used even if virtual or not keyboard-like. |
165
+ | `enabled_devices` | `[]` (all) | Initial state for devices seen for the first time, by id, path or name. Empty enables all. |
166
+ | `keystates` | `["PRESS"]` | Any of `PRESS`, `REPEAT`, `RELEASE` |
167
+ | `rescan_interval` | `5` | Seconds between hotplug scans, `0` disables |
168
+ | `state_file` | see below | Where the enable state is kept |
169
+ | `cleanup_legacy` | `true` | Remove retained 1.x discovery on start |
170
+ | `log_level` | `info` | `debug`, `info`, `warning`, `error`. `-v`, `-d` and `--log-level` override it. |
171
+
172
+ Deprecated 1.x keys still load with a warning: `serverip`, `port`,
173
+ `username`, `password`, `tls`, `tls_ca` map to the `mqtt_*` keys; `topic` and
174
+ `filter_keys_only` are described in [Upgrading from 1.x](#upgrading-from-1x).
175
+
176
+ Configuration is read from, in order: `-c FILE`, `$EVMQTT_CONFIG`,
177
+ `/data/options.json` (add-on), `./config.local.json`, `./config.json`.
178
+
179
+ ```json
180
+ {
181
+ "mqtt_host": "192.168.1.10",
182
+ "mqtt_username": "mqtt_user",
183
+ "mqtt_password": "mqtt_password",
184
+ "name": "Living room remote",
185
+ "keystates": ["PRESS", "RELEASE"],
186
+ "enabled_devices": ["gpio-ir-recv-1a2b3c4d"]
187
+ }
188
+ ```
189
+
190
+ ### Home Assistant add-on
191
+
192
+ Leave **MQTT Host** empty: the add-on declares `services: mqtt:need` and
193
+ reads host, port, credentials and TLS of the broker Home Assistant provides
194
+ (the Mosquitto add-on) from the Supervisor. Any `mqtt_*` option you set
195
+ overrides the provided value.
196
+
197
+ ### Device selection
198
+
199
+ By default evmqtt uses every device that has at least one real keyboard key,
200
+ so mice, power buttons and the video bus are left alone. Virtual devices
201
+ (bus `VIRTUAL` or created through uinput, like keyd's
202
+ `keyd virtual keyboard` or ydotool) are always skipped unless listed in
203
+ `devices` or `enabled_devices`: grabbing keyd's output device takes away all
204
+ keyboard input on a desktop.
205
+
206
+ `evmqtt --list-devices` prints every device with its stable id and whether
207
+ it is selected by default:
208
+
209
+ ```
210
+ /dev/input/event3 razer-razer-huntsman-mini-048d6e11 "Razer Razer Huntsman Mini" [keyboard] (default)
211
+ /dev/input/event10 keyd-virtual-keyboard-271f969c "keyd virtual keyboard" [keyboard, virtual]
212
+ ```
213
+
214
+ The id is also in the log and in every event payload (`deviceId`).
215
+
216
+ ### Enable, grab and persistence
217
+
218
+ An enabled device is grabbed (`EVIOCGRAB`): its keys reach evmqtt only, not
219
+ the console or desktop. Turning the switch off releases the grab and stops
220
+ events; on turns both back on. A device that cannot be grabbed (for example
221
+ because another program holds it) is reported unavailable and retried on the
222
+ next rescan.
223
+
224
+ The switch state is saved to a state file, keyed by device id:
225
+
226
+ | Deployment | State file |
227
+ |------------|------------|
228
+ | add-on | `/data/evmqtt-state.json` |
229
+ | systemd (`StateDirectory=evmqtt`) | `/var/lib/evmqtt/state.json` |
230
+ | compose (`STATE_DIRECTORY`) | `/var/lib/evmqtt/state.json` in the `evmqtt-state` volume |
231
+ | otherwise | `$XDG_STATE_HOME/evmqtt/state.json`, or `~/.local/state/evmqtt/state.json` |
232
+
233
+ `enabled_devices` only seeds devices the state file does not know yet.
234
+
235
+ ### MQTT over TLS
236
+
237
+ Set `mqtt_tls` to use the system CA certificates, or `mqtt_tls_ca` to a CA
238
+ file. The default port becomes 8883. In a container, mount the CA file:
239
+
240
+ ```yaml
241
+ volumes:
242
+ - "/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro"
243
+ ```
244
+
245
+ ## Usage
246
+
247
+ ```
248
+ evmqtt [-h] [-c CONFIG] [--log-level {debug,info,warning,error}] [-v] [-d]
249
+ [--list-devices] [--auto-discover]
250
+ ```
251
+
252
+ evmqtt keeps running when the broker is unreachable or refuses the
253
+ connection, and reconnects with backoff (1 s up to 60 s). It keeps running
254
+ with no devices and picks them up when they are plugged in. It exits with 1
255
+ only for configuration errors (bad option, missing CA file, no broker
256
+ configured, Supervisor refusing access).
257
+
258
+ ## MQTT contract
259
+
260
+ `<base>` is `base_topic`, `<id>` the stable device id, `<node>` the gateway
261
+ id derived from `base_topic` (`evmqtt/pi` gives `pi`).
262
+
263
+ | Topic | Retained | Payload |
264
+ |-------|----------|---------|
265
+ | `<base>/status` | yes | `online` / `offline` (last will) |
266
+ | `<base>/<id>/availability` | yes | `online` / `offline` |
267
+ | `<base>/<id>/event` | no | key event JSON |
268
+ | `<base>/<id>/switch/state` | yes | `ON` / `OFF` |
269
+ | `<base>/<id>/switch/set` | | `ON` / `OFF` (command) |
270
+ | `<prefix>/device/evmqtt_<node>/config` | yes | gateway discovery |
271
+ | `<prefix>/device/evmqtt_<node>_<id>/config` | yes | device discovery |
272
+
273
+ evmqtt also listens to `<prefix>/status` and republishes discovery when
274
+ Home Assistant comes online.
275
+
276
+ Key event, one message per configured key state:
277
+
278
+ ```json
279
+ {
280
+ "event_type": "press",
281
+ "key": "KEY_VOLUMEUP",
282
+ "modifiers": ["KEY_LEFTSHIFT"],
283
+ "state": "PRESS",
284
+ "deviceId": "gpio-ir-recv-1a2b3c4d",
285
+ "deviceName": "gpio_ir_recv",
286
+ "devicePath": "/dev/input/event3"
287
+ }
288
+ ```
289
+
290
+ `key` is the kernel name of the key, `modifiers` the modifier keys held on
291
+ the same device, sorted. Modifier keys and `KEY_NUMLOCK` produce no events
292
+ of their own.
293
+
294
+ Device discovery (`homeassistant/device/evmqtt_pi_gpio-ir-recv-1a2b3c4d/config`):
295
+
296
+ ```json
297
+ {
298
+ "device": {
299
+ "identifiers": ["evmqtt_pi_gpio-ir-recv-1a2b3c4d"],
300
+ "name": "gpio_ir_recv",
301
+ "manufacturer": "Logitech",
302
+ "model": "USB Receiver",
303
+ "model_id": "046d:c52b",
304
+ "via_device": "evmqtt_pi"
305
+ },
306
+ "origin": {"name": "evmqtt", "sw_version": "2.0.0", "support_url": "https://github.com/odtgit/evmqtt"},
307
+ "availability": [
308
+ {"topic": "evmqtt/pi/status", "payload_available": "online", "payload_not_available": "offline"},
309
+ {"topic": "evmqtt/pi/gpio-ir-recv-1a2b3c4d/availability", "payload_available": "online", "payload_not_available": "offline"}
310
+ ],
311
+ "availability_mode": "all",
312
+ "components": {
313
+ "event": {
314
+ "platform": "event",
315
+ "unique_id": "evmqtt_pi_gpio-ir-recv-1a2b3c4d_event",
316
+ "name": "Key",
317
+ "icon": "mdi:keyboard",
318
+ "device_class": "button",
319
+ "state_topic": "evmqtt/pi/gpio-ir-recv-1a2b3c4d/event",
320
+ "event_types": ["press"]
321
+ },
322
+ "switch": {
323
+ "platform": "switch",
324
+ "unique_id": "evmqtt_pi_gpio-ir-recv-1a2b3c4d_switch",
325
+ "name": "Enabled",
326
+ "icon": "mdi:keyboard-settings",
327
+ "entity_category": "config",
328
+ "state_topic": "evmqtt/pi/gpio-ir-recv-1a2b3c4d/switch/state",
329
+ "command_topic": "evmqtt/pi/gpio-ir-recv-1a2b3c4d/switch/set",
330
+ "payload_on": "ON",
331
+ "payload_off": "OFF",
332
+ "state_on": "ON",
333
+ "state_off": "OFF"
334
+ }
335
+ }
336
+ }
337
+ ```
338
+
339
+ `manufacturer` and `model` come from the USB descriptors in sysfs and are
340
+ left out when unknown, `model_id` is `vendor:product`. The gateway device
341
+ has a `Status` connectivity binary_sensor on `<base>/status`. Discovery needs
342
+ Home Assistant 2024.12 or later.
343
+
344
+ A device that is unplugged goes unavailable and keeps its entities; it comes
345
+ back when plugged in again.
346
+
347
+ ## Home Assistant
348
+
349
+ Each input device shows up as a device with `event.<device>_key` and
350
+ `switch.<device>_enabled`. Automation on a key:
351
+
352
+ ```yaml
353
+ automation:
354
+ - alias: "Remote volume up"
355
+ triggers:
356
+ - trigger: state
357
+ entity_id: event.gpio_ir_recv_key
358
+ conditions:
359
+ - condition: template
360
+ value_template: >
361
+ {{ trigger.to_state.attributes.event_type == 'press'
362
+ and trigger.to_state.attributes.key == 'KEY_VOLUMEUP' }}
363
+ actions:
364
+ - action: media_player.volume_up
365
+ target:
366
+ entity_id: media_player.living_room
367
+ ```
368
+
369
+ Node-RED and other MQTT consumers subscribe to `<base>/+/event` for the JSON
370
+ stream.
371
+
372
+ ## Upgrading from 1.x
373
+
374
+ 2.0 changes topics, entities, payloads and some config keys. Old entities
375
+ are removed automatically; automations on them have to be rewritten.
376
+
377
+ **Topics**
378
+
379
+ | 1.x | 2.0 |
380
+ |-----|-----|
381
+ | `<topic>/<slug>/state` | `<base>/<id>/event` |
382
+ | `<topic>/<slug>/config`, `homeassistant/switch/<uid>/config` | `homeassistant/device/evmqtt_<node>_<id>/config` |
383
+ | `<topic>/<slug>/switch/state`, `/switch/set` | `<base>/<id>/switch/state`, `/switch/set` |
384
+ | none | `<base>/status`, `<base>/<id>/availability` |
385
+
386
+ `<slug>` was the name slug (plus `-2` for duplicates, `eventN` in manual
387
+ mode); `<id>` is the stable id (name slug plus a hash), so topics no longer
388
+ move when `eventN` changes.
389
+
390
+ **Entities**
391
+
392
+ - `sensor.<name>_<device>` (last key as state) becomes `event.<device>_key`.
393
+ The key is in the `key` attribute, the state is the event time.
394
+ - `switch.<device>_enable` becomes `switch.<device>_enabled`, in the device's
395
+ configuration section.
396
+ - Every input device is its own HA device, linked to a new gateway device.
397
+
398
+ **Payload**
399
+
400
+ - New: `event_type` (lowercase key state), `modifiers` (list), `deviceId`.
401
+ - `key` is the plain key name. 1.x appended held modifiers
402
+ (`KEY_A_KEY_LEFTSHIFT`) and joined aliased names (`KEY_MIN_INTERESTING|KEY_MUTE`);
403
+ 2.0 sends `KEY_A` with `"modifiers": ["KEY_LEFTSHIFT"]`, and `KEY_MUTE`.
404
+ - `state`, `devicePath` and `deviceName` are unchanged.
405
+
406
+ **Config**
407
+
408
+ - `serverip`, `port`, `username`, `password`, `tls`, `tls_ca`: renamed to
409
+ `mqtt_host`, `mqtt_port`, `mqtt_username`, `mqtt_password`, `mqtt_tls`,
410
+ `mqtt_tls_ca`. The old names still work and log a warning.
411
+ - `topic`: deprecated. If it is under `discovery_prefix` (the 1.x default
412
+ `homeassistant/sensor/evmqtt`), it is ignored for state topics, which move
413
+ to `base_topic`. If it is elsewhere and `base_topic` is not set, it becomes
414
+ `base_topic`. In both cases it tells the cleanup where the 1.x discovery is.
415
+ - `filter_keys_only`: ignored. The default filter is stricter (keyboard-like,
416
+ no virtual devices); list anything else in `devices`.
417
+ - `devices` and `enabled_devices` accept ids and names as well as paths, and
418
+ `devices` no longer requires `auto_discover: false`.
419
+ - `auto_discover` now defaults to `true` in `config.json` too.
420
+ - Add-on: `mqtt_host` can be left empty to use the Mosquitto add-on.
421
+ - Enable/disable is now kept in a state file instead of the retained switch
422
+ topic; the first 2.0 start seeds it from `enabled_devices`.
423
+
424
+ **Automations**
425
+
426
+ - Replace `state` triggers on `sensor.*` with a `state` trigger on the
427
+ `event.*` entity and a condition on `trigger.to_state.attributes.key`
428
+ (see the example above). A `to:` on the key no longer works: the state of
429
+ an event entity is a timestamp.
430
+ - Keys with modifiers: check `attributes.modifiers` instead of matching
431
+ `KEY_A_KEY_LEFTSHIFT`.
432
+ - MQTT triggers and Node-RED flows: subscribe to `<base>/+/event`.
433
+ - Switches: update entity ids.
434
+
435
+ **Cleanup of old entities**
436
+
437
+ On the first connect evmqtt subscribes for a few seconds to
438
+ `<prefix>/+/+/config` and `<topic>/+/config`, and clears (empty retained
439
+ message) only configs whose `unique_id` starts with `evmqtt_` and whose
440
+ `state_topic` is under the 1.x topic, plus the retained 1.x switch state.
441
+ Home Assistant then removes the old sensor and switch entities. Nothing else
442
+ is touched: other integrations' configs, unparseable payloads and 2.0 device
443
+ configs are left alone. Set `cleanup_legacy: false` to skip it.
444
+
445
+ If several 1.x gateways shared one broker and topic, the first upgraded one
446
+ removes the 1.x entities of all of them; the others recreate theirs on their
447
+ next 1.x start. Upgrade them together, or set `cleanup_legacy: false` until
448
+ the last one is upgraded.
449
+
450
+ ## Core Library
451
+
452
+ `evmqtt.core` is the evdev-only asyncio layer the daemon runs on, usable
453
+ without MQTT (`pip install evmqtt`):
454
+
455
+ ```python
456
+ import asyncio
457
+ from evmqtt.core import DeviceReader, KeyState, is_keyboard_like, list_devices, open_device
458
+
459
+ async def main():
460
+ info = list_devices(is_keyboard_like)[0]
461
+ reader = DeviceReader(
462
+ open_device(info.path),
463
+ lambda e: e.state is KeyState.PRESS and print(e.key, e.modifiers),
464
+ info=info,
465
+ )
466
+ await reader.run()
467
+
468
+ asyncio.run(main())
469
+ ```
470
+
471
+ `info.id` is stable across reboots and eventN renumbering: name slug plus a
472
+ hash of bus, vendor, product, name and either the serial (uniq, plus the
473
+ interface number) when the device has a real one, so it survives a port
474
+ move, or the port path (phys) when it does not. The MQTT daemon keys its
475
+ topics and Home Assistant ids on it.
476
+
477
+ ## Development
478
+
479
+ ### Running Tests
480
+
481
+ ```bash
482
+ # Install dev dependencies
483
+ pip install -e ".[mqtt,dev]"
484
+
485
+ # Run tests (see tests/README.md for the broker and uinput tiers)
486
+ pytest -m "not broker and not uinput"
487
+
488
+ # Run with coverage
489
+ pytest tests/ -v --cov=evmqtt --cov-report=html
490
+ ```
491
+
492
+ ### Project Structure
493
+
494
+ ```
495
+ evmqtt/
496
+ ├── src/evmqtt/ # Main package
497
+ │ ├── __init__.py
498
+ │ ├── core/ # evdev-only asyncio library (no MQTT)
499
+ │ ├── __main__.py # CLI entry point
500
+ │ ├── config.py # Configuration
501
+ │ ├── gateway.py # Daemon: readers, hotplug, persistence, MQTT
502
+ │ ├── ha.py # Topics and HA discovery payloads
503
+ │ ├── mqtt_client.py # paho wrapper
504
+ │ ├── state.py # Enable state file
505
+ │ ├── supervisor.py # Add-on broker lookup
506
+ │ └── sysinfo.py # sysfs: virtual devices, vendor/model
507
+ ├── tests/ # Test suite
508
+ ├── config.yaml # HA add-on manifest
509
+ ├── repository.yaml # HA add-on repository manifest
510
+ ├── Dockerfile # Container build
511
+ ├── pyproject.toml # Python packaging
512
+ └── run.sh # Container entrypoint
513
+ ```
514
+
515
+ ### Type Checking
516
+
517
+ ```bash
518
+ mypy src/evmqtt/core
519
+ ```
520
+
521
+ ### Linting
522
+
523
+ ```bash
524
+ ruff check src/ tests/
525
+ ruff format src/ tests/
526
+ ```
527
+
528
+ ## Requirements
529
+
530
+ - Python 3.10+
531
+ - evdev >= 1.6.0
532
+ - paho-mqtt >= 2.0.0 for the daemon (`evmqtt[mqtt]`)
533
+ - Linux with input device access
534
+
535
+ ## Troubleshooting
536
+
537
+ ### Permission Denied for Input Device
538
+
539
+ Add your user to the `input` group:
540
+
541
+ ```bash
542
+ sudo usermod -a -G input $USER
543
+ # Log out and back in
544
+ ```
545
+
546
+ Or run with sudo (not recommended for production).
547
+
548
+ ### Device Not Found
549
+
550
+ 1. Check the device exists: `ls -la /dev/input/`
551
+ 2. Verify permissions: `groups` should include `input`
552
+ 3. For Docker/add-on, ensure the device is passed through
553
+
554
+ ### MQTT Connection Failed
555
+
556
+ evmqtt logs `MQTT broker ... unreachable` or `refused the connection` and
557
+ keeps retrying.
558
+
559
+ 1. Verify `mqtt_host` and `mqtt_port`
560
+ 2. Check username/password (`refused ... Not authorized`)
561
+ 3. Check the broker: `mosquitto_sub -h <broker> -t 'evmqtt/#' -v`
562
+
563
+ ### Devices Not Appearing in Home Assistant
564
+
565
+ 1. Check MQTT discovery is enabled in Home Assistant and `discovery_prefix` matches it
566
+ 2. Check the device is selected: `evmqtt --list-devices`, and the log at startup
567
+ 3. Look in **Settings** → **Devices & Services** → **MQTT** → **Devices**
568
+
569
+ ## License
570
+
571
+ MIT License - see LICENSE file for details.
572
+
573
+ ## Credits
574
+
575
+ - Original concept by [James Bulpin](https://gist.github.com/jamesbulpin/b940e7d81e2e65158f12e59b4d6a0c3c)
576
+ - [python-evdev](https://python-evdev.readthedocs.io/) for input device access
577
+ - [paho-mqtt](https://eclipse.dev/paho/index.php?page=clients/python/index.php) for MQTT client