dbus2mqtt 0.1.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/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ from dbus2mqtt.config import Config
2
+ from dbus2mqtt.event_broker import EventBroker
3
+ from dbus2mqtt.template.templating import TemplateEngine
4
+
5
+
6
+ class AppContext:
7
+ def __init__(self, config: Config, event_broker: EventBroker, templating: TemplateEngine):
8
+ self.config = config
9
+ self.event_broker = event_broker
10
+ self.templating = templating
dbus2mqtt/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from dbus2mqtt.main import main
2
+
3
+ if __name__ == '__main__':
4
+ main()
dbus2mqtt/config.py ADDED
@@ -0,0 +1,143 @@
1
+ import fnmatch
2
+ import uuid
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import Annotated, Any, Literal
6
+
7
+ from pydantic import Field, SecretStr
8
+
9
+ from dbus2mqtt.template.templating import TemplateEngine
10
+
11
+
12
+ @dataclass
13
+ class SignalConfig:
14
+ signal: str
15
+ filter: str | None = None
16
+
17
+ def matches_filter(self, template_engine: TemplateEngine, *args) -> bool:
18
+ res = template_engine.render_template(self.filter, str, { "args": args })
19
+ return res == "True"
20
+
21
+ @dataclass
22
+ class MethodConfig:
23
+ method: str
24
+
25
+ @dataclass
26
+ class PropertyConfig:
27
+ property: str
28
+
29
+ @dataclass
30
+ class InterfaceConfig:
31
+ interface: str
32
+ mqtt_call_method_topic: str | None = None
33
+ signals: list[SignalConfig] = field(default_factory=list)
34
+ methods: list[MethodConfig] = field(default_factory=list)
35
+ properties: list[PropertyConfig] = field(default_factory=list)
36
+
37
+ def render_mqtt_call_method_topic(self, template_engine: TemplateEngine, context: dict[str, Any]) -> Any:
38
+ return template_engine.render_template(self.mqtt_call_method_topic, str, context)
39
+
40
+ @dataclass
41
+ class FlowTriggerMqttConfig:
42
+ type: Literal["mqtt"]
43
+ topic: str
44
+ filter: str | None = None
45
+
46
+ @dataclass
47
+ class FlowTriggerScheduleConfig:
48
+ type: Literal["schedule"] = "schedule"
49
+ id: str = field(default_factory=lambda: uuid.uuid4().hex)
50
+ cron: dict[str, Any] | None = None
51
+ interval: dict[str, Any] | None = None
52
+
53
+ @dataclass
54
+ class FlowTriggerDbusSignalConfig:
55
+ interface: str
56
+ signal: str
57
+ type: Literal["dbus_signal"] = "dbus_signal"
58
+ bus_name: str | None = None
59
+ path: str | None = None
60
+ filter: str | None = None
61
+
62
+ @dataclass
63
+ class FlowTriggerBusNameAddedConfig:
64
+ type: Literal["bus_name_added"] = "bus_name_added"
65
+ filter: str | None = None
66
+
67
+ @dataclass
68
+ class FlowTriggerBusNameRemovedConfig:
69
+ type: Literal["bus_name_removed"] = "bus_name_removed"
70
+ filter: str | None = None
71
+
72
+ FlowTriggerConfig = Annotated[
73
+ FlowTriggerMqttConfig | FlowTriggerScheduleConfig | FlowTriggerDbusSignalConfig | FlowTriggerBusNameAddedConfig | FlowTriggerBusNameRemovedConfig,
74
+ Field(discriminator="type")
75
+ ]
76
+
77
+ @dataclass
78
+ class FlowActionContextSetConfig:
79
+ type: Literal["context_set"] = "context_set"
80
+ context: dict[str, Any] | None = None
81
+ global_context: dict[str, Any] | None = None
82
+
83
+ @dataclass
84
+ class FlowActionMqttPublishConfig:
85
+ topic: str
86
+ payload_template: str | dict[str, Any]
87
+ """should be a dict if payload_type is json/yaml
88
+ or a string if payload_type is text"""
89
+ type: Literal["mqtt_publish"] = "mqtt_publish"
90
+ payload_type: Literal["json", "yaml", "text"] = "json"
91
+
92
+ FlowActionConfig = Annotated[
93
+ FlowActionMqttPublishConfig | FlowActionContextSetConfig,
94
+ Field(discriminator="type")
95
+ ]
96
+
97
+ @dataclass
98
+ class FlowConfig:
99
+ triggers: list[FlowTriggerConfig]
100
+ actions: list[FlowActionConfig]
101
+ name: str | None = None
102
+ id: str = field(default_factory=lambda: uuid.uuid4().hex)
103
+
104
+ @dataclass
105
+ class SubscriptionConfig:
106
+ bus_name: str
107
+ """bus_name pattern supporting * wildcards"""
108
+ path: str
109
+ """path pattern supporting * wildcards"""
110
+ interfaces: list[InterfaceConfig] = field(default_factory=list)
111
+ flows: list[FlowConfig] = field(default_factory=list)
112
+ id: str = field(default_factory=lambda: uuid.uuid4().hex)
113
+
114
+ @dataclass
115
+ class DbusConfig:
116
+ subscriptions: list[SubscriptionConfig]
117
+
118
+ def is_bus_name_configured(self, bus_name: str) -> bool:
119
+
120
+ for subscription in self.subscriptions:
121
+ if fnmatch.fnmatchcase(bus_name, subscription.bus_name):
122
+ return True
123
+ return False
124
+
125
+ def get_subscription_configs(self, bus_name: str, path: str) -> list[SubscriptionConfig]:
126
+ res: list[SubscriptionConfig] = []
127
+ for subscription in self.subscriptions:
128
+ if fnmatch.fnmatchcase(bus_name, subscription.bus_name) and fnmatch.fnmatchcase(path, subscription.path):
129
+ res.append(subscription)
130
+ return res
131
+
132
+ @dataclass
133
+ class MqttConfig:
134
+ host: str
135
+ username: str
136
+ password: SecretStr
137
+ port: int = 1883
138
+
139
+ @dataclass
140
+ class Config:
141
+ mqtt: MqttConfig
142
+ dbus: DbusConfig
143
+ flows: list[FlowConfig] = field(default_factory=list)
@@ -0,0 +1,450 @@
1
+ import json
2
+ import logging
3
+ import re
4
+
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ import dbus_next
9
+ import dbus_next.aio as dbus_aio
10
+ import dbus_next.introspection as dbus_introspection
11
+
12
+ from dbus2mqtt import AppContext
13
+ from dbus2mqtt.config import InterfaceConfig, SubscriptionConfig
14
+ from dbus2mqtt.dbus.dbus_types import BusNameSubscriptions, SubscribedInterface
15
+ from dbus2mqtt.dbus.dbus_util import (
16
+ camel_to_snake,
17
+ unwrap_dbus_object,
18
+ unwrap_dbus_objects,
19
+ )
20
+ from dbus2mqtt.event_broker import DbusSignalWithState, MqttMessage
21
+ from dbus2mqtt.flow.flow_processor import FlowScheduler, FlowTriggerMessage
22
+
23
+ logger = logging.getLogger(__name__)
24
+
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
+
32
+ class DbusClient:
33
+
34
+ def __init__(self, app_context: AppContext, bus: dbus_aio.message_bus.MessageBus, flow_scheduler: FlowScheduler):
35
+ self.app_context = app_context
36
+ self.config = app_context.config.dbus
37
+ self.event_broker = app_context.event_broker
38
+ self.templating = app_context.templating
39
+ self.bus = bus
40
+ self.flow_scheduler = flow_scheduler
41
+ self.subscriptions: dict[str, BusNameSubscriptions] = {}
42
+
43
+ async def connect(self):
44
+
45
+ if not self.bus.connected:
46
+ await self.bus.connect()
47
+
48
+ if self.bus.connected:
49
+ logger.info(f"Connected to {self.bus._bus_address}")
50
+ else:
51
+ logger.warning(f"Failed to connect to {self.bus._bus_address}")
52
+
53
+ introspection = await self.bus.introspect('org.freedesktop.DBus', '/org/freedesktop/DBus')
54
+ obj = self.bus.get_proxy_object('org.freedesktop.DBus', '/org/freedesktop/DBus', introspection)
55
+ dbus_interface = obj.get_interface('org.freedesktop.DBus')
56
+
57
+ # subscribe to NameOwnerChanged which allows us to detect new bus_names
58
+ dbus_interface.__getattribute__("on_name_owner_changed")(self._dbus_name_owner_changed_callback)
59
+
60
+ # subscribe to existing registered bus_names we are interested in
61
+ connected_bus_names = await dbus_interface.__getattribute__("call_list_names")()
62
+
63
+ new_subscriped_interfaces: list[SubscribedInterface] = []
64
+ for bus_name in connected_bus_names:
65
+ new_subscriped_interfaces.extend(await self._handle_bus_name_added(bus_name))
66
+
67
+ logger.info(f"subscriptions on startup: {list(set([si.bus_name for si in new_subscriped_interfaces]))}")
68
+
69
+ def get_proxy_object(self, bus_name: str, path: str) -> dbus_aio.proxy_object.ProxyObject | None:
70
+
71
+ bus_name_subscriptions = self.subscriptions.get(bus_name)
72
+ if bus_name_subscriptions:
73
+ proxy_object = bus_name_subscriptions.path_objects.get(path)
74
+ if proxy_object:
75
+ return proxy_object
76
+
77
+ return None
78
+
79
+ def _ensure_proxy_object_subscription(self, bus_name: str, path: str, introspection: dbus_introspection.Node):
80
+
81
+ bus_name_subscriptions = self.subscriptions.get(bus_name)
82
+ if not bus_name_subscriptions:
83
+ bus_name_subscriptions = BusNameSubscriptions(bus_name)
84
+ self.subscriptions[bus_name] = bus_name_subscriptions
85
+
86
+ proxy_object = bus_name_subscriptions.path_objects.get(path)
87
+ if not proxy_object:
88
+ proxy_object = self.bus.get_proxy_object(bus_name, path, introspection)
89
+ bus_name_subscriptions.path_objects[path] = proxy_object
90
+
91
+ return proxy_object, bus_name_subscriptions
92
+
93
+ def _dbus_next_signal_publisher(self, dbus_signal_state: dict[str, Any], *args):
94
+ unwrapped_args = unwrap_dbus_objects(args)
95
+ self.event_broker.on_dbus_signal(
96
+ DbusSignalWithState(**dbus_signal_state, args=unwrapped_args)
97
+ )
98
+
99
+ def _dbus_next_signal_handler(self, signal: dbus_introspection.Signal, state: dict[str, Any]) -> Any:
100
+ expected_args = len(signal.args)
101
+
102
+ if expected_args == 1:
103
+ return lambda a: self._dbus_next_signal_publisher(state, a)
104
+ elif expected_args == 2:
105
+ return lambda a, b: self._dbus_next_signal_publisher(state, a, b)
106
+ elif expected_args == 3:
107
+ return lambda a, b, c: self._dbus_next_signal_publisher(state, a, b, c)
108
+ elif expected_args == 4:
109
+ return lambda a, b, c, d: self._dbus_next_signal_publisher(state, a, b, c, d)
110
+ raise ValueError("Unsupported nr of arguments")
111
+
112
+ async def _subscribe_interface(self, bus_name: str, path: str, introspection: dbus_introspection.Node, interface: dbus_introspection.Interface, subscription_config: SubscriptionConfig, si: InterfaceConfig) -> SubscribedInterface:
113
+
114
+ proxy_object, bus_name_subscriptions = self._ensure_proxy_object_subscription(bus_name, path, introspection)
115
+ obj_interface = proxy_object.get_interface(interface.name)
116
+
117
+ interface_signals = dict((s.name, s) for s in interface.signals)
118
+
119
+ logger.debug(f"subscribe: bus_name={bus_name}, path={path}, interface={interface.name}, proxy_interface: signals={list(interface_signals.keys())}")
120
+
121
+ for signal_config in si.signals:
122
+ interface_signal = interface_signals.get(signal_config.signal)
123
+ if interface_signal:
124
+
125
+ on_signal_method_name = "on_" + camel_to_snake(signal_config.signal)
126
+ dbus_signal_state = {
127
+ "bus_name_subscriptions": bus_name_subscriptions,
128
+ "path": path,
129
+ "interface_name": interface.name,
130
+ "subscription_config": subscription_config,
131
+ "signal_config": signal_config,
132
+ }
133
+
134
+ handler = self._dbus_next_signal_handler(interface_signal, dbus_signal_state)
135
+ obj_interface.__getattribute__(on_signal_method_name)(handler)
136
+ logger.info(f"subscribed with signal_handler: signal={signal_config.signal}, bus_name={bus_name}, path={path}, interface={interface.name}")
137
+
138
+ else:
139
+ logger.warning(f"Invalid signal: signal={signal_config.signal}, bus_name={bus_name}, path={path}, interface={interface.name}")
140
+
141
+ return SubscribedInterface(
142
+ bus_name=bus_name,
143
+ path=path,
144
+ interface_name=interface.name,
145
+ subscription_config=subscription_config,
146
+ interface_config=si
147
+ )
148
+
149
+ async def _process_interface(self, bus_name: str, path: str, introspection: dbus_introspection.Node, interface: dbus_introspection.Interface) -> list[SubscribedInterface]:
150
+
151
+ logger.debug(f"process_interface: {bus_name}, {path}, {interface.name}")
152
+ subscription_configs = self.config.get_subscription_configs(bus_name, path)
153
+ new_subscriptions: list[SubscribedInterface] = []
154
+ for subscription in subscription_configs:
155
+ logger.debug(f"processing subscription config: {subscription.bus_name}, {subscription.path}")
156
+ for subscription_interface in subscription.interfaces:
157
+ if subscription_interface.interface == interface.name:
158
+ logger.debug(f"matching config found for bus_name={bus_name}, path={path}, interface={interface.name}")
159
+ subscribed_iterface = await self._subscribe_interface(bus_name, path, introspection, interface, subscription, subscription_interface)
160
+
161
+ new_subscriptions.append(subscribed_iterface)
162
+
163
+ return new_subscriptions
164
+
165
+ async def _visit_bus_name_path(self, bus_name: str, path: str) -> list[SubscribedInterface]:
166
+
167
+ new_subscriptions: list[SubscribedInterface] = []
168
+
169
+ try:
170
+ introspection = await self.bus.introspect(bus_name, path)
171
+ except TypeError as e:
172
+ logger.warning(f"bus.introspect failed, bus_name={bus_name}, path={path}: {e}")
173
+ return new_subscriptions
174
+
175
+ if len(introspection.nodes) == 0:
176
+ logger.debug(f"leaf node: bus_name={bus_name}, path={path}, is_root={introspection.is_root}, interfaces={[i.name for i in introspection.interfaces]}")
177
+
178
+ for interface in introspection.interfaces:
179
+ new_subscriptions.extend(
180
+ await self._process_interface(bus_name, path, introspection, interface)
181
+ )
182
+
183
+ for node in introspection.nodes:
184
+ path_seperator = "" if path.endswith('/') else "/"
185
+ new_subscriptions.extend(
186
+ await self._visit_bus_name_path(bus_name, f"{path}{path_seperator}{node.name}")
187
+ )
188
+
189
+ return new_subscriptions
190
+
191
+ async def _handle_bus_name_added(self, bus_name: str) -> list[SubscribedInterface]:
192
+
193
+ if not self.config.is_bus_name_configured(bus_name):
194
+ return []
195
+
196
+ # sanity checks
197
+ for umh in self.bus._user_message_handlers:
198
+ umh_bus_name = umh.__self__.bus_name
199
+ if umh_bus_name == bus_name:
200
+ logger.warning(f"handle_bus_name_added: {umh_bus_name} already added")
201
+
202
+ new_subscriped_interfaces = await self._visit_bus_name_path(bus_name, "/")
203
+
204
+ logger.info(f"new_subscriptions: {list(set([si.bus_name for si in new_subscriped_interfaces]))}")
205
+
206
+ # setup and process triggers for each flow in each subscription
207
+ processed_new_subscriptions: set[str] = set()
208
+
209
+ # With all subscriptions in place, we can now ensure schedulers are created
210
+ # create a FlowProcessor per bus_name/path subscription?
211
+ # One global or a per subscription FlowProcessor.flow_processor_task?
212
+ # Start a new timer job, but leverage existing FlowScheduler
213
+ # How does the FlowScheduler now it should invoke the local FlowPocessor?
214
+ # Maybe use queues to communicate from here with the FlowProcessor?
215
+ # e.g.: StartFlows, StopFlows,
216
+
217
+ for subscribed_interface in new_subscriped_interfaces:
218
+
219
+ subscription_config = subscribed_interface.subscription_config
220
+ if subscription_config.id not in processed_new_subscriptions:
221
+
222
+ # Ensure all schedulers are started
223
+ self.flow_scheduler.start_flow_set(subscription_config.flows)
224
+
225
+ # Trigger flows that have a bus_name_added trigger configured
226
+ await self._trigger_bus_name_added(subscription_config, subscribed_interface.bus_name)
227
+
228
+ processed_new_subscriptions.add(subscription_config.id)
229
+
230
+ return new_subscriped_interfaces
231
+
232
+ async def _trigger_bus_name_added(self, subscription_config: SubscriptionConfig, bus_name: str):
233
+
234
+ for flow in subscription_config.flows:
235
+ for trigger in flow.triggers:
236
+ if trigger.type == "bus_name_added":
237
+ context = {
238
+ "bus_name": bus_name,
239
+ }
240
+ trigger_message = FlowTriggerMessage(flow, trigger, datetime.now(), context)
241
+ await self.event_broker.flow_trigger_queue.async_q.put(trigger_message)
242
+
243
+ async def _handle_bus_name_removed(self, bus_name: str):
244
+
245
+ bus_name_subscriptions = self.subscriptions.get(bus_name)
246
+
247
+ if bus_name_subscriptions:
248
+ for path, proxy_object in bus_name_subscriptions.path_objects.items():
249
+
250
+ subscription_configs = self.config.get_subscription_configs(bus_name=bus_name, path=path)
251
+ for subscription_config in subscription_configs:
252
+
253
+ # Trigger flows that have a bus_name_added trigger configured
254
+ await self._trigger_bus_name_removed(subscription_config, bus_name)
255
+
256
+ # Stop schedule triggers
257
+ self.flow_scheduler.stop_flow_set(subscription_config.flows)
258
+
259
+ # Wait for completion
260
+ await self.event_broker.flow_trigger_queue.async_q.join()
261
+
262
+ # clean up all dbus matchrules
263
+ for interface in proxy_object._interfaces.values():
264
+ proxy_interface: dbus_aio.proxy_object.ProxyInterface = interface
265
+
266
+ # officially you should do 'off_...' but the below is easier
267
+ # proxy_interface.off_properties_changed(self.on_properties_changed)
268
+
269
+ # clean lingering interface matchrule from bus
270
+ if proxy_interface._signal_match_rule in self.bus._match_rules.keys():
271
+ self.bus._remove_match_rule(proxy_interface._signal_match_rule)
272
+
273
+ # clean lingering interface messgage handler from bus
274
+ self.bus.remove_message_handler(proxy_interface._message_handler)
275
+
276
+ del self.subscriptions[bus_name]
277
+
278
+ async def _trigger_bus_name_removed(self, subscription_config: SubscriptionConfig, bus_name: str):
279
+
280
+ # Trigger flows that have a bus_name_removed trigger configured
281
+ for flow in subscription_config.flows:
282
+ for trigger in flow.triggers:
283
+ if trigger.type == "bus_name_removed":
284
+ context = {
285
+ "bus_name": bus_name,
286
+ }
287
+ trigger_message = FlowTriggerMessage(flow, trigger, datetime.now(), context)
288
+ await self.event_broker.flow_trigger_queue.async_q.put(trigger_message)
289
+
290
+ async def _dbus_name_owner_changed_callback(self, name, old_owner, new_owner):
291
+
292
+ logger.debug(f'NameOwnerChanged: name=q{name}, old_owner={old_owner}, new_owner={new_owner}')
293
+
294
+ if new_owner and not old_owner:
295
+ logger.debug(f'NameOwnerChanged.new: name={name}')
296
+ await self._handle_bus_name_added(name)
297
+ if old_owner and not new_owner:
298
+ logger.debug(f'NameOwnerChanged.old: name={name}')
299
+ await self._handle_bus_name_removed(name)
300
+
301
+ async def call_dbus_interface_method(self, interface: dbus_aio.proxy_object.ProxyInterface, method: str, method_args: list[Any]):
302
+
303
+ call_method_name = "call_" + camel_to_snake(method)
304
+ res = await interface.__getattribute__(call_method_name)(*method_args)
305
+
306
+ if res:
307
+ res = unwrap_dbus_object(res)
308
+
309
+ logger.debug(f"call_dbus_interface_method: bus_name={interface.bus_name}, interface={interface.introspection.name}, method={method}, res={res}")
310
+
311
+ return res
312
+
313
+ async def get_dbus_interface_property(self, interface: dbus_aio.proxy_object.ProxyInterface, property: str):
314
+
315
+ call_method_name = "get_" + camel_to_snake(property)
316
+ res = await interface.__getattribute__(call_method_name)()
317
+
318
+ if res:
319
+ res = unwrap_dbus_object(res)
320
+
321
+ logger.debug(f"get_dbus_interface_property: bus_name={interface.bus_name}, interface={interface.introspection.name}, property={property}, res={res}")
322
+
323
+ return res
324
+
325
+ async def set_dbus_interface_property(self, interface: dbus_aio.proxy_object.ProxyInterface, property: str, value):
326
+
327
+ 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}")
334
+
335
+ return res
336
+
337
+ async def mqtt_receive_queue_processor_task(self):
338
+ """Continuously processes messages from the async queue."""
339
+ while True:
340
+ msg = await self.event_broker.mqtt_receive_queue.async_q.get() # Wait for a message
341
+ try:
342
+ await self._on_mqtt_msg(msg)
343
+ except Exception as e:
344
+ logger.warning(f"mqtt_receive_queue_processor_task: Exception {e}", exc_info=True)
345
+ finally:
346
+ self.event_broker.mqtt_receive_queue.async_q.task_done()
347
+
348
+ async def dbus_signal_queue_processor_task(self):
349
+ """Continuously processes messages from the async queue."""
350
+ while True:
351
+ signal = await self.event_broker.dbus_signal_queue.async_q.get() # Wait for a message
352
+ await self._handle_on_dbus_signal(signal)
353
+ self.event_broker.dbus_signal_queue.async_q.task_done()
354
+
355
+ async def _handle_on_dbus_signal(self, signal: DbusSignalWithState):
356
+
357
+ for flow in signal.subscription_config.flows:
358
+ for trigger in flow.triggers:
359
+ if trigger.type == "dbus_signal" and signal.signal_config.signal == trigger.signal:
360
+
361
+ try:
362
+
363
+ matches_filter = True
364
+ if signal.signal_config.filter is not None:
365
+ matches_filter = signal.signal_config.matches_filter(self.app_context.templating, *signal.args)
366
+
367
+ if matches_filter:
368
+ context = {
369
+ "bus_name": signal.bus_name_subscriptions.bus_name,
370
+ "path": signal.path,
371
+ "interface": signal.interface_name,
372
+ "args": signal.args
373
+ }
374
+ trigger_message = FlowTriggerMessage(flow, trigger, datetime.now(), context)
375
+
376
+ await self.event_broker.flow_trigger_queue.async_q.put(trigger_message)
377
+ except Exception as e:
378
+ logger.warning(f"dbus_signal_queue_processor_task: Exception {e}", exc_info=True)
379
+
380
+ async def _on_mqtt_msg(self, msg: MqttMessage):
381
+ # self.queue.put({
382
+ # "topic": topic,
383
+ # "payload": payload
384
+ # })
385
+
386
+ found_matching_topic = False
387
+ for subscription_configs in self.config.subscriptions:
388
+ for interface_config in subscription_configs.interfaces:
389
+ # TODO, performance improvement
390
+ mqtt_topic = interface_config.render_mqtt_call_method_topic(self.templating, {})
391
+ found_matching_topic |= mqtt_topic == msg.topic
392
+
393
+ if not found_matching_topic:
394
+ return
395
+
396
+ logger.debug(f"on_mqtt_msg: topic={msg.topic}, payload={json.dumps(msg.payload)}")
397
+ calls_done: list[str] = []
398
+ properties_set: list[str] = []
399
+
400
+ payload_method = msg.payload.get("method")
401
+ payload_method_args = msg.payload.get("args") or []
402
+
403
+ payload_property = msg.payload.get("property")
404
+ payload_value = msg.payload.get("value")
405
+
406
+ if payload_method is None and (payload_property is None or payload_value is None):
407
+ logger.info(f"on_mqtt_msg: Unsupported payload, missing 'method' or 'property/value', got method={payload_method}, property={payload_property}, value={payload_value} from {msg.payload}")
408
+ return
409
+
410
+ for [bus_name, bus_name_subscription] in self.subscriptions.items():
411
+ for [path, proxy_object] in bus_name_subscription.path_objects.items():
412
+ for subscription_configs in self.config.get_subscription_configs(bus_name=bus_name, path=path):
413
+ for interface_config in subscription_configs.interfaces:
414
+
415
+ for method in interface_config.methods:
416
+
417
+ # filter configured method, configured topic, ...
418
+ if method.method == payload_method:
419
+ interface = proxy_object.get_interface(name=interface_config.interface)
420
+
421
+ try:
422
+ logger.info(f"on_mqtt_msg: method={method.method}, args={payload_method_args}, bus_name={bus_name}, path={path}, interface={interface_config.interface}")
423
+ await self.call_dbus_interface_method(interface, method.method, payload_method_args)
424
+ calls_done.append(method.method)
425
+ except Exception as e:
426
+ logger.warning(f"on_mqtt_msg: method={method.method}, bus_name={bus_name} failed, exception={e}")
427
+
428
+ for property in interface_config.properties:
429
+ # filter configured property, configured topic, ...
430
+ if property.property == payload_property:
431
+ interface = proxy_object.get_interface(name=interface_config.interface)
432
+
433
+ try:
434
+ logger.info(f"on_mqtt_msg: property={property.property}, value={payload_value}, bus_name={bus_name}, path={path}, interface={interface_config.interface}")
435
+ await self.set_dbus_interface_property(interface, property.property, payload_value)
436
+ properties_set.append(property.property)
437
+ except Exception as e:
438
+ logger.warning(f"on_mqtt_msg: property={property.property}, bus_name={bus_name} failed, exception={e}")
439
+
440
+ if len(calls_done) == 0 and len(properties_set) == 0:
441
+ if payload_method:
442
+ logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, method={payload_method}, active bus_names={list(self.subscriptions.keys())}")
443
+ if payload_property:
444
+ logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, property={payload_property}, active bus_names={list(self.subscriptions.keys())}")
445
+
446
+ # raw mode, payload contains: bus_name (specific or wildcard), path, interface_name
447
+ # topic: dbus2mqtt/raw (with allowlist check)
448
+
449
+ # predefined mode with topic matching from configuration
450
+ # topic: dbus2mqtt/MediaPlayer/command
@@ -0,0 +1,23 @@
1
+
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import dbus_next.aio as dbus_aio
6
+
7
+ from dbus2mqtt.config import InterfaceConfig, SubscriptionConfig
8
+
9
+
10
+ class BusNameSubscriptions:
11
+
12
+ def __init__(self, bus_name: str):
13
+ self.bus_name = bus_name
14
+ self.path_objects: dict[str, dbus_aio.proxy_object.ProxyObject] = {}
15
+
16
+ @dataclass
17
+ class SubscribedInterface:
18
+
19
+ interface_config: InterfaceConfig
20
+ subscription_config: SubscriptionConfig
21
+ bus_name: str
22
+ path: str
23
+ interface_name: str
@@ -0,0 +1,23 @@
1
+ import json
2
+ import re
3
+
4
+ import dbus_next.signature as dbus_signature
5
+
6
+
7
+ def _variant_serializer(obj):
8
+ if isinstance(obj, dbus_signature.Variant):
9
+ return obj.value
10
+ return obj
11
+
12
+ def unwrap_dbus_object(o):
13
+ # an easy way to get rid of dbus_next.signature.Variant types
14
+ res = json.dumps(o, default=_variant_serializer)
15
+ json_obj = json.loads(res)
16
+ return json_obj
17
+
18
+ def unwrap_dbus_objects(*args):
19
+ res = [unwrap_dbus_object(o) for o in args]
20
+ return res
21
+
22
+ def camel_to_snake(name):
23
+ return re.sub(r'([a-z])([A-Z])', r'\1_\2', name).lower()