dbus2mqtt 0.1.1__py3-none-any.whl → 0.2.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.py +9 -8
- dbus2mqtt/dbus/dbus_client.py +12 -10
- dbus2mqtt/dbus/dbus_util.py +1 -1
- dbus2mqtt/event_broker.py +1 -2
- dbus2mqtt/flow/__init__.py +22 -3
- dbus2mqtt/flow/actions/context_set.py +2 -0
- dbus2mqtt/flow/actions/mqtt_publish.py +12 -6
- dbus2mqtt/main.py +0 -9
- dbus2mqtt/mqtt/mqtt_client.py +9 -5
- dbus2mqtt/template/templating.py +54 -80
- {dbus2mqtt-0.1.1.dist-info → dbus2mqtt-0.2.0.dist-info}/METADATA +17 -21
- dbus2mqtt-0.2.0.dist-info/RECORD +20 -0
- dbus2mqtt-0.1.1.dist-info/RECORD +0 -20
- {dbus2mqtt-0.1.1.dist-info → dbus2mqtt-0.2.0.dist-info}/WHEEL +0 -0
- {dbus2mqtt-0.1.1.dist-info → dbus2mqtt-0.2.0.dist-info}/entry_points.txt +0 -0
- {dbus2mqtt-0.1.1.dist-info → dbus2mqtt-0.2.0.dist-info}/licenses/LICENSE +0 -0
dbus2mqtt/config.py
CHANGED
|
@@ -15,8 +15,9 @@ class SignalConfig:
|
|
|
15
15
|
filter: str | None = None
|
|
16
16
|
|
|
17
17
|
def matches_filter(self, template_engine: TemplateEngine, *args) -> bool:
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if self.filter:
|
|
19
|
+
return template_engine.render_template(self.filter, bool, { "args": args })
|
|
20
|
+
return True
|
|
20
21
|
|
|
21
22
|
@dataclass
|
|
22
23
|
class MethodConfig:
|
|
@@ -29,13 +30,15 @@ class PropertyConfig:
|
|
|
29
30
|
@dataclass
|
|
30
31
|
class InterfaceConfig:
|
|
31
32
|
interface: str
|
|
32
|
-
|
|
33
|
+
mqtt_command_topic: str | None = None
|
|
33
34
|
signals: list[SignalConfig] = field(default_factory=list)
|
|
34
35
|
methods: list[MethodConfig] = field(default_factory=list)
|
|
35
36
|
properties: list[PropertyConfig] = field(default_factory=list)
|
|
36
37
|
|
|
37
|
-
def
|
|
38
|
-
|
|
38
|
+
def render_mqtt_command_topic(self, template_engine: TemplateEngine, context: dict[str, Any]) -> Any:
|
|
39
|
+
if self.mqtt_command_topic:
|
|
40
|
+
return template_engine.render_template(self.mqtt_command_topic, str, context)
|
|
41
|
+
return None
|
|
39
42
|
|
|
40
43
|
@dataclass
|
|
41
44
|
class FlowTriggerMqttConfig:
|
|
@@ -84,8 +87,6 @@ class FlowActionContextSetConfig:
|
|
|
84
87
|
class FlowActionMqttPublishConfig:
|
|
85
88
|
topic: str
|
|
86
89
|
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
90
|
type: Literal["mqtt_publish"] = "mqtt_publish"
|
|
90
91
|
payload_type: Literal["json", "yaml", "text"] = "json"
|
|
91
92
|
|
|
@@ -125,7 +126,7 @@ class DbusConfig:
|
|
|
125
126
|
def get_subscription_configs(self, bus_name: str, path: str) -> list[SubscriptionConfig]:
|
|
126
127
|
res: list[SubscriptionConfig] = []
|
|
127
128
|
for subscription in self.subscriptions:
|
|
128
|
-
if fnmatch.fnmatchcase(bus_name, subscription.bus_name) and
|
|
129
|
+
if fnmatch.fnmatchcase(bus_name, subscription.bus_name) and path == subscription.path:
|
|
129
130
|
res.append(subscription)
|
|
130
131
|
return res
|
|
131
132
|
|
dbus2mqtt/dbus/dbus_client.py
CHANGED
|
@@ -124,7 +124,7 @@ class DbusClient:
|
|
|
124
124
|
|
|
125
125
|
on_signal_method_name = "on_" + camel_to_snake(signal_config.signal)
|
|
126
126
|
dbus_signal_state = {
|
|
127
|
-
"
|
|
127
|
+
"bus_name": bus_name,
|
|
128
128
|
"path": path,
|
|
129
129
|
"interface_name": interface.name,
|
|
130
130
|
"subscription_config": subscription_config,
|
|
@@ -354,6 +354,8 @@ class DbusClient:
|
|
|
354
354
|
|
|
355
355
|
async def _handle_on_dbus_signal(self, signal: DbusSignalWithState):
|
|
356
356
|
|
|
357
|
+
logger.debug(f"dbus_signal: signal={signal.signal_config.signal}, args={signal.args}, bus_name={signal.bus_name}, path={signal.path}, interface={signal.interface_name}")
|
|
358
|
+
|
|
357
359
|
for flow in signal.subscription_config.flows:
|
|
358
360
|
for trigger in flow.triggers:
|
|
359
361
|
if trigger.type == "dbus_signal" and signal.signal_config.signal == trigger.signal:
|
|
@@ -366,7 +368,7 @@ class DbusClient:
|
|
|
366
368
|
|
|
367
369
|
if matches_filter:
|
|
368
370
|
context = {
|
|
369
|
-
"bus_name": signal.
|
|
371
|
+
"bus_name": signal.bus_name,
|
|
370
372
|
"path": signal.path,
|
|
371
373
|
"interface": signal.interface_name,
|
|
372
374
|
"args": signal.args
|
|
@@ -387,15 +389,15 @@ class DbusClient:
|
|
|
387
389
|
for subscription_configs in self.config.subscriptions:
|
|
388
390
|
for interface_config in subscription_configs.interfaces:
|
|
389
391
|
# TODO, performance improvement
|
|
390
|
-
mqtt_topic = interface_config.
|
|
392
|
+
mqtt_topic = interface_config.render_mqtt_command_topic(self.templating, {})
|
|
391
393
|
found_matching_topic |= mqtt_topic == msg.topic
|
|
392
394
|
|
|
393
395
|
if not found_matching_topic:
|
|
394
396
|
return
|
|
395
397
|
|
|
396
398
|
logger.debug(f"on_mqtt_msg: topic={msg.topic}, payload={json.dumps(msg.payload)}")
|
|
397
|
-
|
|
398
|
-
|
|
399
|
+
matched_method = False
|
|
400
|
+
matched_property = False
|
|
399
401
|
|
|
400
402
|
payload_method = msg.payload.get("method")
|
|
401
403
|
payload_method_args = msg.payload.get("args") or []
|
|
@@ -417,27 +419,27 @@ class DbusClient:
|
|
|
417
419
|
# filter configured method, configured topic, ...
|
|
418
420
|
if method.method == payload_method:
|
|
419
421
|
interface = proxy_object.get_interface(name=interface_config.interface)
|
|
422
|
+
matched_method = True
|
|
420
423
|
|
|
421
424
|
try:
|
|
422
425
|
logger.info(f"on_mqtt_msg: method={method.method}, args={payload_method_args}, bus_name={bus_name}, path={path}, interface={interface_config.interface}")
|
|
423
426
|
await self.call_dbus_interface_method(interface, method.method, payload_method_args)
|
|
424
|
-
calls_done.append(method.method)
|
|
425
427
|
except Exception as e:
|
|
426
|
-
logger.warning(f"on_mqtt_msg: method={method.method}, bus_name={bus_name} failed, exception={e}")
|
|
428
|
+
logger.warning(f"on_mqtt_msg: method={method.method}, args={payload_method_args}, bus_name={bus_name} failed, exception={e}")
|
|
427
429
|
|
|
428
430
|
for property in interface_config.properties:
|
|
429
431
|
# filter configured property, configured topic, ...
|
|
430
432
|
if property.property == payload_property:
|
|
431
433
|
interface = proxy_object.get_interface(name=interface_config.interface)
|
|
434
|
+
matched_property = True
|
|
432
435
|
|
|
433
436
|
try:
|
|
434
437
|
logger.info(f"on_mqtt_msg: property={property.property}, value={payload_value}, bus_name={bus_name}, path={path}, interface={interface_config.interface}")
|
|
435
438
|
await self.set_dbus_interface_property(interface, property.property, payload_value)
|
|
436
|
-
properties_set.append(property.property)
|
|
437
439
|
except Exception as e:
|
|
438
|
-
logger.warning(f"on_mqtt_msg: property={property.property}, bus_name={bus_name} failed, exception={e}")
|
|
440
|
+
logger.warning(f"on_mqtt_msg: property={property.property}, value={payload_value}, bus_name={bus_name} failed, exception={e}")
|
|
439
441
|
|
|
440
|
-
if
|
|
442
|
+
if not matched_method and not matched_property:
|
|
441
443
|
if payload_method:
|
|
442
444
|
logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, method={payload_method}, active bus_names={list(self.subscriptions.keys())}")
|
|
443
445
|
if payload_property:
|
dbus2mqtt/dbus/dbus_util.py
CHANGED
dbus2mqtt/event_broker.py
CHANGED
|
@@ -13,7 +13,6 @@ from dbus2mqtt.config import (
|
|
|
13
13
|
SignalConfig,
|
|
14
14
|
SubscriptionConfig,
|
|
15
15
|
)
|
|
16
|
-
from dbus2mqtt.dbus.dbus_types import BusNameSubscriptions
|
|
17
16
|
|
|
18
17
|
logger = logging.getLogger(__name__)
|
|
19
18
|
|
|
@@ -26,7 +25,7 @@ class MqttMessage:
|
|
|
26
25
|
|
|
27
26
|
@dataclass
|
|
28
27
|
class DbusSignalWithState:
|
|
29
|
-
|
|
28
|
+
bus_name: str
|
|
30
29
|
path: str
|
|
31
30
|
interface_name: str
|
|
32
31
|
subscription_config: SubscriptionConfig
|
dbus2mqtt/flow/__init__.py
CHANGED
|
@@ -6,15 +6,34 @@ class FlowExecutionContext:
|
|
|
6
6
|
|
|
7
7
|
def __init__(self, name: str | None, global_flows_context: dict[str, Any], flow_context: dict[str, Any]):
|
|
8
8
|
self.name = name
|
|
9
|
+
|
|
9
10
|
self.global_flows_context = global_flows_context
|
|
11
|
+
"""
|
|
12
|
+
Global flows context which is shared across all flows.
|
|
13
|
+
Modifiable by user.
|
|
14
|
+
**Not** cleaned up after flow execution.
|
|
15
|
+
"""
|
|
16
|
+
|
|
10
17
|
self.flow_context = flow_context
|
|
18
|
+
"""
|
|
19
|
+
Flow context which contains flow specific context like 'subscription_bus_name'.
|
|
20
|
+
**Not** modifiable by user.
|
|
21
|
+
**Not** cleaned up after flow execution.
|
|
22
|
+
"""
|
|
11
23
|
|
|
12
|
-
# per flow execution context
|
|
13
24
|
self.context: dict[str, Any] = {}
|
|
25
|
+
"""
|
|
26
|
+
Per flow execution context.
|
|
27
|
+
Modifiable by user.
|
|
28
|
+
Cleaned up after each flow execution
|
|
29
|
+
"""
|
|
14
30
|
|
|
15
31
|
def get_aggregated_context(self) -> dict[str, Any]:
|
|
16
|
-
"""
|
|
17
|
-
|
|
32
|
+
"""
|
|
33
|
+
Get the aggregated context for the flow execution.
|
|
34
|
+
Merges global flows context, flow context, and local context
|
|
35
|
+
"""
|
|
36
|
+
|
|
18
37
|
context = {}
|
|
19
38
|
if self.global_flows_context:
|
|
20
39
|
context.update(self.global_flows_context)
|
|
@@ -15,12 +15,14 @@ class ContextSetAction(FlowAction):
|
|
|
15
15
|
async def execute(self, context: FlowExecutionContext):
|
|
16
16
|
|
|
17
17
|
aggregated_context = context.get_aggregated_context()
|
|
18
|
+
|
|
18
19
|
if self.config.global_context:
|
|
19
20
|
context_new = await self.templating.async_render_template(self.config.global_context, dict, aggregated_context)
|
|
20
21
|
logger.debug(f"Update global_context with: {context_new}")
|
|
21
22
|
context.global_flows_context.update(context_new)
|
|
22
23
|
|
|
23
24
|
if self.config.context:
|
|
25
|
+
|
|
24
26
|
context_new = await self.templating.async_render_template(self.config.context, dict, aggregated_context)
|
|
25
27
|
logger.debug(f"Update context with: {context_new}")
|
|
26
28
|
context.context.update(context_new)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
|
-
from jinja2.exceptions import
|
|
4
|
+
from jinja2.exceptions import TemplateError
|
|
5
5
|
|
|
6
6
|
from dbus2mqtt import AppContext
|
|
7
7
|
from dbus2mqtt.config import FlowActionMqttPublishConfig
|
|
@@ -24,14 +24,20 @@ class MqttPublishAction(FlowAction):
|
|
|
24
24
|
try:
|
|
25
25
|
mqtt_topic = await self.templating.async_render_template(self.config.topic, str, render_context)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if self.config.payload_type == "text":
|
|
28
|
+
res_type = str
|
|
29
|
+
else:
|
|
30
|
+
res_type = dict
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
payload = await self.templating.async_render_template(self.config.payload_template, res_type, render_context)
|
|
33
|
+
|
|
34
|
+
except TemplateError as e:
|
|
35
|
+
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)
|
|
32
36
|
return
|
|
33
37
|
except Exception as e:
|
|
34
|
-
|
|
38
|
+
# Dont log full exception info to avoid log spamming on dbus errors
|
|
39
|
+
# due to clients disconnecting
|
|
40
|
+
logger.warning(f"Error rendering jinja template, flow: '{context.name or ''}', msg={e} payload_template={self.config.payload_template}, render_context={render_context}")
|
|
35
41
|
return
|
|
36
42
|
|
|
37
43
|
logger.debug(f"public_mqtt: flow={context.name}, payload={payload}")
|
dbus2mqtt/main.py
CHANGED
|
@@ -121,15 +121,6 @@ def main():
|
|
|
121
121
|
apscheduler_logger = logging.getLogger("apscheduler")
|
|
122
122
|
apscheduler_logger.setLevel(logging.WARNING)
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
# handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)s:%(name)s:%(message)s'))
|
|
126
|
-
|
|
127
|
-
# logger = colorlog.getLogger('')
|
|
128
|
-
# for handler in logger.handlers:
|
|
129
|
-
# print(handler.st)
|
|
130
|
-
# if isinstance(handler, colorlog.StreamHandler):
|
|
131
|
-
# handler.setFormatter(colorlog.ColoredFormatter('%(log_color)s%(levelname)s:%(name)s:%(message)s'))
|
|
132
|
-
|
|
133
124
|
logger.debug(f"config: {config}")
|
|
134
125
|
|
|
135
126
|
asyncio.run(run(config))
|
dbus2mqtt/mqtt/mqtt_client.py
CHANGED
|
@@ -43,15 +43,14 @@ class MqttClient:
|
|
|
43
43
|
port=self.config.port
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
# def on_dbus_signal(self, bus_name: str, path: str, interface: str, signal: str, topic, msg: dict[str, Any]):
|
|
47
|
-
# payload = json.dumps(msg)
|
|
48
|
-
# logger.debug(f"on_dbus_signal: payload={payload}")
|
|
49
|
-
# self.client.publish(topic=topic, payload=payload)
|
|
50
|
-
|
|
51
46
|
async def mqtt_publish_queue_processor_task(self):
|
|
47
|
+
|
|
48
|
+
first_message = True
|
|
49
|
+
|
|
52
50
|
"""Continuously processes messages from the async queue."""
|
|
53
51
|
while True:
|
|
54
52
|
msg = await self.event_broker.mqtt_publish_queue.async_q.get() # Wait for a message
|
|
53
|
+
|
|
55
54
|
try:
|
|
56
55
|
payload = msg.payload
|
|
57
56
|
type = msg.payload_serialization_type
|
|
@@ -65,6 +64,11 @@ class MqttClient:
|
|
|
65
64
|
|
|
66
65
|
logger.debug(f"mqtt_publish_queue_processor_task: payload={payload}")
|
|
67
66
|
self.client.publish(topic=msg.topic, payload=payload)
|
|
67
|
+
|
|
68
|
+
if first_message:
|
|
69
|
+
logger.info(f"First message published: topic={msg.topic}, payload={payload}")
|
|
70
|
+
first_message = False
|
|
71
|
+
|
|
68
72
|
except Exception as e:
|
|
69
73
|
logger.warning(f"mqtt_publish_queue_processor_task: Exception {e}", exc_info=True)
|
|
70
74
|
finally:
|
dbus2mqtt/template/templating.py
CHANGED
|
@@ -1,49 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Environment,
|
|
10
|
-
StrictUndefined,
|
|
11
|
-
)
|
|
12
|
-
from yaml import SafeDumper, SafeLoader
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _represent_template_str(dumper: SafeDumper, data: str):
|
|
16
|
-
data = data.replace("{{", "template:{{", 1)
|
|
17
|
-
data = data.replace("}}", "}}:template", 1)
|
|
18
|
-
# return dumper.represent_str(f"template:{data}:template")
|
|
19
|
-
return dumper.represent_str(data)
|
|
20
|
-
|
|
21
|
-
class _CustomSafeLoader(SafeLoader):
|
|
22
|
-
def __init__(self, stream):
|
|
23
|
-
super().__init__(stream)
|
|
24
|
-
|
|
25
|
-
# Disable parsing ISO date strings
|
|
26
|
-
self.add_constructor('tag:yaml.org,2002:timestamp', lambda _l, n: n.value)
|
|
27
|
-
|
|
28
|
-
class _CustomSafeDumper(SafeDumper):
|
|
29
|
-
def __init__(self, stream, **kwargs):
|
|
30
|
-
super().__init__(stream, **kwargs)
|
|
31
|
-
self.add_representer(_TemplatedStr, _represent_template_str)
|
|
32
|
-
|
|
33
|
-
class _TemplatedStr(str):
|
|
34
|
-
"""A marker class to force template string formatting in YAML."""
|
|
35
|
-
pass
|
|
36
|
-
|
|
37
|
-
def _mark_templates(obj):
|
|
38
|
-
if isinstance(obj, dict):
|
|
39
|
-
return {k: _mark_templates(v) for k, v in obj.items()}
|
|
40
|
-
elif isinstance(obj, list):
|
|
41
|
-
return [_mark_templates(v) for v in obj]
|
|
42
|
-
elif isinstance(obj, str):
|
|
43
|
-
s = obj.strip()
|
|
44
|
-
if s.startswith("{{") and s.endswith("}}"):
|
|
45
|
-
return _TemplatedStr(obj)
|
|
46
|
-
return obj
|
|
3
|
+
from typing import Any, TypeVar
|
|
4
|
+
|
|
5
|
+
from jinja2 import BaseLoader, StrictUndefined, TemplateError
|
|
6
|
+
from jinja2.nativetypes import NativeEnvironment
|
|
7
|
+
|
|
8
|
+
TemplateResultType = TypeVar('TemplateResultType')
|
|
47
9
|
|
|
48
10
|
class TemplateEngine:
|
|
49
11
|
def __init__(self):
|
|
@@ -51,14 +13,14 @@ class TemplateEngine:
|
|
|
51
13
|
engine_globals = {}
|
|
52
14
|
engine_globals['now'] = datetime.now
|
|
53
15
|
|
|
54
|
-
self.jinja2_env =
|
|
16
|
+
self.jinja2_env = NativeEnvironment(
|
|
55
17
|
loader=BaseLoader(),
|
|
56
18
|
extensions=['jinja2_ansible_filters.AnsibleCoreFiltersExtension'],
|
|
57
19
|
undefined=StrictUndefined,
|
|
58
20
|
keep_trailing_newline=False
|
|
59
21
|
)
|
|
60
22
|
|
|
61
|
-
self.jinja2_async_env =
|
|
23
|
+
self.jinja2_async_env = NativeEnvironment(
|
|
62
24
|
loader=BaseLoader(),
|
|
63
25
|
extensions=['jinja2_ansible_filters.AnsibleCoreFiltersExtension'],
|
|
64
26
|
undefined=StrictUndefined,
|
|
@@ -66,7 +28,6 @@ class TemplateEngine:
|
|
|
66
28
|
)
|
|
67
29
|
|
|
68
30
|
self.app_context: dict[str, Any] = {}
|
|
69
|
-
# self.dbus_context: dict[str, Any] = {}
|
|
70
31
|
|
|
71
32
|
self.jinja2_env.globals.update(engine_globals)
|
|
72
33
|
self.jinja2_async_env.globals.update(engine_globals)
|
|
@@ -78,52 +39,65 @@ class TemplateEngine:
|
|
|
78
39
|
def update_app_context(self, context: dict[str, Any]):
|
|
79
40
|
self.app_context.update(context)
|
|
80
41
|
|
|
81
|
-
def
|
|
82
|
-
template_str = _mark_templates(value)
|
|
83
|
-
template_str = yaml.dump(template_str, Dumper=_CustomSafeDumper)
|
|
84
|
-
# value= yaml.safe_dump(value, default_style=None)
|
|
85
|
-
# print(f"_dict_to_templatable_str: {value}")
|
|
86
|
-
template_str = template_str.replace("template:{{", "{{").replace("}}:template", "}}")
|
|
87
|
-
# print(value)
|
|
88
|
-
return template_str
|
|
42
|
+
def _convert_value(self, res: Any, res_type: type[TemplateResultType]) -> TemplateResultType:
|
|
89
43
|
|
|
90
|
-
|
|
91
|
-
|
|
44
|
+
if isinstance(res, res_type):
|
|
45
|
+
return res
|
|
92
46
|
|
|
93
|
-
|
|
47
|
+
try:
|
|
48
|
+
return res_type(res) # type: ignore
|
|
94
49
|
|
|
95
|
-
|
|
96
|
-
|
|
50
|
+
except Exception as e:
|
|
51
|
+
raise ValueError(f"Error converting rendered template result from '{type(res).__name__}' to '{res_type.__name__}'") from e
|
|
97
52
|
|
|
98
|
-
|
|
99
|
-
raise ValueError(f"Unsupported result type: {res_type}")
|
|
53
|
+
def _render_template_nested(self, templatable: str | dict[str, Any], context: dict[str, Any] = {}) -> Any:
|
|
100
54
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
55
|
+
if isinstance(templatable, str):
|
|
56
|
+
try:
|
|
57
|
+
return self.jinja2_env.from_string(templatable).render(**context)
|
|
58
|
+
except TemplateError as e:
|
|
59
|
+
raise TemplateError(f"Error compiling template, template={templatable}: {e}") from e
|
|
104
60
|
|
|
105
|
-
|
|
61
|
+
elif isinstance(templatable, dict):
|
|
62
|
+
res = {}
|
|
63
|
+
for k, v in templatable.items():
|
|
64
|
+
if isinstance(v, dict) or isinstance(v, str):
|
|
65
|
+
res[k] = self._render_template_nested(v, context)
|
|
66
|
+
else:
|
|
67
|
+
res[k] = v
|
|
68
|
+
return res
|
|
106
69
|
|
|
107
|
-
|
|
108
|
-
res = self._render_result_to_dict(res)
|
|
70
|
+
def render_template(self, templatable: str | dict[str, Any], res_type: type[TemplateResultType], context: dict[str, Any] = {}) -> TemplateResultType:
|
|
109
71
|
|
|
110
|
-
|
|
72
|
+
if isinstance(templatable, dict) and res_type is not dict:
|
|
73
|
+
raise ValueError(f"res_type should dict for dictionary templates, templatable={templatable}")
|
|
111
74
|
|
|
112
|
-
|
|
75
|
+
res = self._render_template_nested(templatable, context)
|
|
76
|
+
res = self._convert_value(res, res_type)
|
|
77
|
+
return res
|
|
113
78
|
|
|
114
|
-
|
|
115
|
-
return None
|
|
79
|
+
async def _async_render_template_nested(self, templatable: str | dict[str, Any], context: dict[str, Any] = {}) -> Any:
|
|
116
80
|
|
|
117
|
-
if
|
|
118
|
-
|
|
81
|
+
if isinstance(templatable, str):
|
|
82
|
+
try:
|
|
83
|
+
return await self.jinja2_async_env.from_string(templatable).render_async(**context)
|
|
84
|
+
except TemplateError as e:
|
|
85
|
+
raise TemplateError(f"Error compiling template, template={templatable}: {e}") from e
|
|
119
86
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
87
|
+
elif isinstance(templatable, dict):
|
|
88
|
+
res = {}
|
|
89
|
+
for k, v in templatable.items():
|
|
90
|
+
if isinstance(v, dict) or isinstance(v, str):
|
|
91
|
+
res[k] = await self._async_render_template_nested(v, context)
|
|
92
|
+
else:
|
|
93
|
+
res[k] = v
|
|
94
|
+
return res
|
|
123
95
|
|
|
124
|
-
|
|
96
|
+
async def async_render_template(self, templatable: str | dict[str, Any], res_type: type[TemplateResultType], context: dict[str, Any] = {}) -> TemplateResultType:
|
|
125
97
|
|
|
126
|
-
if res_type is dict:
|
|
127
|
-
|
|
98
|
+
if isinstance(templatable, dict) and res_type is not dict:
|
|
99
|
+
raise ValueError(f"res_type should be dict for dictionary templates, templatable={templatable}")
|
|
128
100
|
|
|
101
|
+
res = await self._async_render_template_nested(templatable, context)
|
|
102
|
+
res = self._convert_value(res, res_type)
|
|
129
103
|
return res
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbus2mqtt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -9,6 +9,7 @@ License-File: LICENSE
|
|
|
9
9
|
Keywords: dbus,home-assistant,mpris,mqtt,python
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
13
|
Classifier: Operating System :: POSIX :: Linux
|
|
13
14
|
Classifier: Programming Language :: Python :: 3
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -31,8 +32,6 @@ Description-Content-Type: text/markdown
|
|
|
31
32
|
|
|
32
33
|
# dbus2mqtt
|
|
33
34
|
|
|
34
|
-
> **⚠️ Warning:** This project has no releases yet. Running from source works. Docker images and Python packages are planned but not yet available.
|
|
35
|
-
|
|
36
35
|
**dbus2mqtt** is a Python application that bridges **Linux D-Bus** with **MQTT**.
|
|
37
36
|
It lets you forward D-Bus signals and properties to MQTT topics, call D-Bus methods via MQTT messages, and shape payloads using flexible **Jinja2 templating**.
|
|
38
37
|
|
|
@@ -46,12 +45,11 @@ This makes it easy to integrate Linux desktop services or system signals into MQ
|
|
|
46
45
|
* 📡 Expose **D-Bus methods** for remote control via MQTT messages.
|
|
47
46
|
* 🏠 Includes example configurations for **MPRIS** and **Home Assistant Media Player** integration.
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
## Project status
|
|
49
|
+
|
|
50
|
+
**dbus2mqtt** is considered stable for the use-cases it has been tested against, and is actively being developed. Documentation is continuously being improved.
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
* Release a docker image
|
|
53
|
-
* Improve error handling when deleting message with 'retain' set. WARNING:dbus2mqtt.mqtt_client:on_message: Unexpected payload, expecting json, topic=dbus2mqtt/org.mpris.MediaPlayer2/command, payload=, error=Expecting value: line 1 column 1 (char 0)
|
|
54
|
-
* Property set only works the first time, need to restart after which the first set will work again
|
|
52
|
+
Initial testing has focused on MPRIS integration. A table of tested MPRIS players and their supported methods can be found here: [home_assistant_media_player.md](https://github.com/jwnmulder/dbus2mqtt/blob/main/docs/examples/home_assistant_media_player.md)
|
|
55
53
|
|
|
56
54
|
## Getting started with dbus2mqtt
|
|
57
55
|
|
|
@@ -82,7 +80,7 @@ dbus:
|
|
|
82
80
|
topic: dbus2mqtt/org.mpris.MediaPlayer2/state
|
|
83
81
|
payload_type: json
|
|
84
82
|
payload_template: |
|
|
85
|
-
{{ dbus_call(mpris_bus_name, path, 'org.freedesktop.DBus.Properties', 'GetAll', ['org.mpris.MediaPlayer2.Player'])
|
|
83
|
+
{{ dbus_call(mpris_bus_name, path, 'org.freedesktop.DBus.Properties', 'GetAll', ['org.mpris.MediaPlayer2.Player']) }}
|
|
86
84
|
```
|
|
87
85
|
|
|
88
86
|
MQTT connection details can be configured in that same `config.yaml` file or via environment variables. For now create a `.env` file with the following contents.
|
|
@@ -94,17 +92,17 @@ MQTT__USERNAME=
|
|
|
94
92
|
MQTT__PASSWORD=
|
|
95
93
|
```
|
|
96
94
|
|
|
97
|
-
###
|
|
98
|
-
|
|
99
|
-
To run dbus2mqtt from source (requires uv to be installed)
|
|
95
|
+
### Install and run dbus2mqtt
|
|
100
96
|
|
|
101
97
|
```bash
|
|
102
|
-
|
|
98
|
+
python -m pip install dbus2mqtt
|
|
99
|
+
dbus2mqtt --config config.yaml
|
|
103
100
|
```
|
|
104
101
|
|
|
102
|
+
|
|
105
103
|
### Run using docker with auto start behavior
|
|
106
104
|
|
|
107
|
-
To build and run dbus2mqtt using Docker with the [home_assistant_media_player.yaml](docs/examples/home_assistant_media_player.yaml) example from this repository
|
|
105
|
+
To build and run dbus2mqtt using Docker with the [home_assistant_media_player.yaml](https://github.com/jwnmulder/dbus2mqtt/blob/main/docs/examples/home_assistant_media_player.yaml) example from this repository.
|
|
108
106
|
|
|
109
107
|
```bash
|
|
110
108
|
# setup configuration
|
|
@@ -112,10 +110,8 @@ mkdir -p $HOME/.config/dbus2mqtt
|
|
|
112
110
|
cp docs/examples/home_assistant_media_player.yaml $HOME/.config/dbus2mqtt/config.yaml
|
|
113
111
|
cp .env.example $HOME/.config/dbus2mqtt/.env
|
|
114
112
|
|
|
115
|
-
# build image
|
|
116
|
-
docker build -t jwnmulder/dbus2mqtt:latest .
|
|
117
|
-
|
|
118
113
|
# run image and automatically start on reboot
|
|
114
|
+
sudo docker pull jwnmulder/dbus2mqtt
|
|
119
115
|
docker run --detach --name dbus2mqtt \
|
|
120
116
|
--volume "$HOME"/.config/dbus2mqtt:"$HOME"/.config/dbus2mqtt \
|
|
121
117
|
--volume /run/user:/run/user \
|
|
@@ -133,7 +129,7 @@ sudo docker logs dbus2mqtt -f
|
|
|
133
129
|
|
|
134
130
|
## Examples
|
|
135
131
|
|
|
136
|
-
This repository contains
|
|
132
|
+
This repository contains examples under [docs/examples](https://github.com/jwnmulder/dbus2mqtt/blob/main//docs/examples.md). The most complete one being [MPRIS to Home Assistant Media Player integration](https://github.com/jwnmulder/dbus2mqtt/blob/main/docs/examples/home_assistant_media_player.md)
|
|
137
133
|
|
|
138
134
|
## Configuration reference
|
|
139
135
|
|
|
@@ -169,7 +165,7 @@ dbus:
|
|
|
169
165
|
path: /org/mpris/MediaPlayer2
|
|
170
166
|
interfaces:
|
|
171
167
|
- interface: org.mpris.MediaPlayer2.Player
|
|
172
|
-
|
|
168
|
+
mqtt_command_topic: dbus2mqtt/org.mpris.MediaPlayer2/command
|
|
173
169
|
methods:
|
|
174
170
|
- method: Pause
|
|
175
171
|
- method: Play
|
|
@@ -216,8 +212,8 @@ dbus:
|
|
|
216
212
|
|
|
217
213
|
## Flows
|
|
218
214
|
|
|
219
|
-
TODO: Document flows, for now see the [MPRIS to Home Assistant Media Player integration](docs/examples/home_assistant_media_player.md) example
|
|
215
|
+
TODO: Document flows, for now see the [MPRIS to Home Assistant Media Player integration](https://github.com/jwnmulder/dbus2mqtt/blob/main/docs/examples/home_assistant_media_player.md) example
|
|
220
216
|
|
|
221
217
|
## Jinja templating
|
|
222
218
|
|
|
223
|
-
TODO: Document Jinja templating, for now see the [MPRIS to Home Assistant Media Player integration](docs/examples/home_assistant_media_player.md) example
|
|
219
|
+
TODO: Document Jinja templating, for now see the [MPRIS to Home Assistant Media Player integration](https://github.com/jwnmulder/dbus2mqtt/blob/main/docs/examples/home_assistant_media_player.md) example
|
|
@@ -0,0 +1,20 @@
|
|
|
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,,
|
dbus2mqtt-0.1.1.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
dbus2mqtt/__init__.py,sha256=VunubKEH4_lne9Ttd2YRpyXjZMIAzyD2eiJ2sfvvTFU,362
|
|
2
|
-
dbus2mqtt/__main__.py,sha256=NAoa3nwgBSQI22eWzzRx61SIDThDwXmUofWWZU3_4-Q,71
|
|
3
|
-
dbus2mqtt/config.py,sha256=zcRP0Q_PHz4ypOhSvvxwlJyT6C3BE1vpjcbiUqXcXDw,4198
|
|
4
|
-
dbus2mqtt/event_broker.py,sha256=bFxjrQae87XFWmV6OrYcxpiDPrVQEJv0DJucnI7HZFY,1926
|
|
5
|
-
dbus2mqtt/main.py,sha256=3-2rorUesggZ9Gv3FksqZw_wapfjNRvjibaX51mVadA,4281
|
|
6
|
-
dbus2mqtt/dbus/dbus_client.py,sha256=Hoa7p9zUyFCPKQtl90gCG8eBuQ8annrQoAsvjAekEMI,21882
|
|
7
|
-
dbus2mqtt/dbus/dbus_types.py,sha256=bUik8LWPnLLJhhJExPuvyn1_MmkUjTn4jxXh3EkYgzI,495
|
|
8
|
-
dbus2mqtt/dbus/dbus_util.py,sha256=Lk8w57j8TQxUs8d-DnTcB8wLx-cwkzzHtJY3RbTcXbo,570
|
|
9
|
-
dbus2mqtt/flow/__init__.py,sha256=oatdVeBUjAJkUwM9DZsj67Z2rptOHWUn_QFVJrRu2DA,1063
|
|
10
|
-
dbus2mqtt/flow/flow_processor.py,sha256=v5LvcNe-IpEXkWgBW0mK76khrqb5aFkZF0Nw2IvxIEs,7834
|
|
11
|
-
dbus2mqtt/flow/actions/context_set.py,sha256=_XWJ-xHx6nhRYVrd8pBKE3rePc1KL0VU1W0q1pqYRBE,1085
|
|
12
|
-
dbus2mqtt/flow/actions/mqtt_publish.py,sha256=CmW-CPIYjJ0VldcLjbK6uYDkyE65btQzT1797F2F1D0,1650
|
|
13
|
-
dbus2mqtt/mqtt/mqtt_client.py,sha256=FxP82_P5mIbiFk5LOYBPRZN7zYp8XQY7xed2HCVXQoo,4071
|
|
14
|
-
dbus2mqtt/template/dbus_template_functions.py,sha256=mSZr4s7XzmMCYnJYV1MlBWOBz71MGEBj6mLJzIapNf8,2427
|
|
15
|
-
dbus2mqtt/template/templating.py,sha256=aBDLW6RaqiivvWjzu3jhTDdZvtZCWMtxGxm_VCaZDKs,4202
|
|
16
|
-
dbus2mqtt-0.1.1.dist-info/METADATA,sha256=7dImzxIO65xyS3sJ6qyku2LjbkYNUVHgxNc33MjTwJE,7649
|
|
17
|
-
dbus2mqtt-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
-
dbus2mqtt-0.1.1.dist-info/entry_points.txt,sha256=pmmacoHCsvTTUB5dIPaY4i0H9gX7BQlpd3rBU-Jbv3A,50
|
|
19
|
-
dbus2mqtt-0.1.1.dist-info/licenses/LICENSE,sha256=a4bIEgyA9rrnAfUN90CgbgZ6BQIFHeABkk0JihiBaxM,1074
|
|
20
|
-
dbus2mqtt-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|