dbus2mqtt 0.2.0__py3-none-any.whl → 0.3.1__py3-none-any.whl
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/{config.py → config/__init__.py} +10 -8
- dbus2mqtt/config/jsonarparse.py +31 -0
- dbus2mqtt/dbus/dbus_client.py +61 -37
- dbus2mqtt/dbus/dbus_types.py +2 -2
- dbus2mqtt/dbus/dbus_util.py +2 -2
- dbus2mqtt/dbus/introspection_patches/mpris_playerctl.py +151 -0
- dbus2mqtt/dbus/introspection_patches/mpris_vlc.py +122 -0
- dbus2mqtt/event_broker.py +1 -1
- dbus2mqtt/flow/actions/mqtt_publish.py +12 -0
- dbus2mqtt/flow/flow_processor.py +28 -26
- dbus2mqtt/main.py +12 -9
- dbus2mqtt/mqtt/mqtt_client.py +31 -19
- dbus2mqtt/template/dbus_template_functions.py +2 -2
- dbus2mqtt/template/templating.py +3 -0
- {dbus2mqtt-0.2.0.dist-info → dbus2mqtt-0.3.1.dist-info}/METADATA +7 -5
- dbus2mqtt-0.3.1.dist-info/RECORD +23 -0
- dbus2mqtt-0.2.0.dist-info/RECORD +0 -20
- {dbus2mqtt-0.2.0.dist-info → dbus2mqtt-0.3.1.dist-info}/WHEEL +0 -0
- {dbus2mqtt-0.2.0.dist-info → dbus2mqtt-0.3.1.dist-info}/entry_points.txt +0 -0
- {dbus2mqtt-0.2.0.dist-info → dbus2mqtt-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -50,8 +50,8 @@ class FlowTriggerMqttConfig:
|
|
|
50
50
|
class FlowTriggerScheduleConfig:
|
|
51
51
|
type: Literal["schedule"] = "schedule"
|
|
52
52
|
id: str = field(default_factory=lambda: uuid.uuid4().hex)
|
|
53
|
-
cron: dict[str,
|
|
54
|
-
interval: dict[str,
|
|
53
|
+
cron: dict[str, object] | None = None
|
|
54
|
+
interval: dict[str, object] | None = None
|
|
55
55
|
|
|
56
56
|
@dataclass
|
|
57
57
|
class FlowTriggerDbusSignalConfig:
|
|
@@ -60,17 +60,17 @@ class FlowTriggerDbusSignalConfig:
|
|
|
60
60
|
type: Literal["dbus_signal"] = "dbus_signal"
|
|
61
61
|
bus_name: str | None = None
|
|
62
62
|
path: str | None = None
|
|
63
|
-
filter: str | None = None
|
|
63
|
+
# filter: str | None = None
|
|
64
64
|
|
|
65
65
|
@dataclass
|
|
66
66
|
class FlowTriggerBusNameAddedConfig:
|
|
67
67
|
type: Literal["bus_name_added"] = "bus_name_added"
|
|
68
|
-
filter: str | None = None
|
|
68
|
+
# filter: str | None = None
|
|
69
69
|
|
|
70
70
|
@dataclass
|
|
71
71
|
class FlowTriggerBusNameRemovedConfig:
|
|
72
72
|
type: Literal["bus_name_removed"] = "bus_name_removed"
|
|
73
|
-
filter: str | None = None
|
|
73
|
+
# filter: str | None = None
|
|
74
74
|
|
|
75
75
|
FlowTriggerConfig = Annotated[
|
|
76
76
|
FlowTriggerMqttConfig | FlowTriggerScheduleConfig | FlowTriggerDbusSignalConfig | FlowTriggerBusNameAddedConfig | FlowTriggerBusNameRemovedConfig,
|
|
@@ -80,15 +80,17 @@ FlowTriggerConfig = Annotated[
|
|
|
80
80
|
@dataclass
|
|
81
81
|
class FlowActionContextSetConfig:
|
|
82
82
|
type: Literal["context_set"] = "context_set"
|
|
83
|
-
context: dict[str,
|
|
84
|
-
|
|
83
|
+
context: dict[str, object] | None = None
|
|
84
|
+
"""Per flow execution context"""
|
|
85
|
+
global_context: dict[str, object] | None = None
|
|
86
|
+
"""Global context, shared between multiple flow executions, over all subscriptions"""
|
|
85
87
|
|
|
86
88
|
@dataclass
|
|
87
89
|
class FlowActionMqttPublishConfig:
|
|
88
90
|
topic: str
|
|
89
91
|
payload_template: str | dict[str, Any]
|
|
90
92
|
type: Literal["mqtt_publish"] = "mqtt_publish"
|
|
91
|
-
payload_type: Literal["json", "yaml", "text"] = "json"
|
|
93
|
+
payload_type: Literal["json", "yaml", "text", "binary"] = "json"
|
|
92
94
|
|
|
93
95
|
FlowActionConfig = Annotated[
|
|
94
96
|
FlowActionMqttPublishConfig | FlowActionContextSetConfig,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import jsonargparse
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _custom_yaml_load(stream):
|
|
5
|
+
if isinstance(stream, str):
|
|
6
|
+
v = stream.strip()
|
|
7
|
+
|
|
8
|
+
# jsonargparse tries to parse yaml 1.1 boolean like values
|
|
9
|
+
# Without this, str:"{'PlaybackStatus': 'Off'}" would become dict:{'PlaybackStatus': False}
|
|
10
|
+
if v in ['on', 'On', 'off', 'Off', 'TRUE', 'FALSE', 'True', 'False']:
|
|
11
|
+
return stream
|
|
12
|
+
|
|
13
|
+
# Anoyingly, values starting with {{ and ending with }} are working with the default yaml_loader
|
|
14
|
+
# from jsonargparse. Somehow its not when we use the custom yaml loader.
|
|
15
|
+
# This fixes it
|
|
16
|
+
if v.startswith("{{") or v.startswith("{%"):
|
|
17
|
+
return stream
|
|
18
|
+
|
|
19
|
+
# Delegate to default yaml loader from jsonargparse
|
|
20
|
+
yaml_loader = jsonargparse.get_loader("yaml")
|
|
21
|
+
return yaml_loader(stream)
|
|
22
|
+
|
|
23
|
+
def new_argument_parser() -> jsonargparse.ArgumentParser:
|
|
24
|
+
|
|
25
|
+
# register out custom yaml loader for jsonargparse
|
|
26
|
+
jsonargparse.set_loader("yaml_custom", _custom_yaml_load)
|
|
27
|
+
|
|
28
|
+
# unless specified otherwise, load config from config.yaml
|
|
29
|
+
parser = jsonargparse.ArgumentParser(default_config_files=["config.yaml"], default_env=True, env_prefix=False, parser_mode="yaml_custom")
|
|
30
|
+
|
|
31
|
+
return parser
|
dbus2mqtt/dbus/dbus_client.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
-
import re
|
|
4
3
|
|
|
5
4
|
from datetime import datetime
|
|
6
5
|
from typing import Any
|
|
7
6
|
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import dbus_next.introspection as dbus_introspection
|
|
7
|
+
import dbus_fast.aio as dbus_aio
|
|
8
|
+
import dbus_fast.introspection as dbus_introspection
|
|
11
9
|
|
|
12
10
|
from dbus2mqtt import AppContext
|
|
13
11
|
from dbus2mqtt.config import InterfaceConfig, SubscriptionConfig
|
|
@@ -17,17 +15,15 @@ from dbus2mqtt.dbus.dbus_util import (
|
|
|
17
15
|
unwrap_dbus_object,
|
|
18
16
|
unwrap_dbus_objects,
|
|
19
17
|
)
|
|
18
|
+
from dbus2mqtt.dbus.introspection_patches.mpris_playerctl import (
|
|
19
|
+
mpris_introspection_playerctl,
|
|
20
|
+
)
|
|
21
|
+
from dbus2mqtt.dbus.introspection_patches.mpris_vlc import mpris_introspection_vlc
|
|
20
22
|
from dbus2mqtt.event_broker import DbusSignalWithState, MqttMessage
|
|
21
23
|
from dbus2mqtt.flow.flow_processor import FlowScheduler, FlowTriggerMessage
|
|
22
24
|
|
|
23
25
|
logger = logging.getLogger(__name__)
|
|
24
26
|
|
|
25
|
-
# dbus_next path to support https://docs.flatpak.org/en/latest/portal-api-reference.html
|
|
26
|
-
# Although not correct, flatpak exposes properties with names containing '-'
|
|
27
|
-
# This is failing assertions in dbus_next
|
|
28
|
-
# original: r'^[A-Za-z_][A-Za-z0-9_]*$'
|
|
29
|
-
# patched: r'^[A-Za-z_][A-Za-z0-9_-]*$'
|
|
30
|
-
dbus_next.validators._element_re = re.compile(r'^[A-Za-z_][A-Za-z0-9_-]*$')
|
|
31
27
|
|
|
32
28
|
class DbusClient:
|
|
33
29
|
|
|
@@ -90,23 +86,23 @@ class DbusClient:
|
|
|
90
86
|
|
|
91
87
|
return proxy_object, bus_name_subscriptions
|
|
92
88
|
|
|
93
|
-
def
|
|
89
|
+
def _dbus_fast_signal_publisher(self, dbus_signal_state: dict[str, Any], *args):
|
|
94
90
|
unwrapped_args = unwrap_dbus_objects(args)
|
|
95
91
|
self.event_broker.on_dbus_signal(
|
|
96
92
|
DbusSignalWithState(**dbus_signal_state, args=unwrapped_args)
|
|
97
93
|
)
|
|
98
94
|
|
|
99
|
-
def
|
|
95
|
+
def _dbus_fast_signal_handler(self, signal: dbus_introspection.Signal, state: dict[str, Any]) -> Any:
|
|
100
96
|
expected_args = len(signal.args)
|
|
101
97
|
|
|
102
98
|
if expected_args == 1:
|
|
103
|
-
return lambda a: self.
|
|
99
|
+
return lambda a: self._dbus_fast_signal_publisher(state, a)
|
|
104
100
|
elif expected_args == 2:
|
|
105
|
-
return lambda a, b: self.
|
|
101
|
+
return lambda a, b: self._dbus_fast_signal_publisher(state, a, b)
|
|
106
102
|
elif expected_args == 3:
|
|
107
|
-
return lambda a, b, c: self.
|
|
103
|
+
return lambda a, b, c: self._dbus_fast_signal_publisher(state, a, b, c)
|
|
108
104
|
elif expected_args == 4:
|
|
109
|
-
return lambda a, b, c, d: self.
|
|
105
|
+
return lambda a, b, c, d: self._dbus_fast_signal_publisher(state, a, b, c, d)
|
|
110
106
|
raise ValueError("Unsupported nr of arguments")
|
|
111
107
|
|
|
112
108
|
async def _subscribe_interface(self, bus_name: str, path: str, introspection: dbus_introspection.Node, interface: dbus_introspection.Interface, subscription_config: SubscriptionConfig, si: InterfaceConfig) -> SubscribedInterface:
|
|
@@ -131,7 +127,7 @@ class DbusClient:
|
|
|
131
127
|
"signal_config": signal_config,
|
|
132
128
|
}
|
|
133
129
|
|
|
134
|
-
handler = self.
|
|
130
|
+
handler = self._dbus_fast_signal_handler(interface_signal, dbus_signal_state)
|
|
135
131
|
obj_interface.__getattribute__(on_signal_method_name)(handler)
|
|
136
132
|
logger.info(f"subscribed with signal_handler: signal={signal_config.signal}, bus_name={bus_name}, path={path}, interface={interface.name}")
|
|
137
133
|
|
|
@@ -162,12 +158,28 @@ class DbusClient:
|
|
|
162
158
|
|
|
163
159
|
return new_subscriptions
|
|
164
160
|
|
|
161
|
+
async def _introspect(self, bus_name: str, path: str) -> dbus_introspection.Node:
|
|
162
|
+
|
|
163
|
+
if path == "/org/mpris/MediaPlayer2" and bus_name.startswith("org.mpris.MediaPlayer2.vlc"):
|
|
164
|
+
# vlc 3.x branch contains an incomplete dbus introspection
|
|
165
|
+
# https://github.com/videolan/vlc/commit/48e593f164d2bf09b0ca096d88c86d78ec1a2ca0
|
|
166
|
+
# Until vlc 4.x is out we use the official specification instead
|
|
167
|
+
introspection = mpris_introspection_vlc
|
|
168
|
+
else:
|
|
169
|
+
introspection = await self.bus.introspect(bus_name, path)
|
|
170
|
+
|
|
171
|
+
# MPRIS: If no introspection data is available, load a default
|
|
172
|
+
if path == "/org/mpris/MediaPlayer2" and bus_name.startswith("org.mpris.MediaPlayer2.") and len(introspection.interfaces) == 0:
|
|
173
|
+
introspection = mpris_introspection_playerctl
|
|
174
|
+
|
|
175
|
+
return introspection
|
|
176
|
+
|
|
165
177
|
async def _visit_bus_name_path(self, bus_name: str, path: str) -> list[SubscribedInterface]:
|
|
166
178
|
|
|
167
179
|
new_subscriptions: list[SubscribedInterface] = []
|
|
168
180
|
|
|
169
181
|
try:
|
|
170
|
-
introspection = await self.
|
|
182
|
+
introspection = await self._introspect(bus_name, path)
|
|
171
183
|
except TypeError as e:
|
|
172
184
|
logger.warning(f"bus.introspect failed, bus_name={bus_name}, path={path}: {e}")
|
|
173
185
|
return new_subscriptions
|
|
@@ -223,21 +235,27 @@ class DbusClient:
|
|
|
223
235
|
self.flow_scheduler.start_flow_set(subscription_config.flows)
|
|
224
236
|
|
|
225
237
|
# Trigger flows that have a bus_name_added trigger configured
|
|
226
|
-
await self._trigger_bus_name_added(subscription_config, subscribed_interface.bus_name)
|
|
238
|
+
await self._trigger_bus_name_added(subscription_config, subscribed_interface.bus_name, subscribed_interface.path)
|
|
227
239
|
|
|
228
240
|
processed_new_subscriptions.add(subscription_config.id)
|
|
229
241
|
|
|
230
242
|
return new_subscriped_interfaces
|
|
231
243
|
|
|
232
|
-
async def _trigger_bus_name_added(self, subscription_config: SubscriptionConfig, bus_name: str):
|
|
244
|
+
async def _trigger_bus_name_added(self, subscription_config: SubscriptionConfig, bus_name: str, path: str):
|
|
233
245
|
|
|
234
246
|
for flow in subscription_config.flows:
|
|
235
247
|
for trigger in flow.triggers:
|
|
236
248
|
if trigger.type == "bus_name_added":
|
|
237
|
-
|
|
249
|
+
trigger_context = {
|
|
238
250
|
"bus_name": bus_name,
|
|
251
|
+
"path": path
|
|
239
252
|
}
|
|
240
|
-
trigger_message = FlowTriggerMessage(
|
|
253
|
+
trigger_message = FlowTriggerMessage(
|
|
254
|
+
flow,
|
|
255
|
+
trigger,
|
|
256
|
+
datetime.now(),
|
|
257
|
+
trigger_context=trigger_context
|
|
258
|
+
)
|
|
241
259
|
await self.event_broker.flow_trigger_queue.async_q.put(trigger_message)
|
|
242
260
|
|
|
243
261
|
async def _handle_bus_name_removed(self, bus_name: str):
|
|
@@ -251,7 +269,7 @@ class DbusClient:
|
|
|
251
269
|
for subscription_config in subscription_configs:
|
|
252
270
|
|
|
253
271
|
# Trigger flows that have a bus_name_added trigger configured
|
|
254
|
-
await self._trigger_bus_name_removed(subscription_config, bus_name)
|
|
272
|
+
await self._trigger_bus_name_removed(subscription_config, bus_name, path)
|
|
255
273
|
|
|
256
274
|
# Stop schedule triggers
|
|
257
275
|
self.flow_scheduler.stop_flow_set(subscription_config.flows)
|
|
@@ -275,16 +293,22 @@ class DbusClient:
|
|
|
275
293
|
|
|
276
294
|
del self.subscriptions[bus_name]
|
|
277
295
|
|
|
278
|
-
async def _trigger_bus_name_removed(self, subscription_config: SubscriptionConfig, bus_name: str):
|
|
296
|
+
async def _trigger_bus_name_removed(self, subscription_config: SubscriptionConfig, bus_name: str, path: str):
|
|
279
297
|
|
|
280
298
|
# Trigger flows that have a bus_name_removed trigger configured
|
|
281
299
|
for flow in subscription_config.flows:
|
|
282
300
|
for trigger in flow.triggers:
|
|
283
301
|
if trigger.type == "bus_name_removed":
|
|
284
|
-
|
|
302
|
+
trigger_context = {
|
|
285
303
|
"bus_name": bus_name,
|
|
304
|
+
"path": path
|
|
286
305
|
}
|
|
287
|
-
trigger_message = FlowTriggerMessage(
|
|
306
|
+
trigger_message = FlowTriggerMessage(
|
|
307
|
+
flow,
|
|
308
|
+
trigger,
|
|
309
|
+
datetime.now(),
|
|
310
|
+
trigger_context=trigger_context
|
|
311
|
+
)
|
|
288
312
|
await self.event_broker.flow_trigger_queue.async_q.put(trigger_message)
|
|
289
313
|
|
|
290
314
|
async def _dbus_name_owner_changed_callback(self, name, old_owner, new_owner):
|
|
@@ -310,7 +334,7 @@ class DbusClient:
|
|
|
310
334
|
|
|
311
335
|
return res
|
|
312
336
|
|
|
313
|
-
async def get_dbus_interface_property(self, interface: dbus_aio.proxy_object.ProxyInterface, property: str):
|
|
337
|
+
async def get_dbus_interface_property(self, interface: dbus_aio.proxy_object.ProxyInterface, property: str) -> Any:
|
|
314
338
|
|
|
315
339
|
call_method_name = "get_" + camel_to_snake(property)
|
|
316
340
|
res = await interface.__getattribute__(call_method_name)()
|
|
@@ -322,17 +346,12 @@ class DbusClient:
|
|
|
322
346
|
|
|
323
347
|
return res
|
|
324
348
|
|
|
325
|
-
async def set_dbus_interface_property(self, interface: dbus_aio.proxy_object.ProxyInterface, property: str, value):
|
|
349
|
+
async def set_dbus_interface_property(self, interface: dbus_aio.proxy_object.ProxyInterface, property: str, value: Any) -> None:
|
|
326
350
|
|
|
327
351
|
call_method_name = "set_" + camel_to_snake(property)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if res:
|
|
331
|
-
res = unwrap_dbus_object(res)
|
|
332
|
-
|
|
333
|
-
logger.info(f"set_dbus_interface_property: bus_name={interface.bus_name}, interface={interface.introspection.name}, property={property}, res={res}")
|
|
352
|
+
await interface.__getattribute__(call_method_name)(value)
|
|
334
353
|
|
|
335
|
-
|
|
354
|
+
logger.info(f"set_dbus_interface_property: bus_name={interface.bus_name}, interface={interface.introspection.name}, property={property}, value={value}")
|
|
336
355
|
|
|
337
356
|
async def mqtt_receive_queue_processor_task(self):
|
|
338
357
|
"""Continuously processes messages from the async queue."""
|
|
@@ -367,13 +386,18 @@ class DbusClient:
|
|
|
367
386
|
matches_filter = signal.signal_config.matches_filter(self.app_context.templating, *signal.args)
|
|
368
387
|
|
|
369
388
|
if matches_filter:
|
|
370
|
-
|
|
389
|
+
trigger_context = {
|
|
371
390
|
"bus_name": signal.bus_name,
|
|
372
391
|
"path": signal.path,
|
|
373
392
|
"interface": signal.interface_name,
|
|
374
393
|
"args": signal.args
|
|
375
394
|
}
|
|
376
|
-
trigger_message = FlowTriggerMessage(
|
|
395
|
+
trigger_message = FlowTriggerMessage(
|
|
396
|
+
flow,
|
|
397
|
+
trigger,
|
|
398
|
+
datetime.now(),
|
|
399
|
+
trigger_context=trigger_context
|
|
400
|
+
)
|
|
377
401
|
|
|
378
402
|
await self.event_broker.flow_trigger_queue.async_q.put(trigger_message)
|
|
379
403
|
except Exception as e:
|
dbus2mqtt/dbus/dbus_types.py
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import dbus_fast.aio as dbus_aio
|
|
6
6
|
|
|
7
7
|
from dbus2mqtt.config import InterfaceConfig, SubscriptionConfig
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class BusNameSubscriptions:
|
|
11
11
|
|
|
12
|
-
def __init__(self,
|
|
12
|
+
def __init__(self, bus_name: str):
|
|
13
13
|
self.bus_name = bus_name
|
|
14
14
|
self.path_objects: dict[str, dbus_aio.proxy_object.ProxyObject] = {}
|
|
15
15
|
|
dbus2mqtt/dbus/dbus_util.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import re
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import dbus_fast.signature as dbus_signature
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def _variant_serializer(obj):
|
|
@@ -10,7 +10,7 @@ def _variant_serializer(obj):
|
|
|
10
10
|
return obj
|
|
11
11
|
|
|
12
12
|
def unwrap_dbus_object(o):
|
|
13
|
-
# an easy way to get rid of
|
|
13
|
+
# an easy way to get rid of dbus_fast.signature.Variant types
|
|
14
14
|
res = json.dumps(o, default=_variant_serializer)
|
|
15
15
|
json_obj = json.loads(res)
|
|
16
16
|
return json_obj
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import dbus_fast.introspection as dbus_introspection
|
|
2
|
+
|
|
3
|
+
# taken from https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-daemon.c#L578
|
|
4
|
+
mpris_introspection_playerctl = dbus_introspection.Node.parse("""\
|
|
5
|
+
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
|
6
|
+
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
|
7
|
+
<node>
|
|
8
|
+
<interface name="org.freedesktop.DBus.Introspectable">
|
|
9
|
+
<method name="Introspect">
|
|
10
|
+
<arg name="data" direction="out" type="s"/>
|
|
11
|
+
</method>
|
|
12
|
+
</interface>
|
|
13
|
+
|
|
14
|
+
<interface name="org.freedesktop.DBus.Properties">
|
|
15
|
+
<method name="Get">
|
|
16
|
+
<arg direction="in" type="s"/>
|
|
17
|
+
<arg direction="in" type="s"/>
|
|
18
|
+
<arg direction="out" type="v"/>
|
|
19
|
+
</method>
|
|
20
|
+
<method name="Set">
|
|
21
|
+
<arg direction="in" type="s"/>
|
|
22
|
+
<arg direction="in" type="s"/>
|
|
23
|
+
<arg direction="in" type="v"/>
|
|
24
|
+
</method>
|
|
25
|
+
<method name="GetAll">
|
|
26
|
+
<arg direction="in" type="s"/>
|
|
27
|
+
<arg direction="out" type="a{sv}"/>
|
|
28
|
+
</method>
|
|
29
|
+
<signal name="PropertiesChanged">
|
|
30
|
+
<arg type="s"/>
|
|
31
|
+
<arg type="a{sv}"/>
|
|
32
|
+
<arg type="as"/>
|
|
33
|
+
</signal>
|
|
34
|
+
</interface>
|
|
35
|
+
|
|
36
|
+
<interface name="org.mpris.MediaPlayer2">
|
|
37
|
+
<property name="CanQuit" type="b" access="read"/>
|
|
38
|
+
<property name="Fullscreen" type="b" access="readwrite"/>
|
|
39
|
+
<property name="CanSetFullscreen" type="b" access="read"/>
|
|
40
|
+
<property name="CanRaise" type="b" access="read"/>
|
|
41
|
+
<property name="HasTrackList" type="b" access="read"/>
|
|
42
|
+
<property name="Identity" type="s" access="read"/>
|
|
43
|
+
<property name="DesktopEntry" type="s" access="read"/>
|
|
44
|
+
<property name="SupportedUriSchemes" type="as" access="read"/>
|
|
45
|
+
<property name="SupportedMimeTypes" type="as" access="read"/>
|
|
46
|
+
<method name="Raise"/>
|
|
47
|
+
<method name="Quit"/>
|
|
48
|
+
</interface>
|
|
49
|
+
|
|
50
|
+
<interface name="org.mpris.MediaPlayer2.Player">
|
|
51
|
+
<property name="PlaybackStatus" type="s" access="read"/>
|
|
52
|
+
<property name="LoopStatus" type="s" access="readwrite"/>
|
|
53
|
+
<property name="Rate" type="d" access="readwrite"/>
|
|
54
|
+
<property name="Shuffle" type="b" access="readwrite"/>
|
|
55
|
+
<property name="Metadata" type="a{sv}" access="read"/>
|
|
56
|
+
<property name="Volume" type="d" access="readwrite"/>
|
|
57
|
+
<property name="Position" type="x" access="read"/>
|
|
58
|
+
<property name="MinimumRate" type="d" access="read"/>
|
|
59
|
+
<property name="MaximumRate" type="d" access="read"/>
|
|
60
|
+
<property name="CanGoNext" type="b" access="read"/>
|
|
61
|
+
<property name="CanGoPrevious" type="b" access="read"/>
|
|
62
|
+
<property name="CanPlay" type="b" access="read"/>
|
|
63
|
+
<property name="CanPause" type="b" access="read"/>
|
|
64
|
+
<property name="CanSeek" type="b" access="read"/>
|
|
65
|
+
<property name="CanControl" type="b" access="read"/>
|
|
66
|
+
<method name="Next"/>
|
|
67
|
+
<method name="Previous"/>
|
|
68
|
+
<method name="Pause"/>
|
|
69
|
+
<method name="PlayPause"/>
|
|
70
|
+
<method name="Stop"/>
|
|
71
|
+
<method name="Play"/>
|
|
72
|
+
<method name="Seek">
|
|
73
|
+
<arg type="x" name="Offset" direction="in"/>
|
|
74
|
+
</method>
|
|
75
|
+
<method name="SetPosition">
|
|
76
|
+
<arg type="o" name="TrackId" direction="in"/>
|
|
77
|
+
<arg type="x" name="Offset" direction="in"/>
|
|
78
|
+
</method>
|
|
79
|
+
<method name="OpenUri">
|
|
80
|
+
<arg type="s" name="Uri" direction="in"/>
|
|
81
|
+
</method>
|
|
82
|
+
<signal name="Seeked">
|
|
83
|
+
<arg type="x" name="Position" direction="out"/>
|
|
84
|
+
</signal>
|
|
85
|
+
</interface>
|
|
86
|
+
|
|
87
|
+
<interface name="org.mpris.MediaPlayer2.TrackList">
|
|
88
|
+
<property name="Tracks" type="ao" access="read">
|
|
89
|
+
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="invalidates"/>
|
|
90
|
+
</property>
|
|
91
|
+
<property name="CanEditTracks" type="b" access="read">
|
|
92
|
+
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
|
93
|
+
</property>
|
|
94
|
+
<method name="GetTracksMetadata">
|
|
95
|
+
<arg direction="in" name="TrackIds" type="ao"/>
|
|
96
|
+
<arg direction="out" name="Metadata" type="aa{sv}"/>
|
|
97
|
+
</method>
|
|
98
|
+
<method name="AddTrack">
|
|
99
|
+
<arg direction="in" name="Uri" type="s"/>
|
|
100
|
+
<arg direction="in" name="AfterTrack" type="o"/>
|
|
101
|
+
<arg direction="in" name="SetAsCurrent" type="b"/>
|
|
102
|
+
</method>
|
|
103
|
+
<method name="RemoveTrack">
|
|
104
|
+
<arg direction="in" name="TrackId" type="o"/>
|
|
105
|
+
</method>
|
|
106
|
+
<method name="GoTo">
|
|
107
|
+
<arg direction="in" name="TrackId" type="o"/>
|
|
108
|
+
</method>
|
|
109
|
+
<signal name="TrackListReplaced">
|
|
110
|
+
<arg name="Tracks" type="ao"/>
|
|
111
|
+
<arg name="CurrentTrack" type="o"/>
|
|
112
|
+
</signal>
|
|
113
|
+
<signal name="TrackAdded">
|
|
114
|
+
<arg name="Metadata" type="a{sv}"/>
|
|
115
|
+
<arg name="AfterTrack" type="o"/>
|
|
116
|
+
</signal>
|
|
117
|
+
<signal name="TrackRemoved">
|
|
118
|
+
<arg name="TrackId" type="o"/>
|
|
119
|
+
</signal>
|
|
120
|
+
<signal name="TrackMetadataChanged">
|
|
121
|
+
<arg name="TrackId" type="o"/>
|
|
122
|
+
<arg name="Metadata" type="a{sv}"/>
|
|
123
|
+
</signal>
|
|
124
|
+
</interface>
|
|
125
|
+
|
|
126
|
+
<interface name="org.mpris.MediaPlayer2.Playlists">
|
|
127
|
+
<method name="ActivatePlaylist">
|
|
128
|
+
<arg direction="in" name="PlaylistId" type="o"/>
|
|
129
|
+
</method>
|
|
130
|
+
<method name="GetPlaylists">
|
|
131
|
+
<arg direction="in" name="Index" type="u"/>
|
|
132
|
+
<arg direction="in" name="MaxCount" type="u"/>
|
|
133
|
+
<arg direction="in" name="Order" type="s"/>
|
|
134
|
+
<arg direction="in" name="ReverseOrder" type="b"/>
|
|
135
|
+
<arg direction="out" name="Playlists" type="a(oss)"/>
|
|
136
|
+
</method>
|
|
137
|
+
<property name="PlaylistCount" type="u" access="read">
|
|
138
|
+
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
|
139
|
+
</property>
|
|
140
|
+
<property name="Orderings" type="as" access="read">
|
|
141
|
+
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
|
142
|
+
</property>
|
|
143
|
+
<property name="ActivePlaylist" type="(b(oss))" access="read">
|
|
144
|
+
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
|
145
|
+
</property>
|
|
146
|
+
<signal name="PlaylistChanged">
|
|
147
|
+
<arg name="Playlist" type="(oss)"/>
|
|
148
|
+
</signal>
|
|
149
|
+
</interface>
|
|
150
|
+
</node>
|
|
151
|
+
""")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import dbus_fast.introspection as dbus_introspection
|
|
2
|
+
|
|
3
|
+
# taken from https://code.videolan.org/videolan/vlc/-/blob/master/modules/control/dbus/dbus_introspect.h
|
|
4
|
+
mpris_introspection_vlc = dbus_introspection.Node.parse("""\
|
|
5
|
+
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
|
6
|
+
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
|
7
|
+
<node>
|
|
8
|
+
<interface name="org.freedesktop.DBus.Introspectable">
|
|
9
|
+
<method name="Introspect">
|
|
10
|
+
<arg name="data" direction="out" type="s"/>
|
|
11
|
+
</method>
|
|
12
|
+
</interface>
|
|
13
|
+
|
|
14
|
+
<interface name="org.freedesktop.DBus.Properties">
|
|
15
|
+
<method name="Get">
|
|
16
|
+
<arg direction="in" type="s"/>
|
|
17
|
+
<arg direction="in" type="s"/>
|
|
18
|
+
<arg direction="out" type="v"/>
|
|
19
|
+
</method>
|
|
20
|
+
<method name="Set">
|
|
21
|
+
<arg direction="in" type="s"/>
|
|
22
|
+
<arg direction="in" type="s"/>
|
|
23
|
+
<arg direction="in" type="v"/>
|
|
24
|
+
</method>
|
|
25
|
+
<method name="GetAll">
|
|
26
|
+
<arg direction="in" type="s"/>
|
|
27
|
+
<arg direction="out" type="a{sv}"/>
|
|
28
|
+
</method>
|
|
29
|
+
<signal name="PropertiesChanged">
|
|
30
|
+
<arg type="s"/>
|
|
31
|
+
<arg type="a{sv}"/>
|
|
32
|
+
<arg type="as"/>
|
|
33
|
+
</signal>
|
|
34
|
+
</interface>
|
|
35
|
+
|
|
36
|
+
<interface name="org.mpris.MediaPlayer2">
|
|
37
|
+
<property name="CanQuit" type="b" access="read"/>
|
|
38
|
+
<property name="Fullscreen" type="b" access="readwrite"/>
|
|
39
|
+
<property name="CanSetFullscreen" type="b" access="read"/>
|
|
40
|
+
<property name="CanRaise" type="b" access="read"/>
|
|
41
|
+
<property name="HasTrackList" type="b" access="read"/>
|
|
42
|
+
<property name="Identity" type="s" access="read"/>
|
|
43
|
+
<property name="DesktopEntry" type="s" access="read"/>
|
|
44
|
+
<property name="SupportedUriSchemes" type="as" access="read"/>
|
|
45
|
+
<property name="SupportedMimeTypes" type="as" access="read"/>
|
|
46
|
+
<method name="Raise"/>
|
|
47
|
+
<method name="Quit"/>
|
|
48
|
+
</interface>
|
|
49
|
+
|
|
50
|
+
<interface name="org.mpris.MediaPlayer2.Player">
|
|
51
|
+
<property name="PlaybackStatus" type="s" access="read"/>
|
|
52
|
+
<property name="LoopStatus" type="s" access="readwrite"/>
|
|
53
|
+
<property name="Rate" type="d" access="readwrite"/>
|
|
54
|
+
<property name="Shuffle" type="b" access="readwrite"/>
|
|
55
|
+
<property name="Metadata" type="a{sv}" access="read"/>
|
|
56
|
+
<property name="Volume" type="d" access="readwrite"/>
|
|
57
|
+
<property name="Position" type="x" access="read"/>
|
|
58
|
+
<property name="MinimumRate" type="d" access="read"/>
|
|
59
|
+
<property name="MaximumRate" type="d" access="read"/>
|
|
60
|
+
<property name="CanGoNext" type="b" access="read"/>
|
|
61
|
+
<property name="CanGoPrevious" type="b" access="read"/>
|
|
62
|
+
<property name="CanPlay" type="b" access="read"/>
|
|
63
|
+
<property name="CanPause" type="b" access="read"/>
|
|
64
|
+
<property name="CanSeek" type="b" access="read"/>
|
|
65
|
+
<property name="CanControl" type="b" access="read"/>
|
|
66
|
+
<method name="Next"/>
|
|
67
|
+
<method name="Previous"/>
|
|
68
|
+
<method name="Pause"/>
|
|
69
|
+
<method name="PlayPause"/>
|
|
70
|
+
<method name="Stop"/>
|
|
71
|
+
<method name="Play"/>
|
|
72
|
+
<method name="Seek">
|
|
73
|
+
<arg type="x" direction="in"/>
|
|
74
|
+
</method>
|
|
75
|
+
<method name="SetPosition">
|
|
76
|
+
<arg type="o" direction="in"/>
|
|
77
|
+
<arg type="x" direction="in"/>
|
|
78
|
+
</method>
|
|
79
|
+
<method name="OpenUri">
|
|
80
|
+
<arg type="s" direction="in"/>
|
|
81
|
+
</method>
|
|
82
|
+
<signal name="Seeked">
|
|
83
|
+
<arg type="x"/>
|
|
84
|
+
</signal>
|
|
85
|
+
</interface>
|
|
86
|
+
|
|
87
|
+
<interface name="org.mpris.MediaPlayer2.TrackList">
|
|
88
|
+
<property name="Tracks" type="ao" access="read"/>
|
|
89
|
+
<property name="CanEditTracks" type="b" access="read"/>
|
|
90
|
+
<method name="GetTracksMetadata">
|
|
91
|
+
<arg type="ao" direction="in"/>
|
|
92
|
+
<arg type="aa{sv}" direction="out"/>
|
|
93
|
+
</method>
|
|
94
|
+
<method name="AddTrack">
|
|
95
|
+
<arg type="s" direction="in"/>
|
|
96
|
+
<arg type="o" direction="in"/>
|
|
97
|
+
<arg type="b" direction="in"/>
|
|
98
|
+
</method>
|
|
99
|
+
<method name="RemoveTrack">
|
|
100
|
+
<arg type="o" direction="in"/>
|
|
101
|
+
</method>
|
|
102
|
+
<method name="GoTo">
|
|
103
|
+
<arg type="o" direction="in"/>
|
|
104
|
+
</method>
|
|
105
|
+
<signal name="TrackListReplaced">
|
|
106
|
+
<arg type="ao"/>
|
|
107
|
+
<arg type="o"/>
|
|
108
|
+
</signal>
|
|
109
|
+
<signal name="TrackAdded">
|
|
110
|
+
<arg type="a{sv}"/>
|
|
111
|
+
<arg type="o"/>
|
|
112
|
+
</signal>
|
|
113
|
+
<signal name="TrackRemoved">
|
|
114
|
+
<arg type="o"/>
|
|
115
|
+
</signal>
|
|
116
|
+
<signal name="TrackMetadataChanged">
|
|
117
|
+
<arg type="o"/>
|
|
118
|
+
<arg type="a{sv}"/>
|
|
119
|
+
</signal>
|
|
120
|
+
</interface>
|
|
121
|
+
</node>
|
|
122
|
+
""")
|
dbus2mqtt/event_broker.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
4
6
|
from jinja2.exceptions import TemplateError
|
|
5
7
|
|
|
6
8
|
from dbus2mqtt import AppContext
|
|
@@ -26,11 +28,21 @@ class MqttPublishAction(FlowAction):
|
|
|
26
28
|
|
|
27
29
|
if self.config.payload_type == "text":
|
|
28
30
|
res_type = str
|
|
31
|
+
elif self.config.payload_type == "binary":
|
|
32
|
+
res_type = str
|
|
29
33
|
else:
|
|
30
34
|
res_type = dict
|
|
31
35
|
|
|
32
36
|
payload = await self.templating.async_render_template(self.config.payload_template, res_type, render_context)
|
|
33
37
|
|
|
38
|
+
# for binary payloads, payload contains the file to read binary data from
|
|
39
|
+
if isinstance(payload, str) and self.config.payload_type == "binary":
|
|
40
|
+
uri = payload
|
|
41
|
+
payload = urlparse(uri)
|
|
42
|
+
if not payload.scheme == "file":
|
|
43
|
+
raise ValueError(f"Expected readable file, got: '{uri}'")
|
|
44
|
+
|
|
45
|
+
|
|
34
46
|
except TemplateError as e:
|
|
35
47
|
logger.warning(f"Error rendering jinja template, flow: '{context.name or ''}', msg={e}, payload_template={self.config.payload_template}, render_context={render_context}", exc_info=True)
|
|
36
48
|
return
|
dbus2mqtt/flow/flow_processor.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
8
8
|
|
|
9
9
|
from dbus2mqtt import AppContext
|
|
10
|
-
from dbus2mqtt.config import FlowConfig, FlowTriggerConfig
|
|
10
|
+
from dbus2mqtt.config import FlowConfig, FlowTriggerConfig, FlowTriggerDbusSignalConfig
|
|
11
11
|
from dbus2mqtt.event_broker import FlowTriggerMessage
|
|
12
12
|
from dbus2mqtt.flow import FlowAction, FlowExecutionContext
|
|
13
13
|
from dbus2mqtt.flow.actions.context_set import ContextSetAction
|
|
@@ -46,6 +46,7 @@ class FlowScheduler:
|
|
|
46
46
|
if not existing_job and trigger.type == "schedule":
|
|
47
47
|
logger.info(f"Starting scheduler[{trigger.id}] for flow {flow.id}")
|
|
48
48
|
if trigger.interval:
|
|
49
|
+
trigger_args: dict[str, Any] = trigger.interval
|
|
49
50
|
# Each schedule gets its own job
|
|
50
51
|
self.scheduler.add_job(
|
|
51
52
|
self._schedule_flow_strigger,
|
|
@@ -55,9 +56,10 @@ class FlowScheduler:
|
|
|
55
56
|
misfire_grace_time=5,
|
|
56
57
|
coalesce=True,
|
|
57
58
|
args=[flow, trigger],
|
|
58
|
-
**
|
|
59
|
+
**trigger_args
|
|
59
60
|
)
|
|
60
61
|
elif trigger.cron:
|
|
62
|
+
trigger_args: dict[str, Any] = trigger.cron
|
|
61
63
|
# Each schedule gets its own job
|
|
62
64
|
self.scheduler.add_job(
|
|
63
65
|
self._schedule_flow_strigger,
|
|
@@ -67,7 +69,7 @@ class FlowScheduler:
|
|
|
67
69
|
misfire_grace_time=5,
|
|
68
70
|
coalesce=True,
|
|
69
71
|
args=[flow, trigger],
|
|
70
|
-
**
|
|
72
|
+
**trigger_args
|
|
71
73
|
)
|
|
72
74
|
|
|
73
75
|
def stop_flow_set(self, flows):
|
|
@@ -160,38 +162,38 @@ class FlowProcessor:
|
|
|
160
162
|
await self._process_flow_trigger(flow_trigger_message)
|
|
161
163
|
|
|
162
164
|
except Exception as e:
|
|
163
|
-
|
|
165
|
+
# exc_info is only set when running in verbose mode to avoid lots of stack traces being printed
|
|
166
|
+
# while flows are still running and the DBus object was just removed. Some examples:
|
|
167
|
+
|
|
168
|
+
log_level = logging.WARN
|
|
169
|
+
|
|
170
|
+
# 1: error during context_set
|
|
171
|
+
# WARNING:dbus2mqtt.flow.flow_processor:flow_processor_task: Exception The name org.mpris.MediaPlayer2.firefox.instance_1_672 was not provided by any .service files
|
|
172
|
+
if "was not provided by any .service files" in str(e):
|
|
173
|
+
log_level = logging.DEBUG
|
|
174
|
+
|
|
175
|
+
logger.log(log_level, f"flow_processor_task: Exception {e}", exc_info=logger.isEnabledFor(logging.DEBUG))
|
|
164
176
|
finally:
|
|
165
177
|
self.event_broker.flow_trigger_queue.async_q.task_done()
|
|
166
178
|
|
|
179
|
+
def _trigger_config_to_str(self, config: FlowTriggerConfig) -> str:
|
|
180
|
+
if isinstance(config, FlowTriggerDbusSignalConfig):
|
|
181
|
+
return f"{config.type}({config.signal})"
|
|
182
|
+
return config.type
|
|
183
|
+
|
|
167
184
|
async def _process_flow_trigger(self, flow_trigger_message: FlowTriggerMessage):
|
|
168
|
-
|
|
185
|
+
|
|
186
|
+
trigger_str = self._trigger_config_to_str(flow_trigger_message.flow_trigger_config)
|
|
187
|
+
flow_str = flow_trigger_message.flow_config.name or flow_trigger_message.flow_config.id
|
|
188
|
+
|
|
189
|
+
log_message = f"on_trigger: {trigger_str}, flow={flow_str}, time={flow_trigger_message.timestamp.isoformat()}"
|
|
190
|
+
|
|
169
191
|
if flow_trigger_message.flow_trigger_config.type != "schedule":
|
|
170
192
|
logger.info(log_message)
|
|
171
193
|
else:
|
|
172
194
|
logger.debug(log_message)
|
|
173
195
|
|
|
174
196
|
flow_id = flow_trigger_message.flow_config.id
|
|
175
|
-
# flow_name = flow_trigger_message.flow_config.name
|
|
176
197
|
|
|
177
198
|
flow = self._flows[flow_id]
|
|
178
|
-
await flow.execute_actions(trigger_context=flow_trigger_message.
|
|
179
|
-
|
|
180
|
-
# # Create a flow from the YAML configuration
|
|
181
|
-
# for flow_config in config['flows']:
|
|
182
|
-
# flow_name = flow_config['name']
|
|
183
|
-
# triggers = flow_config.get('triggers', [])
|
|
184
|
-
# actions = flow_config.get('actions', [])
|
|
185
|
-
|
|
186
|
-
# with Flow(flow_name) as flow:
|
|
187
|
-
# data = "sensor_data"
|
|
188
|
-
# for action in actions:
|
|
189
|
-
# if action['type'] == 'python_script':
|
|
190
|
-
# process_data(data)
|
|
191
|
-
# elif action['type'] == 'mqtt_publish':
|
|
192
|
-
# mqtt_publish(action['topic'], action['message_template'], data)
|
|
193
|
-
|
|
194
|
-
# # Add scheduling trigger if defined
|
|
195
|
-
# for trigger in triggers:
|
|
196
|
-
# if trigger['type'] == 'schedule' and 'cron' in trigger:
|
|
197
|
-
# flow.schedule = CronSchedule(cron=trigger['cron'])
|
|
199
|
+
await flow.execute_actions(trigger_context=flow_trigger_message.trigger_context)
|
dbus2mqtt/main.py
CHANGED
|
@@ -5,12 +5,12 @@ import sys
|
|
|
5
5
|
from typing import cast
|
|
6
6
|
|
|
7
7
|
import colorlog
|
|
8
|
-
import
|
|
8
|
+
import dbus_fast.aio as dbus_aio
|
|
9
9
|
import dotenv
|
|
10
|
-
import jsonargparse
|
|
11
10
|
|
|
12
11
|
from dbus2mqtt import AppContext
|
|
13
12
|
from dbus2mqtt.config import Config
|
|
13
|
+
from dbus2mqtt.config.jsonarparse import new_argument_parser
|
|
14
14
|
from dbus2mqtt.dbus.dbus_client import DbusClient
|
|
15
15
|
from dbus2mqtt.event_broker import EventBroker
|
|
16
16
|
from dbus2mqtt.flow.flow_processor import FlowProcessor, FlowScheduler
|
|
@@ -41,14 +41,14 @@ async def dbus_processor_task(app_context: AppContext, flow_scheduler: FlowSched
|
|
|
41
41
|
|
|
42
42
|
async def mqtt_processor_task(app_context: AppContext):
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
loop = asyncio.get_running_loop()
|
|
45
|
+
mqtt_client_run_future = loop.create_future()
|
|
46
|
+
|
|
47
|
+
mqtt_client = MqttClient(app_context, loop)
|
|
45
48
|
|
|
46
49
|
mqtt_client.connect()
|
|
47
50
|
mqtt_client.client.loop_start()
|
|
48
51
|
|
|
49
|
-
loop = asyncio.get_running_loop()
|
|
50
|
-
mqtt_client_run_future = loop.create_future()
|
|
51
|
-
|
|
52
52
|
try:
|
|
53
53
|
await asyncio.gather(
|
|
54
54
|
mqtt_client_run_future,
|
|
@@ -84,6 +84,7 @@ async def run(config: Config):
|
|
|
84
84
|
except asyncio.CancelledError:
|
|
85
85
|
pass
|
|
86
86
|
|
|
87
|
+
|
|
87
88
|
def main():
|
|
88
89
|
|
|
89
90
|
# load environment from .env if it exists
|
|
@@ -92,8 +93,7 @@ def main():
|
|
|
92
93
|
logger.info(f"Loaded environment variables from {dotenv_file}")
|
|
93
94
|
dotenv.load_dotenv(dotenv_path=dotenv_file)
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
parser = jsonargparse.ArgumentParser(default_config_files=["config.yaml"], default_env=True, env_prefix=False)
|
|
96
|
+
parser = new_argument_parser()
|
|
97
97
|
|
|
98
98
|
parser.add_argument("--verbose", "-v", nargs="?", const=True, help="Enable verbose logging")
|
|
99
99
|
parser.add_argument("--config", action="config")
|
|
@@ -123,4 +123,7 @@ def main():
|
|
|
123
123
|
|
|
124
124
|
logger.debug(f"config: {config}")
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
try:
|
|
127
|
+
asyncio.run(run(config))
|
|
128
|
+
except KeyboardInterrupt:
|
|
129
|
+
return 0
|
dbus2mqtt/mqtt/mqtt_client.py
CHANGED
|
@@ -4,6 +4,8 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
|
|
6
6
|
from typing import Any
|
|
7
|
+
from urllib.parse import ParseResult
|
|
8
|
+
from urllib.request import urlopen
|
|
7
9
|
|
|
8
10
|
import paho.mqtt.client as mqtt
|
|
9
11
|
import yaml
|
|
@@ -18,7 +20,7 @@ logger = logging.getLogger(__name__)
|
|
|
18
20
|
|
|
19
21
|
class MqttClient:
|
|
20
22
|
|
|
21
|
-
def __init__(self, app_context: AppContext):
|
|
23
|
+
def __init__(self, app_context: AppContext, loop):
|
|
22
24
|
self.config = app_context.config.mqtt
|
|
23
25
|
self.event_broker = app_context.event_broker
|
|
24
26
|
|
|
@@ -35,6 +37,9 @@ class MqttClient:
|
|
|
35
37
|
self.client.on_connect = self.on_connect
|
|
36
38
|
self.client.on_message = self.on_message
|
|
37
39
|
|
|
40
|
+
self.loop = loop
|
|
41
|
+
self.connected_event = asyncio.Event()
|
|
42
|
+
|
|
38
43
|
def connect(self):
|
|
39
44
|
|
|
40
45
|
# mqtt_client.on_message = lambda client, userdata, message: asyncio.create_task(mqtt_on_message(client, userdata, message))
|
|
@@ -52,34 +57,39 @@ class MqttClient:
|
|
|
52
57
|
msg = await self.event_broker.mqtt_publish_queue.async_q.get() # Wait for a message
|
|
53
58
|
|
|
54
59
|
try:
|
|
55
|
-
payload = msg.payload
|
|
60
|
+
payload: str | bytes | None = msg.payload
|
|
56
61
|
type = msg.payload_serialization_type
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if type == "text":
|
|
63
|
+
payload = str(msg.payload)
|
|
64
|
+
if isinstance(msg.payload, dict) and type == "json":
|
|
65
|
+
payload = json.dumps(msg.payload)
|
|
66
|
+
elif isinstance(msg.payload, dict) and type == "yaml":
|
|
67
|
+
payload = yaml.dump(msg.payload)
|
|
68
|
+
elif isinstance(msg.payload, ParseResult) and type == "binary":
|
|
69
|
+
try:
|
|
70
|
+
with urlopen(msg.payload.geturl()) as response:
|
|
71
|
+
payload = response.read()
|
|
72
|
+
except Exception as e:
|
|
73
|
+
# In case failing uri reads, we still publish an empty msg to avoid stale data
|
|
74
|
+
payload = None
|
|
75
|
+
logger.warning(f"mqtt_publish_queue_processor_task: Exception {e}", exc_info=logger.isEnabledFor(logging.DEBUG))
|
|
76
|
+
|
|
77
|
+
payload_log_msg = payload if isinstance(payload, str) else msg.payload
|
|
78
|
+
logger.debug(f"mqtt_publish_queue_processor_task: topic={msg.topic}, type={payload.__class__}, payload={payload_log_msg}")
|
|
64
79
|
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
if first_message:
|
|
81
|
+
await asyncio.wait_for(self.connected_event.wait(), timeout=5)
|
|
67
82
|
|
|
83
|
+
self.client.publish(topic=msg.topic, payload=payload or "").wait_for_publish(timeout=1000)
|
|
68
84
|
if first_message:
|
|
69
|
-
logger.info(f"First message published: topic={msg.topic}, payload={
|
|
85
|
+
logger.info(f"First message published: topic={msg.topic}, payload={payload_log_msg}")
|
|
70
86
|
first_message = False
|
|
71
87
|
|
|
72
88
|
except Exception as e:
|
|
73
|
-
logger.warning(f"mqtt_publish_queue_processor_task: Exception {e}", exc_info=
|
|
89
|
+
logger.warning(f"mqtt_publish_queue_processor_task: Exception {e}", exc_info=logger.isEnabledFor(logging.DEBUG))
|
|
74
90
|
finally:
|
|
75
91
|
self.event_broker.mqtt_publish_queue.async_q.task_done()
|
|
76
92
|
|
|
77
|
-
|
|
78
|
-
async def run(self):
|
|
79
|
-
"""Runs the MQTT loop in a non-blocking way with asyncio."""
|
|
80
|
-
self.client.loop_start() # Runs Paho's loop in a background thread
|
|
81
|
-
await asyncio.Event().wait() # Keeps the coroutine alive
|
|
82
|
-
|
|
83
93
|
# The callback for when the client receives a CONNACK response from the server.
|
|
84
94
|
def on_connect(self, client: mqtt.Client, userdata, flags, reason_code, properties):
|
|
85
95
|
if reason_code.is_failure:
|
|
@@ -90,6 +100,8 @@ class MqttClient:
|
|
|
90
100
|
# reconnect then subscriptions will be renewed.
|
|
91
101
|
client.subscribe("dbus2mqtt/#", options=SubscribeOptions(noLocal=True))
|
|
92
102
|
|
|
103
|
+
self.loop.call_soon_threadsafe(self.connected_event.set)
|
|
104
|
+
|
|
93
105
|
def on_message(self, client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage):
|
|
94
106
|
|
|
95
107
|
payload = msg.payload.decode()
|
|
@@ -3,8 +3,8 @@ import logging
|
|
|
3
3
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
from
|
|
6
|
+
from dbus_fast.constants import ErrorType
|
|
7
|
+
from dbus_fast.errors import DBusError
|
|
8
8
|
|
|
9
9
|
from dbus2mqtt.dbus.dbus_client import DbusClient
|
|
10
10
|
|
dbus2mqtt/template/templating.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbus2mqtt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: apscheduler>=3.11.0
|
|
21
21
|
Requires-Dist: colorlog>=6.9.0
|
|
22
|
-
Requires-Dist: dbus-
|
|
22
|
+
Requires-Dist: dbus-fast>=2.44.1
|
|
23
23
|
Requires-Dist: janus>=2.0.0
|
|
24
24
|
Requires-Dist: jinja2-ansible-filters>=1.3.2
|
|
25
25
|
Requires-Dist: jinja2>=3.1.6
|
|
@@ -111,7 +111,7 @@ cp docs/examples/home_assistant_media_player.yaml $HOME/.config/dbus2mqtt/config
|
|
|
111
111
|
cp .env.example $HOME/.config/dbus2mqtt/.env
|
|
112
112
|
|
|
113
113
|
# run image and automatically start on reboot
|
|
114
|
-
|
|
114
|
+
docker pull jwnmulder/dbus2mqtt
|
|
115
115
|
docker run --detach --name dbus2mqtt \
|
|
116
116
|
--volume "$HOME"/.config/dbus2mqtt:"$HOME"/.config/dbus2mqtt \
|
|
117
117
|
--volume /run/user:/run/user \
|
|
@@ -138,7 +138,7 @@ dbus2mqtt leverages [jsonargparse](https://jsonargparse.readthedocs.io/en/stable
|
|
|
138
138
|
### MQTT and D-Bus connection details
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
|
-
#
|
|
141
|
+
# dbus_fast configuration
|
|
142
142
|
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
|
|
143
143
|
|
|
144
144
|
# dbus2mqtt configuration
|
|
@@ -171,7 +171,9 @@ dbus:
|
|
|
171
171
|
- method: Play
|
|
172
172
|
```
|
|
173
173
|
|
|
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
|
|
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
|
+
|
|
176
|
+
Note that methods are called on **all** bus_names matching the configured pattern
|
|
175
177
|
|
|
176
178
|
```json
|
|
177
179
|
{
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
dbus2mqtt/__init__.py,sha256=VunubKEH4_lne9Ttd2YRpyXjZMIAzyD2eiJ2sfvvTFU,362
|
|
2
|
+
dbus2mqtt/__main__.py,sha256=NAoa3nwgBSQI22eWzzRx61SIDThDwXmUofWWZU3_4-Q,71
|
|
3
|
+
dbus2mqtt/event_broker.py,sha256=WNup2Xywt2MYs1yDyKAytApJn_YOPnxQag3kOkOajX0,1844
|
|
4
|
+
dbus2mqtt/main.py,sha256=pHsRbjjvQSAOWhmfazixKteAdfrB-YnC4LQAZovdkEQ,3863
|
|
5
|
+
dbus2mqtt/config/__init__.py,sha256=lx94zQsejGMRf90h594jId_CKXY0CyL7n6hk3lwAVvE,4307
|
|
6
|
+
dbus2mqtt/config/jsonarparse.py,sha256=VJGFeyQJcOE6bRXlSRr3FClvN_HnmiIlEGmOgNM0oCc,1214
|
|
7
|
+
dbus2mqtt/dbus/dbus_client.py,sha256=LAP6QkjPvDHIXOKncHiNYrTnVEV2NRYc1Mymzp-miMY,23217
|
|
8
|
+
dbus2mqtt/dbus/dbus_types.py,sha256=EejiLlh3ALAnB4OFvNRL4ILBbxSKgFNNwifBrfap1Gc,494
|
|
9
|
+
dbus2mqtt/dbus/dbus_util.py,sha256=Q50Tr1qjqZHpVirzvT9gXYwiCLwPe6BpgVmAcrqdHKE,569
|
|
10
|
+
dbus2mqtt/dbus/introspection_patches/mpris_playerctl.py,sha256=q93d_Yp93u3Y-9q0dRdKW5hrij9GK3CFqKhUWVE8uw4,5883
|
|
11
|
+
dbus2mqtt/dbus/introspection_patches/mpris_vlc.py,sha256=Cf-o-05W6gUoKpcYR7n0dRi-CrbeASPTwkyEzZGnU3Y,4241
|
|
12
|
+
dbus2mqtt/flow/__init__.py,sha256=tAL-CjXQHq_tGTKctIdOZ5teVKBtcJs6Astq_RdruV8,1540
|
|
13
|
+
dbus2mqtt/flow/flow_processor.py,sha256=TR30RpN0DoKWgx3Il-wyIzGwRqTjXyEUlqar3S3E3SI,8215
|
|
14
|
+
dbus2mqtt/flow/actions/context_set.py,sha256=dIT39MJJVb0wuRI_ZM3ssnXYfa-iyB4o_UZD-1BZL2g,1087
|
|
15
|
+
dbus2mqtt/flow/actions/mqtt_publish.py,sha256=psNkTvaR3JZwAwpM4AqiZTDnA5UQX9r4CUZ1LA7iRW4,2366
|
|
16
|
+
dbus2mqtt/mqtt/mqtt_client.py,sha256=9Y0AEuquq4gEQ8j9JD1wauU22wSHZFSyQBNr905lwxA,4899
|
|
17
|
+
dbus2mqtt/template/dbus_template_functions.py,sha256=0WXH-X3Si5D8iKP1WrSK9XmbKcaTGQo92SJsfHG0JEE,2427
|
|
18
|
+
dbus2mqtt/template/templating.py,sha256=ZLp1A8PFAs_-Bndx4yGqyppaDfh8marHlK7P3bFInu4,4144
|
|
19
|
+
dbus2mqtt-0.3.1.dist-info/METADATA,sha256=5xbhQusP8EUrvvMuN6rK2MUWBNKMgWOu1XkfADaGSd8,7830
|
|
20
|
+
dbus2mqtt-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
dbus2mqtt-0.3.1.dist-info/entry_points.txt,sha256=pmmacoHCsvTTUB5dIPaY4i0H9gX7BQlpd3rBU-Jbv3A,50
|
|
22
|
+
dbus2mqtt-0.3.1.dist-info/licenses/LICENSE,sha256=a4bIEgyA9rrnAfUN90CgbgZ6BQIFHeABkk0JihiBaxM,1074
|
|
23
|
+
dbus2mqtt-0.3.1.dist-info/RECORD,,
|
dbus2mqtt-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
dbus2mqtt/__init__.py,sha256=VunubKEH4_lne9Ttd2YRpyXjZMIAzyD2eiJ2sfvvTFU,362
|
|
2
|
-
dbus2mqtt/__main__.py,sha256=NAoa3nwgBSQI22eWzzRx61SIDThDwXmUofWWZU3_4-Q,71
|
|
3
|
-
dbus2mqtt/config.py,sha256=aTAWR4G1y12-96w1db2rjdCafEy6gF9gAiNFcW3Sz4M,4152
|
|
4
|
-
dbus2mqtt/event_broker.py,sha256=GG7vZZHu08vJgCH5cA-yw3yWU3I2wWHhiEFNMHA4oJk,1836
|
|
5
|
-
dbus2mqtt/main.py,sha256=1zodEH7s-u75eY4fYLSpV-mtcCV_VL38Do2VXLinrh4,3898
|
|
6
|
-
dbus2mqtt/dbus/dbus_client.py,sha256=Y5upYGX84TCZRzJb7cQhxv9irXBfxo6bFmUil-jdlo4,22005
|
|
7
|
-
dbus2mqtt/dbus/dbus_types.py,sha256=bUik8LWPnLLJhhJExPuvyn1_MmkUjTn4jxXh3EkYgzI,495
|
|
8
|
-
dbus2mqtt/dbus/dbus_util.py,sha256=kAqA9SPR1N45fzXeXmBXlhulFEIFKrOIvr_LeygO928,569
|
|
9
|
-
dbus2mqtt/flow/__init__.py,sha256=tAL-CjXQHq_tGTKctIdOZ5teVKBtcJs6Astq_RdruV8,1540
|
|
10
|
-
dbus2mqtt/flow/flow_processor.py,sha256=v5LvcNe-IpEXkWgBW0mK76khrqb5aFkZF0Nw2IvxIEs,7834
|
|
11
|
-
dbus2mqtt/flow/actions/context_set.py,sha256=dIT39MJJVb0wuRI_ZM3ssnXYfa-iyB4o_UZD-1BZL2g,1087
|
|
12
|
-
dbus2mqtt/flow/actions/mqtt_publish.py,sha256=-qQtyc1KspPzAus3AK0iO629RP3RIAMwUecMqqBIrRY,1878
|
|
13
|
-
dbus2mqtt/mqtt/mqtt_client.py,sha256=xUJzkkv2uE3xWy0-SINeRjwfMNnDNLytdxEe3ahIIvk,4006
|
|
14
|
-
dbus2mqtt/template/dbus_template_functions.py,sha256=mSZr4s7XzmMCYnJYV1MlBWOBz71MGEBj6mLJzIapNf8,2427
|
|
15
|
-
dbus2mqtt/template/templating.py,sha256=phmh18uslexw1CmjYHotMHc4zfzIEUflwLnrnBGfAVs,4096
|
|
16
|
-
dbus2mqtt-0.2.0.dist-info/METADATA,sha256=qwjAjBYoDxEsbFkwZXnhEW7hzwXICEd4qFj8sdo8xTw,7750
|
|
17
|
-
dbus2mqtt-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
-
dbus2mqtt-0.2.0.dist-info/entry_points.txt,sha256=pmmacoHCsvTTUB5dIPaY4i0H9gX7BQlpd3rBU-Jbv3A,50
|
|
19
|
-
dbus2mqtt-0.2.0.dist-info/licenses/LICENSE,sha256=a4bIEgyA9rrnAfUN90CgbgZ6BQIFHeABkk0JihiBaxM,1074
|
|
20
|
-
dbus2mqtt-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|