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.

@@ -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, Any] | None = None
54
- interval: dict[str, Any] | None = None
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, Any] | None = None
84
- global_context: dict[str, Any] | None = None
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
@@ -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 dbus_next
9
- import dbus_next.aio as dbus_aio
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 _dbus_next_signal_publisher(self, dbus_signal_state: dict[str, Any], *args):
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 _dbus_next_signal_handler(self, signal: dbus_introspection.Signal, state: dict[str, Any]) -> Any:
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._dbus_next_signal_publisher(state, a)
99
+ return lambda a: self._dbus_fast_signal_publisher(state, a)
104
100
  elif expected_args == 2:
105
- return lambda a, b: self._dbus_next_signal_publisher(state, a, b)
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._dbus_next_signal_publisher(state, a, b, c)
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._dbus_next_signal_publisher(state, a, b, c, d)
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._dbus_next_signal_handler(interface_signal, dbus_signal_state)
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.bus.introspect(bus_name, path)
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
- context = {
249
+ trigger_context = {
238
250
  "bus_name": bus_name,
251
+ "path": path
239
252
  }
240
- trigger_message = FlowTriggerMessage(flow, trigger, datetime.now(), context)
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
- context = {
302
+ trigger_context = {
285
303
  "bus_name": bus_name,
304
+ "path": path
286
305
  }
287
- trigger_message = FlowTriggerMessage(flow, trigger, datetime.now(), context)
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
- res = await interface.__getattribute__(call_method_name)(value)
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
- return res
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
- context = {
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(flow, trigger, datetime.now(), context)
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:
@@ -2,14 +2,14 @@
2
2
 
3
3
  from dataclasses import dataclass
4
4
 
5
- import dbus_next.aio as dbus_aio
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, bus_name: str):
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
 
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import re
3
3
 
4
- import dbus_next.signature as dbus_signature
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 dbus_next.signature.Variant types
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
@@ -37,7 +37,7 @@ class FlowTriggerMessage:
37
37
  flow_config: FlowConfig
38
38
  flow_trigger_config: FlowTriggerConfig
39
39
  timestamp: datetime
40
- context: dict[str, Any] | None = None
40
+ trigger_context: dict[str, Any] | None = None
41
41
 
42
42
  class EventBroker:
43
43
  def __init__(self):
@@ -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
@@ -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
- **trigger.interval
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
- **trigger.cron
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
- logger.warning(f"flow_processor_task: Exception {e}", exc_info=True)
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
- log_message = f"on_trigger: {flow_trigger_message.flow_trigger_config.type}, time={flow_trigger_message.timestamp.isoformat()}"
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.context)
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 dbus_next.aio as dbus_aio
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
- mqtt_client = MqttClient(app_context)
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
- # unless specified otherwise, load config from config.yaml
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
- asyncio.run(run(config))
126
+ try:
127
+ asyncio.run(run(config))
128
+ except KeyboardInterrupt:
129
+ return 0
@@ -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 isinstance(msg.payload, dict):
58
- if type == "json":
59
- payload = json.dumps(msg.payload)
60
- elif type == "yaml":
61
- payload = yaml.dump(msg.payload)
62
- elif type == "text":
63
- payload = str(payload)
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
- logger.debug(f"mqtt_publish_queue_processor_task: payload={payload}")
66
- self.client.publish(topic=msg.topic, payload=payload)
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={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=True)
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 dbus_next.constants import ErrorType
7
- from dbus_next.errors import DBusError
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
 
@@ -41,6 +41,9 @@ class TemplateEngine:
41
41
 
42
42
  def _convert_value(self, res: Any, res_type: type[TemplateResultType]) -> TemplateResultType:
43
43
 
44
+ if res is None:
45
+ return res
46
+
44
47
  if isinstance(res, res_type):
45
48
  return res
46
49
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbus2mqtt
3
- Version: 0.2.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-next>=0.2.3
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
- sudo docker pull jwnmulder/dbus2mqtt
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
- # dbus_next configuration
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,,
@@ -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,,