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.
- evmqtt-2.0.0/LICENSE +21 -0
- evmqtt-2.0.0/PKG-INFO +577 -0
- evmqtt-2.0.0/README.md +543 -0
- evmqtt-2.0.0/pyproject.toml +89 -0
- evmqtt-2.0.0/setup.cfg +4 -0
- evmqtt-2.0.0/src/evmqtt/__init__.py +39 -0
- evmqtt-2.0.0/src/evmqtt/__main__.py +163 -0
- evmqtt-2.0.0/src/evmqtt/config.py +304 -0
- evmqtt-2.0.0/src/evmqtt/core/__init__.py +65 -0
- evmqtt-2.0.0/src/evmqtt/core/devices.py +238 -0
- evmqtt-2.0.0/src/evmqtt/core/events.py +59 -0
- evmqtt-2.0.0/src/evmqtt/core/keys.py +104 -0
- evmqtt-2.0.0/src/evmqtt/core/reader.py +222 -0
- evmqtt-2.0.0/src/evmqtt/core/watcher.py +67 -0
- evmqtt-2.0.0/src/evmqtt/gateway.py +435 -0
- evmqtt-2.0.0/src/evmqtt/ha.py +227 -0
- evmqtt-2.0.0/src/evmqtt/mqtt_client.py +232 -0
- evmqtt-2.0.0/src/evmqtt/state.py +79 -0
- evmqtt-2.0.0/src/evmqtt/supervisor.py +118 -0
- evmqtt-2.0.0/src/evmqtt/sysinfo.py +61 -0
- evmqtt-2.0.0/src/evmqtt.egg-info/PKG-INFO +577 -0
- evmqtt-2.0.0/src/evmqtt.egg-info/SOURCES.txt +32 -0
- evmqtt-2.0.0/src/evmqtt.egg-info/dependency_links.txt +1 -0
- evmqtt-2.0.0/src/evmqtt.egg-info/entry_points.txt +2 -0
- evmqtt-2.0.0/src/evmqtt.egg-info/requires.txt +11 -0
- evmqtt-2.0.0/src/evmqtt.egg-info/top_level.txt +1 -0
- evmqtt-2.0.0/tests/test_cli.py +160 -0
- evmqtt-2.0.0/tests/test_config.py +230 -0
- evmqtt-2.0.0/tests/test_gateway.py +769 -0
- evmqtt-2.0.0/tests/test_mqtt_wrapper.py +132 -0
- evmqtt-2.0.0/tests/test_package.py +25 -0
- evmqtt-2.0.0/tests/test_slugs.py +24 -0
- evmqtt-2.0.0/tests/test_supervisor.py +184 -0
- 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
|
+
[](https://www.python.org/downloads/)
|
|
38
|
+
[](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
|