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.

Files changed (71) hide show
  1. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/release-drafter.yml +2 -2
  2. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.pre-commit-config.yaml +2 -2
  3. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/PKG-INFO +14 -4
  4. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/README.md +13 -3
  5. dbus2mqtt-0.4.1/docs/examples/bluez.md +37 -0
  6. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/bluez.yaml +25 -5
  7. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/pyproject.toml +4 -2
  8. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/dbus_client.py +43 -42
  9. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/mqtt/mqtt_client.py +1 -1
  10. dbus2mqtt-0.4.1/tests/conftest.py +16 -0
  11. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/dbus/test_dbus_client.py +1 -1
  12. dbus2mqtt-0.4.1/tests/dbus/test_dbus_client_mqtt_command.py +206 -0
  13. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/uv.lock +4 -4
  14. dbus2mqtt-0.4.0/docs/examples/bluez.md +0 -19
  15. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.dockerignore +0 -0
  16. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.env.example +0 -0
  17. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  18. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  19. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/scripts/release-versions.py +0 -0
  20. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/ci.yml +0 -0
  21. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/docker-dev.yml +0 -0
  22. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/docker-stable.yml +0 -0
  23. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/pre-commit.yml +0 -0
  24. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/publish.yml +0 -0
  25. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.github/workflows/release-drafter.yml +0 -0
  26. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.gitignore +0 -0
  27. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.python-version +0 -0
  28. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.vscode/launch.json +0 -0
  29. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.vscode/settings.json +0 -0
  30. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/.yamllint.yml +0 -0
  31. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/LICENSE +0 -0
  32. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docker/Dockerfile.dev +0 -0
  33. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docker/Dockerfile.pypi +0 -0
  34. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/debugging.md +0 -0
  35. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/dbus2mqtt_internal_state.md +0 -0
  36. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/dbus2mqtt_internal_state.yaml +0 -0
  37. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/home_assistant_media_player.md +0 -0
  38. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/home_assistant_media_player.yaml +0 -0
  39. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/linux_desktop.md +0 -0
  40. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples/linux_desktop.yaml +0 -0
  41. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/examples.md +0 -0
  42. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/docs/flows.md +0 -0
  43. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/renovate.json +0 -0
  44. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/__init__.py +0 -0
  45. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/__main__.py +0 -0
  46. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/config/__init__.py +0 -0
  47. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/config/jsonarparse.py +0 -0
  48. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/dbus_types.py +0 -0
  49. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/dbus_util.py +0 -0
  50. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/introspection_patches/mpris_playerctl.py +0 -0
  51. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/dbus/introspection_patches/mpris_vlc.py +0 -0
  52. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/event_broker.py +0 -0
  53. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/flow/__init__.py +0 -0
  54. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/flow/actions/context_set.py +0 -0
  55. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/flow/actions/mqtt_publish.py +0 -0
  56. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/flow/flow_processor.py +0 -0
  57. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/main.py +0 -0
  58. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/template/dbus_template_functions.py +0 -0
  59. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/src/dbus2mqtt/template/templating.py +0 -0
  60. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/__init__.py +0 -0
  61. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/fixtures/payload_template_jinja_expressions.yaml +0 -0
  62. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/fixtures/payload_template_off.yaml +0 -0
  63. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/fixtures/schedule_cron_trigger.yaml +0 -0
  64. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/test_config.py +0 -0
  65. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/config/test_examples.py +0 -0
  66. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/flow/actions/test_context_set.py +0 -0
  67. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/flow/actions/test_mqtt_publish.py +0 -0
  68. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/flow/test_flow_processor.py +0 -0
  69. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/flow/triggers/test_dbus_client_triggers.py +0 -0
  70. {dbus2mqtt-0.4.0 → dbus2mqtt-0.4.1}/tests/template/test_templating.py +0 -0
  71. {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.11.13
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.13
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.0
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
- Note that methods are called on **all** bus_names matching the configured pattern
176
+ Some examples that call methods on **all** bus_names matching the configured pattern
177
177
 
178
178
  ```json
179
179
  {
180
- "method" : "Play",
180
+ "method": "Play",
181
181
  }
182
182
  ```
183
183
 
184
184
  ```json
185
185
  {
186
- "method" : "OpenUri",
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
- Note that methods are called on **all** bus_names matching the configured pattern
144
+ Some examples that call methods on **all** bus_names matching the configured pattern
145
145
 
146
146
  ```json
147
147
  {
148
- "method" : "Play",
148
+ "method": "Play",
149
149
  }
150
150
  ```
151
151
 
152
152
  ```json
153
153
  {
154
- "method" : "OpenUri",
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: GetAll
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
- hci0: "{{ adapter_properties }}"
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: GetAll
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: "{{ device_properties }}"
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.26.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 = "session"
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
- # self.queue.put({
751
- # "topic": topic,
752
- # "payload": payload
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
- 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}")
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
- for [path, proxy_object] in bus_name_subscription.path_objects.items():
781
- for subscription_configs in self.config.get_subscription_configs(bus_name=bus_name, path=path):
782
- for interface_config in subscription_configs.interfaces:
783
-
784
- for method in interface_config.methods:
785
-
786
- # filter configured method, configured topic, ...
787
- if method.method == payload_method:
788
- interface = proxy_object.get_interface(name=interface_config.interface)
789
- matched_method = True
790
-
791
- try:
792
- logger.info(f"on_mqtt_msg: method={method.method}, args={payload_method_args}, bus_name={bus_name}, path={path}, interface={interface_config.interface}")
793
- await self.call_dbus_interface_method(interface, method.method, payload_method_args)
794
- except Exception as e:
795
- logger.warning(f"on_mqtt_msg: method={method.method}, args={payload_method_args}, bus_name={bus_name} failed, exception={e}")
796
-
797
- for property in interface_config.properties:
798
- # filter configured property, configured topic, ...
799
- if property.property == payload_property:
800
- interface = proxy_object.get_interface(name=interface_config.interface)
801
- matched_property = True
802
-
803
- try:
804
- logger.info(f"on_mqtt_msg: property={property.property}, value={payload_value}, bus_name={bus_name}, path={path}, interface={interface_config.interface}")
805
- await self.set_dbus_interface_property(interface, property.property, payload_value)
806
- except Exception as e:
807
- logger.warning(f"on_mqtt_msg: property={property.property}, value={payload_value}, bus_name={bus_name} failed, exception={e}")
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" : True}, []]
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.26.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.26.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/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" }
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/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" },
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