dbus2mqtt 0.4.0__tar.gz → 0.4.1__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.
Potentially problematic release.
This version of dbus2mqtt might be problematic. Click here for more details.
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/release-drafter.yml +2 -2
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.pre-commit-config.yaml +2 -2
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/PKG-INFO +14 -4
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/README.md +13 -3
- dbus2mqtt-0.4.1/docs/examples/bluez.md +37 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/bluez.yaml +25 -5
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/pyproject.toml +4 -2
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/dbus_client.py +43 -42
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/mqtt/mqtt_client.py +1 -1
- dbus2mqtt-0.4.1/tests/conftest.py +16 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/dbus/test_dbus_client.py +1 -1
- dbus2mqtt-0.4.1/tests/dbus/test_dbus_client_mqtt_command.py +206 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/uv.lock +4 -4
- dbus2mqtt-0.4.0/docs/examples/bluez.md +0 -19
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.dockerignore +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.env.example +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/scripts/release-versions.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/ci.yml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/docker-dev.yml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/docker-stable.yml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/pre-commit.yml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/publish.yml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/release-drafter.yml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.gitignore +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.python-version +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.vscode/launch.json +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.vscode/settings.json +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.yamllint.yml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/LICENSE +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docker/Dockerfile.dev +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docker/Dockerfile.pypi +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/debugging.md +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/dbus2mqtt_internal_state.md +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/dbus2mqtt_internal_state.yaml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/home_assistant_media_player.md +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/home_assistant_media_player.yaml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/linux_desktop.md +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/linux_desktop.yaml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples.md +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/flows.md +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/renovate.json +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/__init__.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/__main__.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/config/__init__.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/config/jsonarparse.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/dbus_types.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/dbus_util.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/introspection_patches/mpris_playerctl.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/introspection_patches/mpris_vlc.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/event_broker.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/flow/__init__.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/flow/actions/context_set.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/flow/actions/mqtt_publish.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/flow/flow_processor.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/main.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/template/dbus_template_functions.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/template/templating.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/__init__.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/fixtures/payload_template_jinja_expressions.yaml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/fixtures/payload_template_off.yaml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/fixtures/schedule_cron_trigger.yaml +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/test_config.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/test_examples.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/flow/actions/test_context_set.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/flow/actions/test_mqtt_publish.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/flow/test_flow_processor.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/flow/triggers/test_dbus_client_triggers.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/template/test_templating.py +0 -0
- {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/template/test_templating_config.py +0 -0
|
@@ -38,12 +38,12 @@ version-resolver:
|
|
|
38
38
|
minor:
|
|
39
39
|
labels:
|
|
40
40
|
- minor
|
|
41
|
-
- feature
|
|
42
|
-
- enhancement
|
|
43
41
|
- breaking-change # minor until we are on 1.x
|
|
44
42
|
patch:
|
|
45
43
|
labels:
|
|
46
44
|
- patch
|
|
45
|
+
- feature # patch until we are on 1.x
|
|
46
|
+
- enhancement # patch until we are on 1.x
|
|
47
47
|
default: patch
|
|
48
48
|
change-template: '* $TITLE (#$NUMBER)'
|
|
49
49
|
template: |
|
|
@@ -33,7 +33,7 @@ repos:
|
|
|
33
33
|
- --strict
|
|
34
34
|
|
|
35
35
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
36
|
-
rev: v0.
|
|
36
|
+
rev: v0.12.1
|
|
37
37
|
hooks:
|
|
38
38
|
- id: ruff
|
|
39
39
|
args:
|
|
@@ -42,7 +42,7 @@ repos:
|
|
|
42
42
|
- I
|
|
43
43
|
|
|
44
44
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
45
|
-
rev: 0.7.
|
|
45
|
+
rev: 0.7.16
|
|
46
46
|
hooks:
|
|
47
47
|
- id: uv-lock
|
|
48
48
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbus2mqtt
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A Python tool to expose Linux D-Bus signals, methods and properties over MQTT - featuring templating, payload enrichment and Home Assistant-ready examples
|
|
5
5
|
Project-URL: Repository, https://github.com/jwnmulder/dbus2mqtt.git
|
|
6
6
|
Project-URL: Issues, https://github.com/jwnmulder/dbus2mqtt/issues
|
|
@@ -173,21 +173,31 @@ dbus:
|
|
|
173
173
|
|
|
174
174
|
This configuration will expose 2 methods. Triggering methods can be done by publishing json messages to the `dbus2mqtt/org.mpris.MediaPlayer2/command` MQTT topic. Arguments can be passed along in `args`.
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
Some examples that call methods on **all** bus_names matching the configured pattern
|
|
177
177
|
|
|
178
178
|
```json
|
|
179
179
|
{
|
|
180
|
-
"method"
|
|
180
|
+
"method": "Play",
|
|
181
181
|
}
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
```json
|
|
185
185
|
{
|
|
186
|
-
"method"
|
|
186
|
+
"method": "OpenUri",
|
|
187
187
|
"args": []
|
|
188
188
|
}
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
To specifically target objects the properties `bus_name` and/or `path` can be used. Both properties support wildcards
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"method": "Play",
|
|
196
|
+
"bus_name": "*.firefox",
|
|
197
|
+
"path": "/org/mpris/MediaPlayer2"
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
191
201
|
### Exposing dbus signals
|
|
192
202
|
|
|
193
203
|
Publishing signals to MQTT topics works by subscribing to the relevant signal and using flows for publishing
|
|
@@ -141,21 +141,31 @@ dbus:
|
|
|
141
141
|
|
|
142
142
|
This configuration will expose 2 methods. Triggering methods can be done by publishing json messages to the `dbus2mqtt/org.mpris.MediaPlayer2/command` MQTT topic. Arguments can be passed along in `args`.
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
Some examples that call methods on **all** bus_names matching the configured pattern
|
|
145
145
|
|
|
146
146
|
```json
|
|
147
147
|
{
|
|
148
|
-
"method"
|
|
148
|
+
"method": "Play",
|
|
149
149
|
}
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
```json
|
|
153
153
|
{
|
|
154
|
-
"method"
|
|
154
|
+
"method": "OpenUri",
|
|
155
155
|
"args": []
|
|
156
156
|
}
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
To specifically target objects the properties `bus_name` and/or `path` can be used. Both properties support wildcards
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"method": "Play",
|
|
164
|
+
"bus_name": "*.firefox",
|
|
165
|
+
"path": "/org/mpris/MediaPlayer2"
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
159
169
|
### Exposing dbus signals
|
|
160
170
|
|
|
161
171
|
Publishing signals to MQTT topics works by subscribing to the relevant signal and using flows for publishing
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Bluez
|
|
2
|
+
|
|
3
|
+
This configuration file demonstrates how to use dbus2mqtt to bridge D-Bus events from BlueZ (the official Linux Bluetooth protocol stack) to MQTT topics. It subscribes to relevant D-Bus signals and properties for both the Bluetooth adapter (`hci0`) and all Bluetooth devices managed by BlueZ. The configuration defines flows that:
|
|
4
|
+
|
|
5
|
+
* Monitor property changes and object lifecycle events (added/removed) for the Bluetooth adapter and devices.
|
|
6
|
+
* Retrieve the current state of the adapter or device using the `GetAll` method from the `org.freedesktop.DBus.Properties` interface.
|
|
7
|
+
* Publish the retrieved state as JSON payloads to structured MQTT topics, enabling real-time monitoring and integration with home automation or IoT systems.
|
|
8
|
+
|
|
9
|
+
This setup allows MQTT clients to receive updates about Bluetooth adapter and device states, as well as notifications when devices are removed, making it easier to integrate Bluetooth events into broader automation workflows.
|
|
10
|
+
|
|
11
|
+
Configuration activities
|
|
12
|
+
|
|
13
|
+
* dbus2mqtt setup using the supplied [bluez.yaml](https://github.com/jwnmulder/dbus2mqtt/blob/main/docs/examples/bluez.yaml)
|
|
14
|
+
|
|
15
|
+
Execute the following command to run dbus2mqtt with the example configuration in this repository.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv run dbus2mqtt --config docs/examples/bluez.yaml
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
The following table lists commands, their descriptions, and an example JSON payload for invoking them via MQTT.
|
|
24
|
+
|
|
25
|
+
Dbus methods can be invoked by sendig the JSON payload to MQTT topic `dbus2mqtt/bluez/hci0/command`. Method calls will be done for all matching dbus objects.
|
|
26
|
+
|
|
27
|
+
| Interface | Method<br />Property | Description | Example MQTT JSON Payload |
|
|
28
|
+
|----------------------|-----------------------|--------------------------------------|-------------------------------------------------|
|
|
29
|
+
| `org.bluez.Adapter1` | `StartDiscovery` | Starts bluetooth discovery | `{ "method": "StartDiscovery" }` |
|
|
30
|
+
| `org.bluez.Adapter1` | `StopDiscovery` | Stops bluetooth discovery | `{ "method": "StopDiscovery" }` |
|
|
31
|
+
| `org.bluez.Device1` | `Connect` | | `{ "method": "Connect", "path": "/org/bluez/hci0/dev_A1_A2_A3_A4_A5_A6" }` |
|
|
32
|
+
| `org.bluez.Device1` | `Disconnect` | | `{ "method": "Disconnect", "path": "/org/bluez/hci0/dev_A1_A2_A3_A4_A5_A6" }` |
|
|
33
|
+
| `org.bluez.Device1` | `Pair` | | `{ "method": "Pair", "path": "/org/bluez/hci0/dev_A1_A2_A3_A4_A5_A6" }` |
|
|
34
|
+
| `org.bluez.Device1` | `CancelPairing` | | `{ "method": "CancelPairing", "path": "/org/bluez/hci0/dev_A1_A2_A3_A4_A5_A6" }` |
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
https://manpages.ubuntu.com/manpages/noble/man5/org.bluez.
|
|
@@ -5,12 +5,18 @@ dbus:
|
|
|
5
5
|
- bus_name: org.bluez
|
|
6
6
|
path: /org/bluez/hci0
|
|
7
7
|
|
|
8
|
+
# https://manpages.ubuntu.com/manpages/oracular/man5/org.bluez.Adapter.5.html
|
|
9
|
+
|
|
8
10
|
interfaces:
|
|
9
11
|
- interface: org.freedesktop.DBus.Properties
|
|
10
12
|
signals:
|
|
11
13
|
- signal: PropertiesChanged
|
|
14
|
+
|
|
15
|
+
- interface: org.bluez.Adapter1
|
|
16
|
+
mqtt_command_topic: dbus2mqtt/bluez/hci0/command
|
|
12
17
|
methods:
|
|
13
|
-
- method:
|
|
18
|
+
- method: StartDiscovery
|
|
19
|
+
- method: StopDiscovery
|
|
14
20
|
|
|
15
21
|
flows:
|
|
16
22
|
- name: "publish adapter state"
|
|
@@ -27,18 +33,28 @@ dbus:
|
|
|
27
33
|
- type: mqtt_publish
|
|
28
34
|
topic: dbus2mqtt/bluez/hci0
|
|
29
35
|
payload_type: json
|
|
30
|
-
payload_template:
|
|
31
|
-
|
|
36
|
+
payload_template: |
|
|
37
|
+
{{
|
|
38
|
+
{ 'dbus_object_path': path }
|
|
39
|
+
| combine(adapter_properties)
|
|
40
|
+
}}
|
|
32
41
|
|
|
33
42
|
- bus_name: org.bluez
|
|
34
43
|
path: /org/bluez/hci0/dev_*
|
|
35
44
|
|
|
45
|
+
# https://manpages.ubuntu.com/manpages/noble/man5/org.bluez.Device.5.html
|
|
36
46
|
interfaces:
|
|
37
47
|
- interface: org.freedesktop.DBus.Properties
|
|
38
48
|
signals:
|
|
39
49
|
- signal: PropertiesChanged
|
|
50
|
+
|
|
51
|
+
- interface: org.bluez.Device1
|
|
52
|
+
mqtt_command_topic: dbus2mqtt/bluez/hci0/command
|
|
40
53
|
methods:
|
|
41
|
-
- method:
|
|
54
|
+
- method: Connect
|
|
55
|
+
- method: Disconnect
|
|
56
|
+
- method: Pair
|
|
57
|
+
- method: CancelPairing
|
|
42
58
|
|
|
43
59
|
flows:
|
|
44
60
|
- name: "publish device state"
|
|
@@ -55,7 +71,11 @@ dbus:
|
|
|
55
71
|
- type: mqtt_publish
|
|
56
72
|
topic: dbus2mqtt/bluez/{{ path | replace('/org/bluez/', '') }}
|
|
57
73
|
payload_type: json
|
|
58
|
-
payload_template:
|
|
74
|
+
payload_template: |
|
|
75
|
+
{{
|
|
76
|
+
{ 'dbus_object_path': path }
|
|
77
|
+
| combine(device_properties)
|
|
78
|
+
}}
|
|
59
79
|
- name: "device removed"
|
|
60
80
|
triggers:
|
|
61
81
|
- type: object_removed
|
|
@@ -60,7 +60,7 @@ dev = [
|
|
|
60
60
|
"pyright>=1.1.396",
|
|
61
61
|
"pre-commit>=4.2.0",
|
|
62
62
|
"pip>=25.0.1",
|
|
63
|
-
"pytest-asyncio>=0.
|
|
63
|
+
"pytest-asyncio>=1.0.0"
|
|
64
64
|
]
|
|
65
65
|
|
|
66
66
|
[project.urls]
|
|
@@ -81,7 +81,9 @@ lint-pyright = "pyright"
|
|
|
81
81
|
addopts = "-s"
|
|
82
82
|
testpaths = ["tests"]
|
|
83
83
|
pythonpath = "."
|
|
84
|
-
asyncio_default_fixture_loop_scope = "
|
|
84
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
85
|
+
asyncio_default_test_loop_scope = "function"
|
|
86
|
+
log_cli = true
|
|
85
87
|
|
|
86
88
|
[tool.pyright]
|
|
87
89
|
venvPath = "."
|
|
@@ -745,12 +745,13 @@ class DbusClient:
|
|
|
745
745
|
path = message.body[0]
|
|
746
746
|
await self._handle_interfaces_removed(bus_name, path)
|
|
747
747
|
|
|
748
|
-
|
|
749
748
|
async def _on_mqtt_msg(self, msg: MqttMessage):
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
749
|
+
"""Executes dbus method calls or property updates on objects when messages have
|
|
750
|
+
1. a matching subscription configured
|
|
751
|
+
2. a matching method
|
|
752
|
+
3. a matching bus_name (if provided)
|
|
753
|
+
4. a matching path (if provided)
|
|
754
|
+
"""
|
|
754
755
|
|
|
755
756
|
found_matching_topic = False
|
|
756
757
|
for subscription_configs in self.config.subscriptions:
|
|
@@ -766,6 +767,9 @@ class DbusClient:
|
|
|
766
767
|
matched_method = False
|
|
767
768
|
matched_property = False
|
|
768
769
|
|
|
770
|
+
payload_bus_name = msg.payload.get("bus_name") or "*"
|
|
771
|
+
payload_path = msg.payload.get("path") or "*"
|
|
772
|
+
|
|
769
773
|
payload_method = msg.payload.get("method")
|
|
770
774
|
payload_method_args = msg.payload.get("args") or []
|
|
771
775
|
|
|
@@ -773,47 +777,44 @@ class DbusClient:
|
|
|
773
777
|
payload_value = msg.payload.get("value")
|
|
774
778
|
|
|
775
779
|
if payload_method is None and (payload_property is None or payload_value is None):
|
|
776
|
-
|
|
780
|
+
if msg.payload:
|
|
781
|
+
logger.info(f"on_mqtt_msg: Unsupported payload, missing 'method' or 'property/value', got method={payload_method}, property={payload_property}, value={payload_value} from {msg.payload}")
|
|
777
782
|
return
|
|
778
783
|
|
|
779
784
|
for [bus_name, bus_name_subscription] in self.subscriptions.items():
|
|
780
|
-
|
|
781
|
-
for
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
785
|
+
if fnmatch.fnmatchcase(bus_name, payload_bus_name):
|
|
786
|
+
for [path, proxy_object] in bus_name_subscription.path_objects.items():
|
|
787
|
+
if fnmatch.fnmatchcase(path, payload_path):
|
|
788
|
+
for subscription_configs in self.config.get_subscription_configs(bus_name=bus_name, path=path):
|
|
789
|
+
for interface_config in subscription_configs.interfaces:
|
|
790
|
+
|
|
791
|
+
for method in interface_config.methods:
|
|
792
|
+
|
|
793
|
+
# filter configured method, configured topic, ...
|
|
794
|
+
if method.method == payload_method:
|
|
795
|
+
interface = proxy_object.get_interface(name=interface_config.interface)
|
|
796
|
+
matched_method = True
|
|
797
|
+
|
|
798
|
+
try:
|
|
799
|
+
logger.info(f"on_mqtt_msg: method={method.method}, args={payload_method_args}, bus_name={bus_name}, path={path}, interface={interface_config.interface}")
|
|
800
|
+
await self.call_dbus_interface_method(interface, method.method, payload_method_args)
|
|
801
|
+
except Exception as e:
|
|
802
|
+
logger.warning(f"on_mqtt_msg: method={method.method}, args={payload_method_args}, bus_name={bus_name} failed, exception={e}")
|
|
803
|
+
|
|
804
|
+
for property in interface_config.properties:
|
|
805
|
+
# filter configured property, configured topic, ...
|
|
806
|
+
if property.property == payload_property:
|
|
807
|
+
interface = proxy_object.get_interface(name=interface_config.interface)
|
|
808
|
+
matched_property = True
|
|
809
|
+
|
|
810
|
+
try:
|
|
811
|
+
logger.info(f"on_mqtt_msg: property={property.property}, value={payload_value}, bus_name={bus_name}, path={path}, interface={interface_config.interface}")
|
|
812
|
+
await self.set_dbus_interface_property(interface, property.property, payload_value)
|
|
813
|
+
except Exception as e:
|
|
814
|
+
logger.warning(f"on_mqtt_msg: property={property.property}, value={payload_value}, bus_name={bus_name} failed, exception={e}")
|
|
808
815
|
|
|
809
816
|
if not matched_method and not matched_property:
|
|
810
817
|
if payload_method:
|
|
811
|
-
logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, method={payload_method}, active bus_names={list(self.subscriptions.keys())}")
|
|
818
|
+
logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, method={payload_method}, bus_name={payload_bus_name}, path={payload_path or '*'}, active bus_names={list(self.subscriptions.keys())}")
|
|
812
819
|
if payload_property:
|
|
813
|
-
logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, property={payload_property}, active bus_names={list(self.subscriptions.keys())}")
|
|
814
|
-
|
|
815
|
-
# raw mode, payload contains: bus_name (specific or wildcard), path, interface_name
|
|
816
|
-
# topic: dbus2mqtt/raw (with allowlist check)
|
|
817
|
-
|
|
818
|
-
# predefined mode with topic matching from configuration
|
|
819
|
-
# topic: dbus2mqtt/MediaPlayer/command
|
|
820
|
+
logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, property={payload_property}, bus_name={payload_bus_name}, path={payload_path or '*'}, active bus_names={list(self.subscriptions.keys())}")
|
|
@@ -117,7 +117,7 @@ class MqttClient:
|
|
|
117
117
|
return
|
|
118
118
|
|
|
119
119
|
try:
|
|
120
|
-
json_payload = json.loads(payload)
|
|
120
|
+
json_payload = json.loads(payload) if payload else {}
|
|
121
121
|
logger.debug(f"on_message: msg.topic={msg.topic}, msg.payload={json.dumps(json_payload)}")
|
|
122
122
|
self.event_broker.on_mqtt_receive(MqttMessage(msg.topic, json_payload))
|
|
123
123
|
except json.JSONDecodeError as e:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def pytest_runtest_logstart(nodeid, location):
|
|
5
|
+
# Switch to INFO during setup
|
|
6
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
7
|
+
|
|
8
|
+
def pytest_runtest_call(item):
|
|
9
|
+
# Enable live logs during the call phase
|
|
10
|
+
|
|
11
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
12
|
+
logging.getLogger("tzlocal").setLevel(logging.INFO)
|
|
13
|
+
|
|
14
|
+
def pytest_runtest_teardown(item, nextitem):
|
|
15
|
+
# Back to INFO during teardown
|
|
16
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
@@ -50,4 +50,4 @@ async def test_signal_handler_unwrap_args():
|
|
|
50
50
|
|
|
51
51
|
# message args should be unwrapped
|
|
52
52
|
assert mqtt_message is not None
|
|
53
|
-
assert mqtt_message.args == ["org.mpris.MediaPlayer2.Player", {"CanPause"
|
|
53
|
+
assert mqtt_message.args == ["org.mpris.MediaPlayer2.Player", {"CanPause": True}, []]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import dbus2mqtt.config as config
|
|
6
|
+
|
|
7
|
+
from dbus2mqtt import AppContext
|
|
8
|
+
from dbus2mqtt.dbus.dbus_client import DbusClient
|
|
9
|
+
from dbus2mqtt.dbus.dbus_types import BusNameSubscriptions
|
|
10
|
+
from dbus2mqtt.event_broker import MqttMessage
|
|
11
|
+
from tests import mocked_app_context, mocked_dbus_client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
async def test_method_only():
|
|
16
|
+
""" Mock contains 3 bus objects, test with valid method.
|
|
17
|
+
Expect the method to be called 2 times, once for each bus object with matching subscription
|
|
18
|
+
"""
|
|
19
|
+
mocked_proxy_interface = await _publish_msg(
|
|
20
|
+
MqttMessage(
|
|
21
|
+
topic="dbus2mqtt/test/command",
|
|
22
|
+
payload={
|
|
23
|
+
"method": "TestMethod2",
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
assert mocked_proxy_interface.call_test_method2.call_count == 2
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
async def test_invalid_method():
|
|
32
|
+
""" Mock contains 3 bus objects, test with invalid method.
|
|
33
|
+
Expect the method to be called zero times
|
|
34
|
+
"""
|
|
35
|
+
mocked_proxy_interface = await _publish_msg(
|
|
36
|
+
MqttMessage(
|
|
37
|
+
topic="dbus2mqtt/test/command",
|
|
38
|
+
payload={
|
|
39
|
+
"method": "InvalidTestMethod",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert mocked_proxy_interface.call_invalid_test_method.call_count == 0
|
|
45
|
+
|
|
46
|
+
@pytest.mark.asyncio
|
|
47
|
+
async def test_method_with_bus_name():
|
|
48
|
+
""" Mock contains 3 bus objects, test with valid method and valid bus_name.
|
|
49
|
+
Expect the method to be called 1 time, once for each matching bus name and subscription
|
|
50
|
+
"""
|
|
51
|
+
mocked_proxy_interface = await _publish_msg(
|
|
52
|
+
MqttMessage(
|
|
53
|
+
topic="dbus2mqtt/test/command",
|
|
54
|
+
payload={
|
|
55
|
+
"method": "TestMethod2",
|
|
56
|
+
"bus_name": "org.mpris.MediaPlayer2.vlc"
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
assert mocked_proxy_interface.call_test_method2.call_count == 1
|
|
62
|
+
|
|
63
|
+
@pytest.mark.asyncio
|
|
64
|
+
async def test_method_with_bus_name_pattern():
|
|
65
|
+
""" Mock contains 3 bus objects, test with valid method and valid bus_name.
|
|
66
|
+
Expect the method to be called 1 time, once for each matching bus name and subscription
|
|
67
|
+
"""
|
|
68
|
+
mocked_proxy_interface = await _publish_msg(
|
|
69
|
+
MqttMessage(
|
|
70
|
+
topic="dbus2mqtt/test/command",
|
|
71
|
+
payload={
|
|
72
|
+
"method": "TestMethod2",
|
|
73
|
+
"bus_name": "*.vlc"
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert mocked_proxy_interface.call_test_method2.call_count == 1
|
|
79
|
+
|
|
80
|
+
@pytest.mark.asyncio
|
|
81
|
+
async def test_method_invalid_bus_name():
|
|
82
|
+
""" Mock contains 3 bus objects, test with valid method and valid bus_name.
|
|
83
|
+
Expect the method to be called zero times
|
|
84
|
+
"""
|
|
85
|
+
mocked_proxy_interface = await _publish_msg(
|
|
86
|
+
MqttMessage(
|
|
87
|
+
topic="dbus2mqtt/test/command",
|
|
88
|
+
payload={
|
|
89
|
+
"method": "TestMethod2",
|
|
90
|
+
"bus_name": "org.mpris.MediaPlayer2.non-existing"
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert mocked_proxy_interface.call_test_method2.call_count == 0
|
|
96
|
+
|
|
97
|
+
@pytest.mark.asyncio
|
|
98
|
+
async def test_method_with_path():
|
|
99
|
+
""" Mock contains 3 bus objects, test with valid method and path.
|
|
100
|
+
Expect the method to be called 2 times, once for each bus name with matching path and subscription
|
|
101
|
+
"""
|
|
102
|
+
mocked_proxy_interface = await _publish_msg(
|
|
103
|
+
MqttMessage(
|
|
104
|
+
topic="dbus2mqtt/test/command",
|
|
105
|
+
payload={
|
|
106
|
+
"method": "TestMethod2",
|
|
107
|
+
"path": "/org/mpris/MediaPlayer2"
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
assert mocked_proxy_interface.call_test_method2.call_count == 2
|
|
113
|
+
|
|
114
|
+
@pytest.mark.asyncio
|
|
115
|
+
async def test_method_with_path_pattern():
|
|
116
|
+
""" Mock contains 3 bus objects, test with valid method and path.
|
|
117
|
+
Expect the method to be called 2 times, once for each bus name with matching path and subscription
|
|
118
|
+
"""
|
|
119
|
+
mocked_proxy_interface = await _publish_msg(
|
|
120
|
+
MqttMessage(
|
|
121
|
+
topic="dbus2mqtt/test/command",
|
|
122
|
+
payload={
|
|
123
|
+
"method": "TestMethod2",
|
|
124
|
+
"path": "*/MediaPlayer2"
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
assert mocked_proxy_interface.call_test_method2.call_count == 2
|
|
130
|
+
|
|
131
|
+
@pytest.mark.asyncio
|
|
132
|
+
async def test_method_invalid_path():
|
|
133
|
+
""" Mock contains 3 bus objects, test with valid method and invalid path.
|
|
134
|
+
Expect the method to be called zero times
|
|
135
|
+
"""
|
|
136
|
+
mocked_proxy_interface = await _publish_msg(
|
|
137
|
+
MqttMessage(
|
|
138
|
+
topic="dbus2mqtt/test/command",
|
|
139
|
+
payload={
|
|
140
|
+
"method": "TestMethod2",
|
|
141
|
+
"path": "/invalid/path/to/object"
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
assert mocked_proxy_interface.call_test_method2.call_count == 0
|
|
147
|
+
|
|
148
|
+
async def _publish_msg(msg: MqttMessage):
|
|
149
|
+
|
|
150
|
+
app_context = _mocked_app_context()
|
|
151
|
+
dbus_client, proxy_interface = _mocked_dbus_client(app_context)
|
|
152
|
+
|
|
153
|
+
await dbus_client._on_mqtt_msg(msg)
|
|
154
|
+
|
|
155
|
+
return proxy_interface
|
|
156
|
+
|
|
157
|
+
def _mocked_app_context() -> AppContext:
|
|
158
|
+
app_context = mocked_app_context()
|
|
159
|
+
|
|
160
|
+
app_context.config.dbus.subscriptions = [
|
|
161
|
+
config.SubscriptionConfig(
|
|
162
|
+
bus_name="org.mpris.MediaPlayer2.*",
|
|
163
|
+
path="/org/mpris/MediaPlayer2",
|
|
164
|
+
interfaces=[
|
|
165
|
+
config.InterfaceConfig(
|
|
166
|
+
interface="test-interface-name",
|
|
167
|
+
mqtt_command_topic="dbus2mqtt/test/command",
|
|
168
|
+
methods=[
|
|
169
|
+
config.MethodConfig(method="TestMethod1"),
|
|
170
|
+
config.MethodConfig(method="TestMethod2")
|
|
171
|
+
]
|
|
172
|
+
)
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
)
|
|
176
|
+
]
|
|
177
|
+
return app_context
|
|
178
|
+
|
|
179
|
+
def _mocked_dbus_client(app_context: AppContext) -> tuple[DbusClient, MagicMock]:
|
|
180
|
+
|
|
181
|
+
dbus_objects = [
|
|
182
|
+
("org.mpris.MediaPlayer2.vlc", "/org/mpris/MediaPlayer2"),
|
|
183
|
+
("org.mpris.MediaPlayer2.firefox", "/org/mpris/MediaPlayer2"),
|
|
184
|
+
("org.mpris.MediaPlayer2.kodi", "/another/path/to/object"),
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
dbus_client = mocked_dbus_client(app_context)
|
|
188
|
+
|
|
189
|
+
mocked_proxy_interface = MagicMock()
|
|
190
|
+
mocked_proxy_interface.call_test_method1 = AsyncMock()
|
|
191
|
+
mocked_proxy_interface.call_test_method2 = AsyncMock()
|
|
192
|
+
mocked_proxy_interface.call_invalid_test_method = AsyncMock()
|
|
193
|
+
|
|
194
|
+
index = 1
|
|
195
|
+
for bus_name, path in dbus_objects:
|
|
196
|
+
|
|
197
|
+
dbus_client.subscriptions[bus_name] = BusNameSubscriptions(bus_name, f":1:{index}")
|
|
198
|
+
|
|
199
|
+
mocked_proxy_object = MagicMock()
|
|
200
|
+
mocked_proxy_object.get_interface.return_value = mocked_proxy_interface
|
|
201
|
+
|
|
202
|
+
dbus_client.subscriptions[bus_name].path_objects[path] = mocked_proxy_object
|
|
203
|
+
|
|
204
|
+
index += 1
|
|
205
|
+
|
|
206
|
+
return dbus_client, mocked_proxy_interface
|
|
@@ -144,7 +144,7 @@ dev = [
|
|
|
144
144
|
{ name = "pre-commit", specifier = ">=4.2.0" },
|
|
145
145
|
{ name = "pyright", specifier = ">=1.1.396" },
|
|
146
146
|
{ name = "pytest", specifier = ">=8.3.5" },
|
|
147
|
-
{ name = "pytest-asyncio", specifier = ">=0.
|
|
147
|
+
{ name = "pytest-asyncio", specifier = ">=1.0.0" },
|
|
148
148
|
{ name = "ruff", specifier = ">=0.11.2" },
|
|
149
149
|
{ name = "taskipy", specifier = ">=1.14.1" },
|
|
150
150
|
{ name = "types-pyyaml", specifier = ">=6.0.12.20241230" },
|
|
@@ -527,14 +527,14 @@ wheels = [
|
|
|
527
527
|
|
|
528
528
|
[[package]]
|
|
529
529
|
name = "pytest-asyncio"
|
|
530
|
-
version = "0.
|
|
530
|
+
version = "1.0.0"
|
|
531
531
|
source = { registry = "https://pypi.org/simple" }
|
|
532
532
|
dependencies = [
|
|
533
533
|
{ name = "pytest" },
|
|
534
534
|
]
|
|
535
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
535
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" }
|
|
536
536
|
wheels = [
|
|
537
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
537
|
+
{ url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" },
|
|
538
538
|
]
|
|
539
539
|
|
|
540
540
|
[[package]]
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# Bluez
|
|
2
|
-
|
|
3
|
-
This configuration file demonstrates how to use dbus2mqtt to bridge D-Bus events from BlueZ (the official Linux Bluetooth protocol stack) to MQTT topics. It subscribes to relevant D-Bus signals and properties for both the Bluetooth adapter (`hci0`) and all Bluetooth devices managed by BlueZ. The configuration defines flows that:
|
|
4
|
-
|
|
5
|
-
* Monitor property changes and object lifecycle events (added/removed) for the Bluetooth adapter and devices.
|
|
6
|
-
* Retrieve the current state of the adapter or device using the `GetAll` method from the `org.freedesktop.DBus.Properties` interface.
|
|
7
|
-
* Publish the retrieved state as JSON payloads to structured MQTT topics, enabling real-time monitoring and integration with home automation or IoT systems.
|
|
8
|
-
|
|
9
|
-
This setup allows MQTT clients to receive updates about Bluetooth adapter and device states, as well as notifications when devices are removed, making it easier to integrate Bluetooth events into broader automation workflows.
|
|
10
|
-
|
|
11
|
-
Configuration activities
|
|
12
|
-
|
|
13
|
-
* dbus2mqtt setup using the supplied [bluez.yaml](https://github.com/jwnmulder/dbus2mqtt/blob/main/docs/examples/bluez.yaml)
|
|
14
|
-
|
|
15
|
-
Execute the following command to run dbus2mqtt with the example configuration in this repository.
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
uv run dbus2mqtt --config docs/examples/bluez.yaml
|
|
19
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/introspection_patches/mpris_playerctl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/fixtures/payload_template_jinja_expressions.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|