dbus2mqtt 0.3.1__py3-none-any.whl → 0.4.0__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/__init__.py +24 -5
- dbus2mqtt/dbus/dbus_client.py +460 -117
- dbus2mqtt/dbus/dbus_types.py +17 -3
- dbus2mqtt/dbus/dbus_util.py +12 -11
- dbus2mqtt/event_broker.py +1 -21
- dbus2mqtt/flow/flow_processor.py +19 -5
- dbus2mqtt/main.py +17 -3
- dbus2mqtt/mqtt/mqtt_client.py +9 -2
- dbus2mqtt/template/dbus_template_functions.py +2 -2
- {dbus2mqtt-0.3.1.dist-info → dbus2mqtt-0.4.0.dist-info}/METADATA +5 -5
- dbus2mqtt-0.4.0.dist-info/RECORD +23 -0
- dbus2mqtt-0.3.1.dist-info/RECORD +0 -23
- {dbus2mqtt-0.3.1.dist-info → dbus2mqtt-0.4.0.dist-info}/WHEEL +0 -0
- {dbus2mqtt-0.3.1.dist-info → dbus2mqtt-0.4.0.dist-info}/entry_points.txt +0 -0
- {dbus2mqtt-0.3.1.dist-info → dbus2mqtt-0.4.0.dist-info}/licenses/LICENSE +0 -0
dbus2mqtt/config/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fnmatch
|
|
2
2
|
import uuid
|
|
3
|
+
import warnings
|
|
3
4
|
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
from typing import Annotated, Any, Literal
|
|
@@ -65,15 +66,29 @@ class FlowTriggerDbusSignalConfig:
|
|
|
65
66
|
@dataclass
|
|
66
67
|
class FlowTriggerBusNameAddedConfig:
|
|
67
68
|
type: Literal["bus_name_added"] = "bus_name_added"
|
|
68
|
-
|
|
69
|
+
|
|
70
|
+
def __post_init__(self):
|
|
71
|
+
warnings.warn(f"{self.type} flow trigger may be removed in a future version.", DeprecationWarning, stacklevel=2)
|
|
69
72
|
|
|
70
73
|
@dataclass
|
|
71
74
|
class FlowTriggerBusNameRemovedConfig:
|
|
72
75
|
type: Literal["bus_name_removed"] = "bus_name_removed"
|
|
76
|
+
|
|
77
|
+
def __post_init__(self):
|
|
78
|
+
warnings.warn(f"{self.type} flow trigger may be removed in a future version.", DeprecationWarning, stacklevel=2)
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class FlowTriggerObjectAddedConfig:
|
|
82
|
+
type: Literal["object_added"] = "object_added"
|
|
83
|
+
# filter: str | None = None
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class FlowTriggerObjectRemovedConfig:
|
|
87
|
+
type: Literal["object_removed"] = "object_removed"
|
|
73
88
|
# filter: str | None = None
|
|
74
89
|
|
|
75
90
|
FlowTriggerConfig = Annotated[
|
|
76
|
-
FlowTriggerMqttConfig | FlowTriggerScheduleConfig | FlowTriggerDbusSignalConfig | FlowTriggerBusNameAddedConfig | FlowTriggerBusNameRemovedConfig,
|
|
91
|
+
FlowTriggerMqttConfig | FlowTriggerScheduleConfig | FlowTriggerDbusSignalConfig | FlowTriggerBusNameAddedConfig | FlowTriggerBusNameRemovedConfig | FlowTriggerObjectAddedConfig | FlowTriggerObjectRemovedConfig,
|
|
77
92
|
Field(discriminator="type")
|
|
78
93
|
]
|
|
79
94
|
|
|
@@ -117,6 +132,7 @@ class SubscriptionConfig:
|
|
|
117
132
|
@dataclass
|
|
118
133
|
class DbusConfig:
|
|
119
134
|
subscriptions: list[SubscriptionConfig]
|
|
135
|
+
bus_type: Literal["SESSION", "SYSTEM"] = "SESSION"
|
|
120
136
|
|
|
121
137
|
def is_bus_name_configured(self, bus_name: str) -> bool:
|
|
122
138
|
|
|
@@ -125,11 +141,14 @@ class DbusConfig:
|
|
|
125
141
|
return True
|
|
126
142
|
return False
|
|
127
143
|
|
|
128
|
-
def get_subscription_configs(self, bus_name: str, path: str) -> list[SubscriptionConfig]:
|
|
144
|
+
def get_subscription_configs(self, bus_name: str, path: str|None = None) -> list[SubscriptionConfig]:
|
|
129
145
|
res: list[SubscriptionConfig] = []
|
|
130
146
|
for subscription in self.subscriptions:
|
|
131
|
-
if fnmatch.fnmatchcase(bus_name, subscription.bus_name)
|
|
132
|
-
|
|
147
|
+
if fnmatch.fnmatchcase(bus_name, subscription.bus_name):
|
|
148
|
+
if not path or path == subscription.path:
|
|
149
|
+
res.append(subscription)
|
|
150
|
+
elif fnmatch.fnmatchcase(path, subscription.path):
|
|
151
|
+
res.append(subscription)
|
|
133
152
|
return res
|
|
134
153
|
|
|
135
154
|
@dataclass
|
dbus2mqtt/dbus/dbus_client.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fnmatch
|
|
1
2
|
import json
|
|
2
3
|
import logging
|
|
3
4
|
|
|
@@ -5,11 +6,19 @@ from datetime import datetime
|
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
import dbus_fast.aio as dbus_aio
|
|
9
|
+
import dbus_fast.constants as dbus_constants
|
|
10
|
+
import dbus_fast.errors as dbus_errors
|
|
8
11
|
import dbus_fast.introspection as dbus_introspection
|
|
12
|
+
import dbus_fast.message as dbus_message
|
|
13
|
+
import janus
|
|
9
14
|
|
|
10
15
|
from dbus2mqtt import AppContext
|
|
11
|
-
from dbus2mqtt.config import
|
|
12
|
-
from dbus2mqtt.dbus.dbus_types import
|
|
16
|
+
from dbus2mqtt.config import SubscriptionConfig
|
|
17
|
+
from dbus2mqtt.dbus.dbus_types import (
|
|
18
|
+
BusNameSubscriptions,
|
|
19
|
+
DbusSignalWithState,
|
|
20
|
+
SubscribedInterface,
|
|
21
|
+
)
|
|
13
22
|
from dbus2mqtt.dbus.dbus_util import (
|
|
14
23
|
camel_to_snake,
|
|
15
24
|
unwrap_dbus_object,
|
|
@@ -19,11 +28,13 @@ from dbus2mqtt.dbus.introspection_patches.mpris_playerctl import (
|
|
|
19
28
|
mpris_introspection_playerctl,
|
|
20
29
|
)
|
|
21
30
|
from dbus2mqtt.dbus.introspection_patches.mpris_vlc import mpris_introspection_vlc
|
|
22
|
-
from dbus2mqtt.event_broker import
|
|
31
|
+
from dbus2mqtt.event_broker import MqttMessage
|
|
23
32
|
from dbus2mqtt.flow.flow_processor import FlowScheduler, FlowTriggerMessage
|
|
24
33
|
|
|
25
34
|
logger = logging.getLogger(__name__)
|
|
26
35
|
|
|
36
|
+
# TODO: Redo flow registration in _handle_bus_name_added, might want to move that to a separate file
|
|
37
|
+
# TODO: deregister signal watcher on shutdown
|
|
27
38
|
|
|
28
39
|
class DbusClient:
|
|
29
40
|
|
|
@@ -36,6 +47,13 @@ class DbusClient:
|
|
|
36
47
|
self.flow_scheduler = flow_scheduler
|
|
37
48
|
self.subscriptions: dict[str, BusNameSubscriptions] = {}
|
|
38
49
|
|
|
50
|
+
self._dbus_signal_queue = janus.Queue[DbusSignalWithState]()
|
|
51
|
+
self._dbus_object_lifecycle_signal_queue = janus.Queue[dbus_message.Message]()
|
|
52
|
+
|
|
53
|
+
self._name_owner_match_rule = "sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',path='/org/freedesktop/DBus',member='NameOwnerChanged'"
|
|
54
|
+
self._interfaces_added_match_rule = "interface='org.freedesktop.DBus.ObjectManager',type='signal',member='InterfacesAdded'"
|
|
55
|
+
self._interfaces_removed_match_rule = "interface='org.freedesktop.DBus.ObjectManager',type='signal',member='InterfacesRemoved'"
|
|
56
|
+
|
|
39
57
|
async def connect(self):
|
|
40
58
|
|
|
41
59
|
if not self.bus.connected:
|
|
@@ -46,37 +64,124 @@ class DbusClient:
|
|
|
46
64
|
else:
|
|
47
65
|
logger.warning(f"Failed to connect to {self.bus._bus_address}")
|
|
48
66
|
|
|
67
|
+
self.bus.add_message_handler(self.object_lifecycle_signal_handler)
|
|
68
|
+
|
|
69
|
+
# Add dbus match rules to get notified of new bus_names or interfaces
|
|
70
|
+
await self._add_match_rule(self._name_owner_match_rule)
|
|
71
|
+
await self._add_match_rule(self._interfaces_added_match_rule)
|
|
72
|
+
await self._add_match_rule(self._interfaces_removed_match_rule)
|
|
73
|
+
|
|
49
74
|
introspection = await self.bus.introspect('org.freedesktop.DBus', '/org/freedesktop/DBus')
|
|
50
75
|
obj = self.bus.get_proxy_object('org.freedesktop.DBus', '/org/freedesktop/DBus', introspection)
|
|
51
76
|
dbus_interface = obj.get_interface('org.freedesktop.DBus')
|
|
52
77
|
|
|
53
|
-
# subscribe to NameOwnerChanged which allows us to detect new bus_names
|
|
54
|
-
dbus_interface.__getattribute__("on_name_owner_changed")(self._dbus_name_owner_changed_callback)
|
|
55
|
-
|
|
56
78
|
# subscribe to existing registered bus_names we are interested in
|
|
57
79
|
connected_bus_names = await dbus_interface.__getattribute__("call_list_names")()
|
|
58
80
|
|
|
59
|
-
|
|
81
|
+
new_subscribed_interfaces: list[SubscribedInterface] = []
|
|
60
82
|
for bus_name in connected_bus_names:
|
|
61
|
-
|
|
83
|
+
new_subscribed_interfaces.extend(await self._handle_bus_name_added(bus_name))
|
|
84
|
+
|
|
85
|
+
logger.info(f"subscriptions on startup: {list(set([si.bus_name for si in new_subscribed_interfaces]))}")
|
|
86
|
+
|
|
87
|
+
async def _add_match_rule(self, match_rule: str):
|
|
88
|
+
reply = await self.bus.call(dbus_message.Message(
|
|
89
|
+
destination='org.freedesktop.DBus',
|
|
90
|
+
path='/org/freedesktop/DBus',
|
|
91
|
+
interface='org.freedesktop.DBus',
|
|
92
|
+
member='AddMatch',
|
|
93
|
+
signature='s',
|
|
94
|
+
body=[(match_rule)]
|
|
95
|
+
))
|
|
96
|
+
assert reply and reply.message_type == dbus_constants.MessageType.METHOD_RETURN
|
|
97
|
+
|
|
98
|
+
async def _remove_match_rule(self, match_rule: str):
|
|
99
|
+
reply = await self.bus.call(dbus_message.Message(
|
|
100
|
+
destination='org.freedesktop.DBus',
|
|
101
|
+
path='/org/freedesktop/DBus',
|
|
102
|
+
interface='org.freedesktop.DBus',
|
|
103
|
+
member='RemoveMatch',
|
|
104
|
+
signature='s',
|
|
105
|
+
body=[(match_rule)]
|
|
106
|
+
))
|
|
107
|
+
assert reply and reply.message_type == dbus_constants.MessageType.METHOD_RETURN
|
|
108
|
+
|
|
109
|
+
def get_well_known_bus_name(self, unique_bus_name: str) -> str:
|
|
110
|
+
|
|
111
|
+
for bns in self.subscriptions.values():
|
|
112
|
+
if unique_bus_name == bns.unique_name:
|
|
113
|
+
return bns.bus_name
|
|
114
|
+
|
|
115
|
+
# # dbus_fast keeps track of well known bus_names for the high-level API.
|
|
116
|
+
# # We can use this to find the bus_name for the sender.
|
|
117
|
+
# for k, v in self.bus._name_owners.items():
|
|
118
|
+
# if v == unique_bus_name:
|
|
119
|
+
# return v
|
|
120
|
+
|
|
121
|
+
return unique_bus_name
|
|
122
|
+
|
|
123
|
+
async def get_unique_name(self, name) -> str | None:
|
|
124
|
+
|
|
125
|
+
if name.startswith(":"):
|
|
126
|
+
return name
|
|
127
|
+
|
|
128
|
+
introspect = await self.bus.introspect("org.freedesktop.DBus", "/org/freedesktop/DBus")
|
|
129
|
+
proxy = self.bus.get_proxy_object("org.freedesktop.DBus", "/org/freedesktop/DBus", introspect)
|
|
130
|
+
dbus_interface = proxy.get_interface("org.freedesktop.DBus")
|
|
131
|
+
|
|
132
|
+
return await dbus_interface.call_get_name_owner(name) # type: ignore
|
|
133
|
+
|
|
134
|
+
def object_lifecycle_signal_handler(self, message: dbus_message.Message) -> None:
|
|
135
|
+
|
|
136
|
+
if not message.message_type == dbus_constants.MessageType.SIGNAL:
|
|
137
|
+
return
|
|
62
138
|
|
|
63
|
-
|
|
139
|
+
logger.debug(f'object_lifecycle_signal_handler: interface={message.interface}, member={message.member}, body={message.body}')
|
|
64
140
|
|
|
65
|
-
|
|
141
|
+
if message.interface in ['org.freedesktop.DBus', 'org.freedesktop.DBus.ObjectManager']:
|
|
142
|
+
self._dbus_object_lifecycle_signal_queue.sync_q.put(message)
|
|
66
143
|
|
|
67
|
-
|
|
144
|
+
def get_bus_name_subscriptions(self, bus_name: str) -> BusNameSubscriptions | None:
|
|
145
|
+
|
|
146
|
+
return self.subscriptions.get(bus_name)
|
|
147
|
+
|
|
148
|
+
def get_subscribed_proxy_object(self, bus_name: str, path: str) -> dbus_aio.proxy_object.ProxyObject | None:
|
|
149
|
+
|
|
150
|
+
bus_name_subscriptions = self.get_bus_name_subscriptions(bus_name)
|
|
68
151
|
if bus_name_subscriptions:
|
|
69
152
|
proxy_object = bus_name_subscriptions.path_objects.get(path)
|
|
70
153
|
if proxy_object:
|
|
71
154
|
return proxy_object
|
|
72
155
|
|
|
156
|
+
async def get_subscribed_or_new_proxy_object(self, bus_name: str, path: str) -> dbus_aio.proxy_object.ProxyObject | None:
|
|
157
|
+
|
|
158
|
+
proxy_object = self.get_subscribed_proxy_object(bus_name, path)
|
|
159
|
+
if proxy_object:
|
|
160
|
+
return proxy_object
|
|
161
|
+
|
|
162
|
+
# No existing subscription that contains the requested proxy_object
|
|
163
|
+
logger.warning(f"Returning temporary proxy_object with an additional introspection call, bus_name={bus_name}, path={path}")
|
|
164
|
+
introspection = await self.bus.introspect(bus_name=bus_name, path=path)
|
|
165
|
+
proxy_object = self.bus.get_proxy_object(bus_name, path, introspection)
|
|
166
|
+
if proxy_object:
|
|
167
|
+
return proxy_object
|
|
168
|
+
|
|
73
169
|
return None
|
|
74
170
|
|
|
75
|
-
def
|
|
171
|
+
async def _create_proxy_object_subscription(self, bus_name: str, path: str, introspection: dbus_introspection.Node):
|
|
76
172
|
|
|
77
|
-
bus_name_subscriptions = self.
|
|
173
|
+
bus_name_subscriptions = self.get_bus_name_subscriptions(bus_name)
|
|
78
174
|
if not bus_name_subscriptions:
|
|
79
|
-
|
|
175
|
+
|
|
176
|
+
if bus_name.startswith(":"):
|
|
177
|
+
unique_name = bus_name
|
|
178
|
+
else:
|
|
179
|
+
# make sure we have both the well known and unique bus_name
|
|
180
|
+
unique_name = await self.get_unique_name(bus_name)
|
|
181
|
+
|
|
182
|
+
assert unique_name is not None
|
|
183
|
+
|
|
184
|
+
bus_name_subscriptions = BusNameSubscriptions(bus_name, unique_name)
|
|
80
185
|
self.subscriptions[bus_name] = bus_name_subscriptions
|
|
81
186
|
|
|
82
187
|
proxy_object = bus_name_subscriptions.path_objects.get(path)
|
|
@@ -87,10 +192,25 @@ class DbusClient:
|
|
|
87
192
|
return proxy_object, bus_name_subscriptions
|
|
88
193
|
|
|
89
194
|
def _dbus_fast_signal_publisher(self, dbus_signal_state: dict[str, Any], *args):
|
|
195
|
+
"""publish a dbus signal to the event broker, one for each subscription_config"""
|
|
196
|
+
|
|
90
197
|
unwrapped_args = unwrap_dbus_objects(args)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
198
|
+
|
|
199
|
+
signal_subscriptions = dbus_signal_state["signal_subscriptions"]
|
|
200
|
+
for signal_subscription in signal_subscriptions:
|
|
201
|
+
subscription_config = signal_subscription["subscription_config"]
|
|
202
|
+
signal_config = signal_subscription["signal_config"]
|
|
203
|
+
|
|
204
|
+
self._dbus_signal_queue.sync_q.put(
|
|
205
|
+
DbusSignalWithState(
|
|
206
|
+
bus_name=dbus_signal_state["bus_name"],
|
|
207
|
+
path=dbus_signal_state["path"],
|
|
208
|
+
interface_name=dbus_signal_state["interface_name"],
|
|
209
|
+
subscription_config=subscription_config,
|
|
210
|
+
signal_config=signal_config,
|
|
211
|
+
args=unwrapped_args
|
|
212
|
+
)
|
|
213
|
+
)
|
|
94
214
|
|
|
95
215
|
def _dbus_fast_signal_handler(self, signal: dbus_introspection.Signal, state: dict[str, Any]) -> Any:
|
|
96
216
|
expected_args = len(signal.args)
|
|
@@ -105,58 +225,81 @@ class DbusClient:
|
|
|
105
225
|
return lambda a, b, c, d: self._dbus_fast_signal_publisher(state, a, b, c, d)
|
|
106
226
|
raise ValueError("Unsupported nr of arguments")
|
|
107
227
|
|
|
108
|
-
async def
|
|
228
|
+
async def _subscribe_interface_signals(self, bus_name: str, path: str, interface: dbus_introspection.Interface, configured_signals: dict[str, list[dict]]) -> int:
|
|
229
|
+
|
|
230
|
+
proxy_object = self.get_subscribed_proxy_object(bus_name, path)
|
|
231
|
+
assert proxy_object is not None
|
|
109
232
|
|
|
110
|
-
proxy_object, bus_name_subscriptions = self._ensure_proxy_object_subscription(bus_name, path, introspection)
|
|
111
233
|
obj_interface = proxy_object.get_interface(interface.name)
|
|
112
234
|
|
|
113
235
|
interface_signals = dict((s.name, s) for s in interface.signals)
|
|
114
236
|
|
|
115
237
|
logger.debug(f"subscribe: bus_name={bus_name}, path={path}, interface={interface.name}, proxy_interface: signals={list(interface_signals.keys())}")
|
|
238
|
+
signal_subscription_count = 0
|
|
116
239
|
|
|
117
|
-
for
|
|
118
|
-
interface_signal = interface_signals.get(
|
|
240
|
+
for signal, signal_subscriptions in configured_signals.items():
|
|
241
|
+
interface_signal = interface_signals.get(signal)
|
|
119
242
|
if interface_signal:
|
|
120
243
|
|
|
121
|
-
on_signal_method_name = "on_" + camel_to_snake(
|
|
244
|
+
on_signal_method_name = "on_" + camel_to_snake(signal)
|
|
122
245
|
dbus_signal_state = {
|
|
123
246
|
"bus_name": bus_name,
|
|
124
247
|
"path": path,
|
|
125
248
|
"interface_name": interface.name,
|
|
126
|
-
"
|
|
127
|
-
"signal_config": signal_config,
|
|
249
|
+
"signal_subscriptions": signal_subscriptions
|
|
128
250
|
}
|
|
129
251
|
|
|
130
252
|
handler = self._dbus_fast_signal_handler(interface_signal, dbus_signal_state)
|
|
131
253
|
obj_interface.__getattribute__(on_signal_method_name)(handler)
|
|
132
|
-
logger.info(f"subscribed with signal_handler: signal={
|
|
254
|
+
logger.info(f"subscribed with signal_handler: signal={signal}, bus_name={bus_name}, path={path}, interface={interface.name}")
|
|
255
|
+
|
|
256
|
+
signal_subscription_count += 1
|
|
133
257
|
|
|
134
258
|
else:
|
|
135
|
-
logger.warning(f"Invalid signal: signal={
|
|
259
|
+
logger.warning(f"Invalid signal: signal={signal}, bus_name={bus_name}, path={path}, interface={interface.name}")
|
|
136
260
|
|
|
137
|
-
return
|
|
138
|
-
bus_name=bus_name,
|
|
139
|
-
path=path,
|
|
140
|
-
interface_name=interface.name,
|
|
141
|
-
subscription_config=subscription_config,
|
|
142
|
-
interface_config=si
|
|
143
|
-
)
|
|
261
|
+
return signal_subscription_count
|
|
144
262
|
|
|
145
263
|
async def _process_interface(self, bus_name: str, path: str, introspection: dbus_introspection.Node, interface: dbus_introspection.Interface) -> list[SubscribedInterface]:
|
|
146
264
|
|
|
147
265
|
logger.debug(f"process_interface: {bus_name}, {path}, {interface.name}")
|
|
148
|
-
|
|
266
|
+
|
|
149
267
|
new_subscriptions: list[SubscribedInterface] = []
|
|
268
|
+
configured_signals: dict[str, list[dict[str, Any]]] = {}
|
|
269
|
+
|
|
270
|
+
subscription_configs = self.config.get_subscription_configs(bus_name, path)
|
|
150
271
|
for subscription in subscription_configs:
|
|
151
272
|
logger.debug(f"processing subscription config: {subscription.bus_name}, {subscription.path}")
|
|
152
273
|
for subscription_interface in subscription.interfaces:
|
|
153
274
|
if subscription_interface.interface == interface.name:
|
|
154
275
|
logger.debug(f"matching config found for bus_name={bus_name}, path={path}, interface={interface.name}")
|
|
155
|
-
subscribed_iterface = await self._subscribe_interface(bus_name, path, introspection, interface, subscription, subscription_interface)
|
|
156
276
|
|
|
157
|
-
|
|
277
|
+
# Determine signals we need to subscribe to
|
|
278
|
+
for signal_config in subscription_interface.signals:
|
|
279
|
+
signal_subscriptions = configured_signals.get(signal_config.signal, [])
|
|
280
|
+
signal_subscriptions.append({
|
|
281
|
+
"signal_config": signal_config,
|
|
282
|
+
"subscription_config": subscription
|
|
283
|
+
})
|
|
284
|
+
configured_signals[signal_config.signal] = signal_subscriptions
|
|
285
|
+
|
|
286
|
+
if subscription_interface.signals:
|
|
287
|
+
new_subscriptions.append(SubscribedInterface(
|
|
288
|
+
bus_name=bus_name,
|
|
289
|
+
path=path,
|
|
290
|
+
interface_name=interface.name,
|
|
291
|
+
subscription_config=subscription
|
|
292
|
+
))
|
|
293
|
+
|
|
294
|
+
if len(configured_signals) > 0:
|
|
295
|
+
|
|
296
|
+
signal_subscription_count = await self._subscribe_interface_signals(
|
|
297
|
+
bus_name, path, interface, configured_signals
|
|
298
|
+
)
|
|
299
|
+
if signal_subscription_count > 0:
|
|
300
|
+
return new_subscriptions
|
|
158
301
|
|
|
159
|
-
return
|
|
302
|
+
return []
|
|
160
303
|
|
|
161
304
|
async def _introspect(self, bus_name: str, path: str) -> dbus_introspection.Node:
|
|
162
305
|
|
|
@@ -174,93 +317,107 @@ class DbusClient:
|
|
|
174
317
|
|
|
175
318
|
return introspection
|
|
176
319
|
|
|
177
|
-
async def
|
|
320
|
+
async def _list_bus_name_paths(self, bus_name: str, path: str) -> list[str]:
|
|
321
|
+
"""list all nested paths. Only paths that have interfaces are returned"""
|
|
178
322
|
|
|
179
|
-
|
|
323
|
+
paths: list[str] = []
|
|
180
324
|
|
|
181
325
|
try:
|
|
182
326
|
introspection = await self._introspect(bus_name, path)
|
|
183
327
|
except TypeError as e:
|
|
184
328
|
logger.warning(f"bus.introspect failed, bus_name={bus_name}, path={path}: {e}")
|
|
185
|
-
return
|
|
329
|
+
return paths
|
|
186
330
|
|
|
187
331
|
if len(introspection.nodes) == 0:
|
|
188
332
|
logger.debug(f"leaf node: bus_name={bus_name}, path={path}, is_root={introspection.is_root}, interfaces={[i.name for i in introspection.interfaces]}")
|
|
189
333
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
await self._process_interface(bus_name, path, introspection, interface)
|
|
193
|
-
)
|
|
334
|
+
if len(introspection.interfaces) > 0:
|
|
335
|
+
paths.append(path)
|
|
194
336
|
|
|
195
337
|
for node in introspection.nodes:
|
|
196
338
|
path_seperator = "" if path.endswith('/') else "/"
|
|
197
|
-
|
|
198
|
-
await self.
|
|
339
|
+
paths.extend(
|
|
340
|
+
await self._list_bus_name_paths(bus_name, f"{path}{path_seperator}{node.name}")
|
|
199
341
|
)
|
|
200
342
|
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
async def _handle_bus_name_added(self, bus_name: str) -> list[SubscribedInterface]:
|
|
343
|
+
return paths
|
|
204
344
|
|
|
345
|
+
async def _subscribe_dbus_object(self, bus_name: str, path: str) -> list[SubscribedInterface]:
|
|
346
|
+
"""Subscribes to a dbus object at the given bus_name and path.
|
|
347
|
+
For each matching subscription config, subscribe to all configured interfaces,
|
|
348
|
+
start listening to signals and start/register flows if configured.
|
|
349
|
+
"""
|
|
205
350
|
if not self.config.is_bus_name_configured(bus_name):
|
|
206
351
|
return []
|
|
207
352
|
|
|
208
|
-
|
|
209
|
-
for umh in self.bus._user_message_handlers:
|
|
210
|
-
umh_bus_name = umh.__self__.bus_name
|
|
211
|
-
if umh_bus_name == bus_name:
|
|
212
|
-
logger.warning(f"handle_bus_name_added: {umh_bus_name} already added")
|
|
353
|
+
new_subscriptions: list[SubscribedInterface] = []
|
|
213
354
|
|
|
214
|
-
|
|
355
|
+
try:
|
|
356
|
+
introspection = await self._introspect(bus_name, path)
|
|
357
|
+
except TypeError as e:
|
|
358
|
+
logger.warning(f"bus.introspect failed, bus_name={bus_name}, path={path}: {e}")
|
|
359
|
+
return new_subscriptions
|
|
215
360
|
|
|
216
|
-
|
|
361
|
+
if len(introspection.interfaces) == 0:
|
|
362
|
+
logger.warning(f"Skipping dbus_object subscription, no interfaces found for bus_name={bus_name}, path={path}")
|
|
363
|
+
return new_subscriptions
|
|
217
364
|
|
|
218
|
-
|
|
219
|
-
|
|
365
|
+
interfaces_names = [i.name for i in introspection.interfaces]
|
|
366
|
+
logger.info(f"subscribe_dbus_object: bus_name={bus_name}, path={path}, interfaces={interfaces_names}")
|
|
220
367
|
|
|
221
|
-
|
|
222
|
-
# create a FlowProcessor per bus_name/path subscription?
|
|
223
|
-
# One global or a per subscription FlowProcessor.flow_processor_task?
|
|
224
|
-
# Start a new timer job, but leverage existing FlowScheduler
|
|
225
|
-
# How does the FlowScheduler now it should invoke the local FlowPocessor?
|
|
226
|
-
# Maybe use queues to communicate from here with the FlowProcessor?
|
|
227
|
-
# e.g.: StartFlows, StopFlows,
|
|
368
|
+
await self._create_proxy_object_subscription(bus_name, path, introspection)
|
|
228
369
|
|
|
229
|
-
for
|
|
370
|
+
for interface in introspection.interfaces:
|
|
371
|
+
new_subscriptions.extend(
|
|
372
|
+
await self._process_interface(bus_name, path, introspection, interface)
|
|
373
|
+
)
|
|
230
374
|
|
|
231
|
-
|
|
232
|
-
if subscription_config.id not in processed_new_subscriptions:
|
|
375
|
+
return new_subscriptions
|
|
233
376
|
|
|
234
|
-
|
|
235
|
-
self.flow_scheduler.start_flow_set(subscription_config.flows)
|
|
377
|
+
async def _handle_bus_name_added(self, bus_name: str) -> list[SubscribedInterface]:
|
|
236
378
|
|
|
237
|
-
|
|
238
|
-
await self._trigger_bus_name_added(subscription_config, subscribed_interface.bus_name, subscribed_interface.path)
|
|
379
|
+
logger.debug(f"_handle_bus_name_added: bus_name={bus_name}")
|
|
239
380
|
|
|
240
|
-
|
|
381
|
+
if not self.config.is_bus_name_configured(bus_name):
|
|
382
|
+
return []
|
|
241
383
|
|
|
242
|
-
|
|
384
|
+
object_paths = []
|
|
385
|
+
subscription_configs = self.config.get_subscription_configs(bus_name=bus_name)
|
|
386
|
+
for subscription_config in subscription_configs:
|
|
243
387
|
|
|
244
|
-
|
|
388
|
+
# if configured path is not a wildcard, use it
|
|
389
|
+
if "*" not in subscription_config.path:
|
|
390
|
+
object_paths.append(subscription_config.path)
|
|
391
|
+
else:
|
|
392
|
+
# if configured path is a wildcard, use introspection to find all paths
|
|
393
|
+
# and filter by subscription_config.path
|
|
394
|
+
introspected_paths = await self._list_bus_name_paths(bus_name, "/")
|
|
395
|
+
logger.debug(f"introspected paths for bus_name: {bus_name}, paths: {introspected_paths}")
|
|
396
|
+
for path in introspected_paths:
|
|
397
|
+
if fnmatch.fnmatchcase(path, subscription_config.path):
|
|
398
|
+
object_paths.append(path)
|
|
245
399
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
400
|
+
# dedupe
|
|
401
|
+
object_paths = list(set(object_paths))
|
|
402
|
+
|
|
403
|
+
new_subscribed_interfaces = []
|
|
404
|
+
|
|
405
|
+
# for each object path, call _subscribe_dbus_object
|
|
406
|
+
for object_path in object_paths:
|
|
407
|
+
subscribed_object_interfaces = await self._subscribe_dbus_object(bus_name, object_path)
|
|
408
|
+
new_subscribed_interfaces.extend(subscribed_object_interfaces)
|
|
409
|
+
|
|
410
|
+
# start all flows for the new subscriptions
|
|
411
|
+
if len(new_subscribed_interfaces) > 0:
|
|
412
|
+
await self._start_subscription_flows(bus_name, new_subscribed_interfaces)
|
|
413
|
+
|
|
414
|
+
return new_subscribed_interfaces
|
|
260
415
|
|
|
261
416
|
async def _handle_bus_name_removed(self, bus_name: str):
|
|
262
417
|
|
|
263
|
-
|
|
418
|
+
logger.debug(f"_handle_bus_name_removed: bus_name={bus_name}")
|
|
419
|
+
|
|
420
|
+
bus_name_subscriptions = self.get_bus_name_subscriptions(bus_name)
|
|
264
421
|
|
|
265
422
|
if bus_name_subscriptions:
|
|
266
423
|
for path, proxy_object in bus_name_subscriptions.path_objects.items():
|
|
@@ -268,11 +425,16 @@ class DbusClient:
|
|
|
268
425
|
subscription_configs = self.config.get_subscription_configs(bus_name=bus_name, path=path)
|
|
269
426
|
for subscription_config in subscription_configs:
|
|
270
427
|
|
|
271
|
-
#
|
|
428
|
+
# Stop schedule triggers. Only done once per subscription_config
|
|
429
|
+
# TODO: Dont stop when other bus_names are using the same flowset
|
|
430
|
+
self.flow_scheduler.stop_flow_set(subscription_config.flows)
|
|
431
|
+
|
|
432
|
+
# Trigger flows that have a bus_name_removed trigger configured
|
|
272
433
|
await self._trigger_bus_name_removed(subscription_config, bus_name, path)
|
|
273
434
|
|
|
274
|
-
#
|
|
275
|
-
self.
|
|
435
|
+
# Trigger flows that have an object_removed trigger configured
|
|
436
|
+
await self._trigger_object_removed(subscription_config, bus_name, path)
|
|
437
|
+
|
|
276
438
|
|
|
277
439
|
# Wait for completion
|
|
278
440
|
await self.event_broker.flow_trigger_queue.async_q.join()
|
|
@@ -293,39 +455,193 @@ class DbusClient:
|
|
|
293
455
|
|
|
294
456
|
del self.subscriptions[bus_name]
|
|
295
457
|
|
|
296
|
-
async def
|
|
458
|
+
async def _handle_interfaces_added(self, bus_name: str, path: str) -> None:
|
|
459
|
+
"""
|
|
460
|
+
Handles the addition of new D-Bus interfaces for a given bus name and object path.
|
|
461
|
+
|
|
462
|
+
This method checks if there are subscription configurations for the specified bus name and path.
|
|
463
|
+
If so, it subscribes to the D-Bus object and starts the necessary subscription flows for any new interfaces.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
bus_name (str): The well-known name of the D-Bus service where the interface was added.
|
|
467
|
+
path (str): The object path on the D-Bus where the interface was added.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
logger.debug(f"_handle_interfaces_added: bus_name={bus_name}, path={path}")
|
|
471
|
+
|
|
472
|
+
if not self.config.get_subscription_configs(bus_name=bus_name, path=path):
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
new_subscribed_interfaces = await self._subscribe_dbus_object(bus_name, path)
|
|
476
|
+
|
|
477
|
+
# start all flows for the new subscriptions
|
|
478
|
+
if len(new_subscribed_interfaces) > 0:
|
|
479
|
+
await self._start_subscription_flows(bus_name, new_subscribed_interfaces)
|
|
480
|
+
|
|
481
|
+
async def _handle_interfaces_removed(self, bus_name: str, path: str) -> None:
|
|
482
|
+
|
|
483
|
+
logger.debug(f"_handle_interfaces_removed: bus_name={bus_name}, path={path}")
|
|
484
|
+
|
|
485
|
+
subscription_configs = self.config.get_subscription_configs(bus_name=bus_name, path=path)
|
|
486
|
+
for subscription_config in subscription_configs:
|
|
487
|
+
|
|
488
|
+
# Stop schedule triggers. Only done once per subscription_config and not per path
|
|
489
|
+
# TODO, only stop if this subscription is not used for any other objects / paths
|
|
490
|
+
self.flow_scheduler.stop_flow_set(subscription_config.flows)
|
|
491
|
+
|
|
492
|
+
# Trigger flows that have an object_removed trigger configured
|
|
493
|
+
await self._trigger_object_removed(subscription_config, bus_name, path)
|
|
494
|
+
|
|
495
|
+
proxy_object = self.get_subscribed_proxy_object(bus_name, path)
|
|
496
|
+
if proxy_object is not None:
|
|
497
|
+
|
|
498
|
+
# Wait for completion
|
|
499
|
+
await self.event_broker.flow_trigger_queue.async_q.join()
|
|
500
|
+
|
|
501
|
+
# clean up all dbus matchrules
|
|
502
|
+
for interface in proxy_object._interfaces.values():
|
|
503
|
+
proxy_interface: dbus_aio.proxy_object.ProxyInterface = interface
|
|
504
|
+
|
|
505
|
+
# officially you should do 'off_...' but the below is easier
|
|
506
|
+
# proxy_interface.off_properties_changed(self.on_properties_changed)
|
|
507
|
+
|
|
508
|
+
# clean lingering interface matchrule from bus
|
|
509
|
+
if proxy_interface._signal_match_rule in self.bus._match_rules.keys():
|
|
510
|
+
self.bus._remove_match_rule(proxy_interface._signal_match_rule)
|
|
511
|
+
|
|
512
|
+
# clean lingering interface messgage handler from bus
|
|
513
|
+
self.bus.remove_message_handler(proxy_interface._message_handler)
|
|
514
|
+
|
|
515
|
+
# For now that InterfacesRemoved signal means the entire object is removed form D-Bus
|
|
516
|
+
del self.subscriptions[bus_name].path_objects[path]
|
|
517
|
+
|
|
518
|
+
# cleanup the entire BusNameSubscriptions if no more objects are subscribed
|
|
519
|
+
bus_name_subscriptions = self.get_bus_name_subscriptions(bus_name)
|
|
520
|
+
if bus_name_subscriptions and len(bus_name_subscriptions.path_objects) == 0:
|
|
521
|
+
del self.subscriptions[bus_name]
|
|
522
|
+
|
|
523
|
+
async def _start_subscription_flows(self, bus_name: str, subscribed_interfaces: list[SubscribedInterface]):
|
|
524
|
+
"""Start all flows for the new subscriptions.
|
|
525
|
+
For each matching bus_name-path subscription_config, the following is done:
|
|
526
|
+
1. Ensure the scheduler is started, at most one scheduler will be active for a subscription_config
|
|
527
|
+
2. Trigger flows that have a bus_name_added trigger configured (only once per bus_name)
|
|
528
|
+
3. Trigger flows that have a interfaces_added trigger configured (once for each bus_name-path pair)
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
bus_name_object_paths = {}
|
|
532
|
+
bus_name_object_path_interfaces = {}
|
|
533
|
+
for si in subscribed_interfaces:
|
|
534
|
+
bus_name_object_paths.setdefault(si.bus_name, [])
|
|
535
|
+
bus_name_object_path_interfaces.setdefault(si.bus_name, {}).setdefault(si.path, [])
|
|
536
|
+
|
|
537
|
+
if si.path not in bus_name_object_paths[si.bus_name]:
|
|
538
|
+
bus_name_object_paths[si.bus_name].append(si.path)
|
|
539
|
+
|
|
540
|
+
bus_name_object_path_interfaces[si.bus_name][si.path].append(si.interface_name)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
# new_subscribed_bus_names = list(set([si.bus_name for si in subscribed_interfaces]))
|
|
544
|
+
# new_subscribed_bus_names_paths = {
|
|
545
|
+
# bus_name: list(set([si.path for si in subscribed_interfaces if si.bus_name == bus_name]))
|
|
546
|
+
# for bus_name in new_subscribed_bus_names
|
|
547
|
+
# }
|
|
548
|
+
|
|
549
|
+
logger.debug(f"_start_subscription_flows: ew_subscriptions: {list(bus_name_object_paths.keys())}")
|
|
550
|
+
logger.debug(f"_start_subscription_flows: new_bus_name_object_paths: {bus_name_object_paths}")
|
|
551
|
+
|
|
552
|
+
# setup and process triggers for each flow in each subscription
|
|
553
|
+
# just once per subscription_config
|
|
554
|
+
processed_new_subscriptions: set[str] = set()
|
|
555
|
+
|
|
556
|
+
# With all subscriptions in place, we can now ensure schedulers are created
|
|
557
|
+
# create a FlowProcessor per bus_name/path subscription?
|
|
558
|
+
# One global or a per subscription FlowProcessor.flow_processor_task?
|
|
559
|
+
# Start a new timer job, but leverage existing FlowScheduler
|
|
560
|
+
# How does the FlowScheduler now it should invoke the local FlowPocessor?
|
|
561
|
+
# Maybe use queues to communicate from here with the FlowProcessor?
|
|
562
|
+
# e.g.: StartFlows, StopFlows,
|
|
563
|
+
|
|
564
|
+
# for each bus_name
|
|
565
|
+
for bus_name, path_interfaces_map in bus_name_object_path_interfaces.items():
|
|
566
|
+
|
|
567
|
+
paths = list(path_interfaces_map.keys())
|
|
568
|
+
|
|
569
|
+
# for each path in the bus_name
|
|
570
|
+
for object_path in paths:
|
|
571
|
+
|
|
572
|
+
object_interfaces = path_interfaces_map[object_path]
|
|
573
|
+
|
|
574
|
+
# For each subscription_config that matches the bus_name and object_path
|
|
575
|
+
subscription_configs = self.config.get_subscription_configs(bus_name, object_path)
|
|
576
|
+
for subscription_config in subscription_configs:
|
|
577
|
+
|
|
578
|
+
# Only process subscription_config once, no matter how many paths it matches
|
|
579
|
+
if subscription_config.id not in processed_new_subscriptions:
|
|
580
|
+
|
|
581
|
+
# Ensure all schedulers are started
|
|
582
|
+
# If a scheduler is already active for this subscription flow, it will be reused
|
|
583
|
+
self.flow_scheduler.start_flow_set(subscription_config.flows)
|
|
584
|
+
|
|
585
|
+
# Trigger flows that have a bus_name_added trigger configured
|
|
586
|
+
|
|
587
|
+
# TODO: path arg doesn't make sense here, it did work for mpris however where there is only one path
|
|
588
|
+
# leaving it now for backwards compatibility
|
|
589
|
+
await self._trigger_bus_name_added(subscription_config, bus_name, object_path)
|
|
590
|
+
|
|
591
|
+
processed_new_subscriptions.add(subscription_config.id)
|
|
592
|
+
|
|
593
|
+
# Trigger flows that have a object_added trigger configured
|
|
594
|
+
await self._trigger_object_added(subscription_config, bus_name, object_path, object_interfaces)
|
|
595
|
+
|
|
596
|
+
async def _trigger_flows(self, subscription_config: SubscriptionConfig, type: str, context: dict):
|
|
297
597
|
|
|
298
|
-
# Trigger flows that have a bus_name_removed trigger configured
|
|
299
598
|
for flow in subscription_config.flows:
|
|
300
599
|
for trigger in flow.triggers:
|
|
301
|
-
if trigger.type ==
|
|
302
|
-
|
|
303
|
-
"bus_name": bus_name,
|
|
304
|
-
"path": path
|
|
305
|
-
}
|
|
306
|
-
trigger_message = FlowTriggerMessage(
|
|
307
|
-
flow,
|
|
308
|
-
trigger,
|
|
309
|
-
datetime.now(),
|
|
310
|
-
trigger_context=trigger_context
|
|
311
|
-
)
|
|
600
|
+
if trigger.type == type:
|
|
601
|
+
trigger_message = FlowTriggerMessage(flow, trigger, datetime.now(), context)
|
|
312
602
|
await self.event_broker.flow_trigger_queue.async_q.put(trigger_message)
|
|
313
603
|
|
|
314
|
-
async def
|
|
604
|
+
async def _trigger_bus_name_added(self, subscription_config: SubscriptionConfig, bus_name: str, path: str):
|
|
605
|
+
|
|
606
|
+
# Trigger flows that have a bus_name_added trigger configured
|
|
607
|
+
await self._trigger_flows(subscription_config, "bus_name_added", {
|
|
608
|
+
"bus_name": bus_name,
|
|
609
|
+
"path": path
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
async def _trigger_bus_name_removed(self, subscription_config: SubscriptionConfig, bus_name: str, path: str):
|
|
613
|
+
|
|
614
|
+
# Trigger flows that have a bus_name_removed trigger configured
|
|
615
|
+
await self._trigger_flows(subscription_config, "bus_name_removed", {
|
|
616
|
+
"bus_name": bus_name,
|
|
617
|
+
"path": path
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
async def _trigger_object_added(self, subscription_config: SubscriptionConfig, bus_name: str, object_path: str, object_interfaces: list[str]):
|
|
315
621
|
|
|
316
|
-
|
|
622
|
+
# Trigger flows that have a object_added trigger configured
|
|
623
|
+
await self._trigger_flows(subscription_config, "object_added", {
|
|
624
|
+
"bus_name": bus_name,
|
|
625
|
+
"path": object_path
|
|
626
|
+
# "interfaces": object_interfaces
|
|
627
|
+
})
|
|
317
628
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
629
|
+
async def _trigger_object_removed(self, subscription_config: SubscriptionConfig, bus_name: str, path: str):
|
|
630
|
+
|
|
631
|
+
# Trigger flows that have a object_removed trigger configured
|
|
632
|
+
await self._trigger_flows(subscription_config, "object_removed", {
|
|
633
|
+
"bus_name": bus_name,
|
|
634
|
+
"path": path
|
|
635
|
+
})
|
|
324
636
|
|
|
325
637
|
async def call_dbus_interface_method(self, interface: dbus_aio.proxy_object.ProxyInterface, method: str, method_args: list[Any]):
|
|
326
638
|
|
|
327
639
|
call_method_name = "call_" + camel_to_snake(method)
|
|
328
|
-
|
|
640
|
+
try:
|
|
641
|
+
res = await interface.__getattribute__(call_method_name)(*method_args)
|
|
642
|
+
except dbus_errors.DBusError as e:
|
|
643
|
+
logger.warning(f"Error while calling dbus object, bus_name={interface.bus_name}, interface={interface.introspection.name}, method={method}")
|
|
644
|
+
raise e
|
|
329
645
|
|
|
330
646
|
if res:
|
|
331
647
|
res = unwrap_dbus_object(res)
|
|
@@ -367,9 +683,16 @@ class DbusClient:
|
|
|
367
683
|
async def dbus_signal_queue_processor_task(self):
|
|
368
684
|
"""Continuously processes messages from the async queue."""
|
|
369
685
|
while True:
|
|
370
|
-
signal = await self.
|
|
686
|
+
signal = await self._dbus_signal_queue.async_q.get()
|
|
371
687
|
await self._handle_on_dbus_signal(signal)
|
|
372
|
-
self.
|
|
688
|
+
self._dbus_signal_queue.async_q.task_done()
|
|
689
|
+
|
|
690
|
+
async def dbus_object_lifecycle_signal_processor_task(self):
|
|
691
|
+
"""Continuously processes messages from the async queue."""
|
|
692
|
+
while True:
|
|
693
|
+
message = await self._dbus_object_lifecycle_signal_queue.async_q.get()
|
|
694
|
+
await self._handle_dbus_object_lifecycle_signal(message)
|
|
695
|
+
self._dbus_object_lifecycle_signal_queue.async_q.task_done()
|
|
373
696
|
|
|
374
697
|
async def _handle_on_dbus_signal(self, signal: DbusSignalWithState):
|
|
375
698
|
|
|
@@ -390,6 +713,7 @@ class DbusClient:
|
|
|
390
713
|
"bus_name": signal.bus_name,
|
|
391
714
|
"path": signal.path,
|
|
392
715
|
"interface": signal.interface_name,
|
|
716
|
+
"signal": signal.signal_config.signal,
|
|
393
717
|
"args": signal.args
|
|
394
718
|
}
|
|
395
719
|
trigger_message = FlowTriggerMessage(
|
|
@@ -403,6 +727,25 @@ class DbusClient:
|
|
|
403
727
|
except Exception as e:
|
|
404
728
|
logger.warning(f"dbus_signal_queue_processor_task: Exception {e}", exc_info=True)
|
|
405
729
|
|
|
730
|
+
async def _handle_dbus_object_lifecycle_signal(self, message: dbus_message.Message):
|
|
731
|
+
|
|
732
|
+
if message.member == 'NameOwnerChanged':
|
|
733
|
+
name, old_owner, new_owner = message.body
|
|
734
|
+
if new_owner != '' and old_owner == '':
|
|
735
|
+
await self._handle_bus_name_added(name)
|
|
736
|
+
if old_owner != '' and new_owner == '':
|
|
737
|
+
await self._handle_bus_name_removed(name)
|
|
738
|
+
|
|
739
|
+
if message.interface == 'org.freedesktop.DBus.ObjectManager':
|
|
740
|
+
bus_name = self.get_well_known_bus_name(message.sender)
|
|
741
|
+
if message.member == 'InterfacesAdded':
|
|
742
|
+
path = message.body[0]
|
|
743
|
+
await self._handle_interfaces_added(bus_name, path)
|
|
744
|
+
elif message.member == 'InterfacesRemoved':
|
|
745
|
+
path = message.body[0]
|
|
746
|
+
await self._handle_interfaces_removed(bus_name, path)
|
|
747
|
+
|
|
748
|
+
|
|
406
749
|
async def _on_mqtt_msg(self, msg: MqttMessage):
|
|
407
750
|
# self.queue.put({
|
|
408
751
|
# "topic": topic,
|
dbus2mqtt/dbus/dbus_types.py
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
import dbus_fast.aio as dbus_aio
|
|
6
7
|
|
|
7
|
-
from dbus2mqtt.config import
|
|
8
|
+
from dbus2mqtt.config import (
|
|
9
|
+
SignalConfig,
|
|
10
|
+
SubscriptionConfig,
|
|
11
|
+
)
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
class BusNameSubscriptions:
|
|
11
15
|
|
|
12
|
-
def __init__(self, bus_name: str):
|
|
16
|
+
def __init__(self, bus_name: str, unique_name: str):
|
|
13
17
|
self.bus_name = bus_name
|
|
18
|
+
self.unique_name = unique_name
|
|
14
19
|
self.path_objects: dict[str, dbus_aio.proxy_object.ProxyObject] = {}
|
|
15
20
|
|
|
16
21
|
@dataclass
|
|
17
22
|
class SubscribedInterface:
|
|
18
23
|
|
|
19
|
-
interface_config: InterfaceConfig
|
|
24
|
+
# interface_config: InterfaceConfig
|
|
20
25
|
subscription_config: SubscriptionConfig
|
|
21
26
|
bus_name: str
|
|
22
27
|
path: str
|
|
23
28
|
interface_name: str
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DbusSignalWithState:
|
|
32
|
+
bus_name: str
|
|
33
|
+
path: str
|
|
34
|
+
interface_name: str
|
|
35
|
+
subscription_config: SubscriptionConfig
|
|
36
|
+
signal_config: SignalConfig
|
|
37
|
+
args: list[Any]
|
dbus2mqtt/dbus/dbus_util.py
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import base64
|
|
2
2
|
import re
|
|
3
3
|
|
|
4
4
|
import dbus_fast.signature as dbus_signature
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def
|
|
8
|
-
if isinstance(obj,
|
|
9
|
-
return obj.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
def unwrap_dbus_object(obj):
|
|
8
|
+
if isinstance(obj, dict):
|
|
9
|
+
return {k: unwrap_dbus_object(v) for k, v in obj.items()}
|
|
10
|
+
elif isinstance(obj, list | tuple | set):
|
|
11
|
+
return type(obj)(unwrap_dbus_object(i) for i in obj)
|
|
12
|
+
elif isinstance(obj, dbus_signature.Variant):
|
|
13
|
+
return unwrap_dbus_object(obj.value)
|
|
14
|
+
elif isinstance(obj, bytes):
|
|
15
|
+
return base64.b64encode(obj).decode('utf-8')
|
|
16
|
+
else:
|
|
17
|
+
return obj
|
|
17
18
|
|
|
18
19
|
def unwrap_dbus_objects(args):
|
|
19
20
|
res = [unwrap_dbus_object(o) for o in args]
|
dbus2mqtt/event_broker.py
CHANGED
|
@@ -7,12 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
import janus
|
|
9
9
|
|
|
10
|
-
from dbus2mqtt.config import
|
|
11
|
-
FlowConfig,
|
|
12
|
-
FlowTriggerConfig,
|
|
13
|
-
SignalConfig,
|
|
14
|
-
SubscriptionConfig,
|
|
15
|
-
)
|
|
10
|
+
from dbus2mqtt.config import FlowConfig, FlowTriggerConfig
|
|
16
11
|
|
|
17
12
|
logger = logging.getLogger(__name__)
|
|
18
13
|
|
|
@@ -23,15 +18,6 @@ class MqttMessage:
|
|
|
23
18
|
payload: Any
|
|
24
19
|
payload_serialization_type: str = "json"
|
|
25
20
|
|
|
26
|
-
@dataclass
|
|
27
|
-
class DbusSignalWithState:
|
|
28
|
-
bus_name: str
|
|
29
|
-
path: str
|
|
30
|
-
interface_name: str
|
|
31
|
-
subscription_config: SubscriptionConfig
|
|
32
|
-
signal_config: SignalConfig
|
|
33
|
-
args: list[Any]
|
|
34
|
-
|
|
35
21
|
@dataclass
|
|
36
22
|
class FlowTriggerMessage:
|
|
37
23
|
flow_config: FlowConfig
|
|
@@ -43,7 +29,6 @@ class EventBroker:
|
|
|
43
29
|
def __init__(self):
|
|
44
30
|
self.mqtt_receive_queue = janus.Queue[MqttMessage]()
|
|
45
31
|
self.mqtt_publish_queue = janus.Queue[MqttMessage]()
|
|
46
|
-
self.dbus_signal_queue = janus.Queue[DbusSignalWithState]()
|
|
47
32
|
self.flow_trigger_queue = janus.Queue[FlowTriggerMessage]()
|
|
48
33
|
# self.dbus_send_queue: janus.Queue
|
|
49
34
|
|
|
@@ -51,7 +36,6 @@ class EventBroker:
|
|
|
51
36
|
await asyncio.gather(
|
|
52
37
|
self.mqtt_receive_queue.aclose(),
|
|
53
38
|
self.mqtt_publish_queue.aclose(),
|
|
54
|
-
self.dbus_signal_queue.aclose(),
|
|
55
39
|
self.flow_trigger_queue.aclose(),
|
|
56
40
|
return_exceptions=True
|
|
57
41
|
)
|
|
@@ -63,7 +47,3 @@ class EventBroker:
|
|
|
63
47
|
async def publish_to_mqtt(self, msg: MqttMessage):
|
|
64
48
|
# logger.debug("publish_to_mqtt")
|
|
65
49
|
await self.mqtt_publish_queue.async_q.put(msg)
|
|
66
|
-
|
|
67
|
-
def on_dbus_signal(self, signal: DbusSignalWithState):
|
|
68
|
-
# logger.debug("on_dbus_signal")
|
|
69
|
-
self.dbus_signal_queue.sync_q.put(signal)
|
dbus2mqtt/flow/flow_processor.py
CHANGED
|
@@ -7,7 +7,13 @@ 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
|
|
10
|
+
from dbus2mqtt.config import (
|
|
11
|
+
FlowConfig,
|
|
12
|
+
FlowTriggerConfig,
|
|
13
|
+
FlowTriggerDbusSignalConfig,
|
|
14
|
+
FlowTriggerObjectAddedConfig,
|
|
15
|
+
FlowTriggerObjectRemovedConfig,
|
|
16
|
+
)
|
|
11
17
|
from dbus2mqtt.event_broker import FlowTriggerMessage
|
|
12
18
|
from dbus2mqtt.flow import FlowAction, FlowExecutionContext
|
|
13
19
|
from dbus2mqtt.flow.actions.context_set import ContextSetAction
|
|
@@ -103,7 +109,7 @@ class FlowActionContext:
|
|
|
103
109
|
|
|
104
110
|
return res
|
|
105
111
|
|
|
106
|
-
async def execute_actions(self, trigger_context: dict[str, Any] | None):
|
|
112
|
+
async def execute_actions(self, trigger_type: str, trigger_context: dict[str, Any] | None):
|
|
107
113
|
|
|
108
114
|
# per flow execution context
|
|
109
115
|
context = FlowExecutionContext(
|
|
@@ -111,6 +117,8 @@ class FlowActionContext:
|
|
|
111
117
|
global_flows_context=self.global_flows_context,
|
|
112
118
|
flow_context=self.flow_context)
|
|
113
119
|
|
|
120
|
+
context.context["trigger_type"] = trigger_type
|
|
121
|
+
|
|
114
122
|
if trigger_context:
|
|
115
123
|
context.context.update(trigger_context)
|
|
116
124
|
|
|
@@ -176,14 +184,20 @@ class FlowProcessor:
|
|
|
176
184
|
finally:
|
|
177
185
|
self.event_broker.flow_trigger_queue.async_q.task_done()
|
|
178
186
|
|
|
179
|
-
def _trigger_config_to_str(self,
|
|
187
|
+
def _trigger_config_to_str(self, msg: FlowTriggerMessage) -> str:
|
|
188
|
+
config = msg.flow_trigger_config
|
|
180
189
|
if isinstance(config, FlowTriggerDbusSignalConfig):
|
|
181
190
|
return f"{config.type}({config.signal})"
|
|
191
|
+
elif isinstance(config, FlowTriggerObjectAddedConfig) or isinstance(config, FlowTriggerObjectRemovedConfig):
|
|
192
|
+
path = msg.trigger_context.get('path') if msg.trigger_context else None
|
|
193
|
+
if path:
|
|
194
|
+
return f"{config.type}({path})"
|
|
182
195
|
return config.type
|
|
183
196
|
|
|
184
197
|
async def _process_flow_trigger(self, flow_trigger_message: FlowTriggerMessage):
|
|
185
198
|
|
|
186
|
-
|
|
199
|
+
trigger_type = flow_trigger_message.flow_trigger_config.type
|
|
200
|
+
trigger_str = self._trigger_config_to_str(flow_trigger_message)
|
|
187
201
|
flow_str = flow_trigger_message.flow_config.name or flow_trigger_message.flow_config.id
|
|
188
202
|
|
|
189
203
|
log_message = f"on_trigger: {trigger_str}, flow={flow_str}, time={flow_trigger_message.timestamp.isoformat()}"
|
|
@@ -196,4 +210,4 @@ class FlowProcessor:
|
|
|
196
210
|
flow_id = flow_trigger_message.flow_config.id
|
|
197
211
|
|
|
198
212
|
flow = self._flows[flow_id]
|
|
199
|
-
await flow.execute_actions(trigger_context=flow_trigger_message.trigger_context)
|
|
213
|
+
await flow.execute_actions(trigger_type, trigger_context=flow_trigger_message.trigger_context)
|
dbus2mqtt/main.py
CHANGED
|
@@ -8,6 +8,8 @@ import colorlog
|
|
|
8
8
|
import dbus_fast.aio as dbus_aio
|
|
9
9
|
import dotenv
|
|
10
10
|
|
|
11
|
+
from dbus_fast import BusType
|
|
12
|
+
|
|
11
13
|
from dbus2mqtt import AppContext
|
|
12
14
|
from dbus2mqtt.config import Config
|
|
13
15
|
from dbus2mqtt.config.jsonarparse import new_argument_parser
|
|
@@ -23,7 +25,8 @@ logger = logging.getLogger(__name__)
|
|
|
23
25
|
|
|
24
26
|
async def dbus_processor_task(app_context: AppContext, flow_scheduler: FlowScheduler):
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
bus_type = BusType.SYSTEM if app_context.config.dbus.bus_type == "SYSTEM" else BusType.SESSION
|
|
29
|
+
bus = dbus_aio.message_bus.MessageBus(bus_type=bus_type)
|
|
27
30
|
|
|
28
31
|
dbus_client = DbusClient(app_context, bus, flow_scheduler)
|
|
29
32
|
app_context.templating.add_functions(jinja_custom_dbus_functions(dbus_client))
|
|
@@ -36,7 +39,8 @@ async def dbus_processor_task(app_context: AppContext, flow_scheduler: FlowSched
|
|
|
36
39
|
await asyncio.gather(
|
|
37
40
|
dbus_client_run_future,
|
|
38
41
|
asyncio.create_task(dbus_client.dbus_signal_queue_processor_task()),
|
|
39
|
-
asyncio.create_task(dbus_client.mqtt_receive_queue_processor_task())
|
|
42
|
+
asyncio.create_task(dbus_client.mqtt_receive_queue_processor_task()),
|
|
43
|
+
asyncio.create_task(dbus_client.dbus_object_lifecycle_signal_processor_task())
|
|
40
44
|
)
|
|
41
45
|
|
|
42
46
|
async def mqtt_processor_task(app_context: AppContext):
|
|
@@ -103,9 +107,19 @@ def main():
|
|
|
103
107
|
|
|
104
108
|
config: Config = cast(Config, parser.instantiate_classes(cfg))
|
|
105
109
|
|
|
110
|
+
class NamePartsFilter(logging.Filter):
|
|
111
|
+
def filter(self, record):
|
|
112
|
+
record.name_last = record.name.rsplit('.', 1)[-1]
|
|
113
|
+
# record.name_first = record.name.split('.', 1)[0]
|
|
114
|
+
# record.name_short = record.name
|
|
115
|
+
# if record.name.startswith("dbus2mqtt"):
|
|
116
|
+
# record.name_short = record.name.split('.', 1)[-1]
|
|
117
|
+
return True
|
|
118
|
+
|
|
106
119
|
handler = colorlog.StreamHandler(stream=sys.stdout)
|
|
120
|
+
handler.addFilter(NamePartsFilter())
|
|
107
121
|
handler.setFormatter(colorlog.ColoredFormatter(
|
|
108
|
-
'%(log_color)s%(levelname)s:%(
|
|
122
|
+
'%(log_color)s%(levelname)s:%(name_last)s:%(message)s',
|
|
109
123
|
log_colors={
|
|
110
124
|
"DEBUG": "light_black",
|
|
111
125
|
"WARNING": "yellow",
|
dbus2mqtt/mqtt/mqtt_client.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
+
import random
|
|
6
|
+
import string
|
|
5
7
|
|
|
6
8
|
from typing import Any
|
|
7
9
|
from urllib.parse import ParseResult
|
|
@@ -24,7 +26,9 @@ class MqttClient:
|
|
|
24
26
|
self.config = app_context.config.mqtt
|
|
25
27
|
self.event_broker = app_context.event_broker
|
|
26
28
|
|
|
29
|
+
unique_client_id_postfix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
27
30
|
self.client = mqtt.Client(
|
|
31
|
+
client_id=f"dbus2mqtt-client-{unique_client_id_postfix}",
|
|
28
32
|
protocol=mqtt.MQTTv5,
|
|
29
33
|
callback_api_version=CallbackAPIVersion.VERSION2
|
|
30
34
|
)
|
|
@@ -42,10 +46,10 @@ class MqttClient:
|
|
|
42
46
|
|
|
43
47
|
def connect(self):
|
|
44
48
|
|
|
45
|
-
# mqtt_client.on_message = lambda client, userdata, message: asyncio.create_task(mqtt_on_message(client, userdata, message))
|
|
46
49
|
self.client.connect_async(
|
|
47
50
|
host=self.config.host,
|
|
48
|
-
port=self.config.port
|
|
51
|
+
port=self.config.port,
|
|
52
|
+
clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY
|
|
49
53
|
)
|
|
50
54
|
|
|
51
55
|
async def mqtt_publish_queue_processor_task(self):
|
|
@@ -98,12 +102,15 @@ class MqttClient:
|
|
|
98
102
|
logger.info(f"on_connect: Connected to {self.config.host}:{self.config.port}")
|
|
99
103
|
# Subscribing in on_connect() means that if we lose the connection and
|
|
100
104
|
# reconnect then subscriptions will be renewed.
|
|
105
|
+
# TODO: Determine topics based on config
|
|
101
106
|
client.subscribe("dbus2mqtt/#", options=SubscribeOptions(noLocal=True))
|
|
102
107
|
|
|
103
108
|
self.loop.call_soon_threadsafe(self.connected_event.set)
|
|
104
109
|
|
|
105
110
|
def on_message(self, client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage):
|
|
106
111
|
|
|
112
|
+
# TODO: Skip messages being sent by other dbus2mqtt clients
|
|
113
|
+
|
|
107
114
|
payload = msg.payload.decode()
|
|
108
115
|
if msg.retain:
|
|
109
116
|
logger.info(f"on_message: skipping msg with retain=True, topic={msg.topic}, payload={payload}")
|
|
@@ -32,7 +32,7 @@ class DbusContext:
|
|
|
32
32
|
# Pylance will mentiod this line is unreachable. It is not as jinja2 can pass in any type
|
|
33
33
|
raise ValueError("method_args must be a list")
|
|
34
34
|
|
|
35
|
-
proxy_object = self.dbus_client.
|
|
35
|
+
proxy_object = self.dbus_client.get_subscribed_proxy_object(bus_name, path)
|
|
36
36
|
if not proxy_object:
|
|
37
37
|
raise ValueError(f"No matching subscription found for bus_name: {bus_name}, path: {path}")
|
|
38
38
|
|
|
@@ -42,7 +42,7 @@ class DbusContext:
|
|
|
42
42
|
|
|
43
43
|
async def async_dbus_property_get_fn(self, bus_name: str, path: str, interface: str, property:str, default_unsupported: Any = None):
|
|
44
44
|
|
|
45
|
-
proxy_object = self.dbus_client.
|
|
45
|
+
proxy_object = self.dbus_client.get_subscribed_proxy_object(bus_name, path)
|
|
46
46
|
if not proxy_object:
|
|
47
47
|
raise ValueError(f"No matching subscription found for bus_name: {bus_name}, path: {path}")
|
|
48
48
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbus2mqtt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -53,7 +53,7 @@ Initial testing has focused on MPRIS integration. A table of tested MPRIS player
|
|
|
53
53
|
|
|
54
54
|
## Getting started with dbus2mqtt
|
|
55
55
|
|
|
56
|
-
Create a `config.yaml` file with the contents shown below. This configuration will expose all bus properties from the `org.mpris.MediaPlayer2.Player` interface to MQTT on the `dbus2mqtt/org.mpris.MediaPlayer2/state` topic. Have a look at [docs/examples](docs/examples.md) for more examples
|
|
56
|
+
Create a `config.yaml` file with the contents shown below. This configuration will expose all bus properties from the `org.mpris.MediaPlayer2.Player` interface to MQTT on the `dbus2mqtt/org.mpris.MediaPlayer2/state` topic. Have a look at [docs/examples](https://github.com/jwnmulder/dbus2mqtt/blob/main/docs/examples.md) for more examples
|
|
57
57
|
|
|
58
58
|
```yaml
|
|
59
59
|
dbus:
|
|
@@ -68,7 +68,7 @@ dbus:
|
|
|
68
68
|
flows:
|
|
69
69
|
- name: "Publish MPRIS state"
|
|
70
70
|
triggers:
|
|
71
|
-
- type:
|
|
71
|
+
- type: object_added
|
|
72
72
|
- type: schedule
|
|
73
73
|
interval: {seconds: 5}
|
|
74
74
|
actions:
|
|
@@ -111,8 +111,8 @@ 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
|
-
docker pull jwnmulder/dbus2mqtt
|
|
115
|
-
docker run --detach --name dbus2mqtt \
|
|
114
|
+
sudo docker pull jwnmulder/dbus2mqtt
|
|
115
|
+
sudo docker run --detach --name dbus2mqtt \
|
|
116
116
|
--volume "$HOME"/.config/dbus2mqtt:"$HOME"/.config/dbus2mqtt \
|
|
117
117
|
--volume /run/user:/run/user \
|
|
118
118
|
--env DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \
|
|
@@ -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=Hp5yurhP8FkAO-y0l2grygHxR2e37ZQ7QScjcQDA2UU,1334
|
|
4
|
+
dbus2mqtt/main.py,sha256=Kr2LRVxWcPDtNwCj8Eqz9TxtGLAVrV4q0nizKh1pLXc,4539
|
|
5
|
+
dbus2mqtt/config/__init__.py,sha256=LXQg2cZ_vMvhAl7_cC6G3gxCaLCq9uaTfZqw7OEXkPQ,5104
|
|
6
|
+
dbus2mqtt/config/jsonarparse.py,sha256=VJGFeyQJcOE6bRXlSRr3FClvN_HnmiIlEGmOgNM0oCc,1214
|
|
7
|
+
dbus2mqtt/dbus/dbus_client.py,sha256=0XneV3wulj6d_l4Jq9KIwROC19DLF853u6weCdyxpwA,39222
|
|
8
|
+
dbus2mqtt/dbus/dbus_types.py,sha256=NmPD9um499e49Pk8DWH4IrIPQh1BinHYQgoXllCNiDw,777
|
|
9
|
+
dbus2mqtt/dbus/dbus_util.py,sha256=h-1Y8Mvz9bj9X7mPZ8LghkvXDrujdJHK0__AOW373hE,697
|
|
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=-fJ5JPFcFqz1RokKaqjdHoU-EPg5McAERKYFwIkMzgU,8749
|
|
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=0d8XSe4-WxrUBxcUprukwxgzIhnCn1G0fCPnLqPp9ko,5148
|
|
17
|
+
dbus2mqtt/template/dbus_template_functions.py,sha256=UEoXK2PqDKF6jR4vTFHQwq58f5APnOJr7B1_I1zW8yM,2449
|
|
18
|
+
dbus2mqtt/template/templating.py,sha256=ZLp1A8PFAs_-Bndx4yGqyppaDfh8marHlK7P3bFInu4,4144
|
|
19
|
+
dbus2mqtt-0.4.0.dist-info/METADATA,sha256=enb7xSlQVzVTVxCLshavr5HoV8tNamfd2AOJzgel_Sg,7887
|
|
20
|
+
dbus2mqtt-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
dbus2mqtt-0.4.0.dist-info/entry_points.txt,sha256=pmmacoHCsvTTUB5dIPaY4i0H9gX7BQlpd3rBU-Jbv3A,50
|
|
22
|
+
dbus2mqtt-0.4.0.dist-info/licenses/LICENSE,sha256=a4bIEgyA9rrnAfUN90CgbgZ6BQIFHeABkk0JihiBaxM,1074
|
|
23
|
+
dbus2mqtt-0.4.0.dist-info/RECORD,,
|
dbus2mqtt-0.3.1.dist-info/RECORD
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|