dbus2mqtt 0.4.4__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dbus2mqtt might be problematic. Click here for more details.

@@ -3,9 +3,9 @@ import uuid
3
3
  import warnings
4
4
 
5
5
  from dataclasses import dataclass, field
6
- from typing import Annotated, Any, Literal
6
+ from typing import Any, Literal
7
7
 
8
- from pydantic import Field, SecretStr
8
+ from jsonargparse.typing import SecretStr
9
9
 
10
10
  from dbus2mqtt.template.templating import TemplateEngine
11
11
 
@@ -32,6 +32,7 @@ class PropertyConfig:
32
32
  class InterfaceConfig:
33
33
  interface: str
34
34
  mqtt_command_topic: str | None = None
35
+ mqtt_response_topic: str | None = None
35
36
  signals: list[SignalConfig] = field(default_factory=list)
36
37
  methods: list[MethodConfig] = field(default_factory=list)
37
38
  properties: list[PropertyConfig] = field(default_factory=list)
@@ -41,11 +42,10 @@ class InterfaceConfig:
41
42
  return template_engine.render_template(self.mqtt_command_topic, str, context)
42
43
  return None
43
44
 
44
- @dataclass
45
- class FlowTriggerMqttConfig:
46
- type: Literal["mqtt"]
47
- topic: str
48
- filter: str | None = None
45
+ def render_mqtt_response_topic(self, template_engine: TemplateEngine, context: dict[str, Any]) -> str | None:
46
+ if self.mqtt_response_topic:
47
+ return template_engine.render_template(self.mqtt_response_topic, str, context)
48
+ return None
49
49
 
50
50
  @dataclass
51
51
  class FlowTriggerScheduleConfig:
@@ -87,10 +87,26 @@ class FlowTriggerObjectRemovedConfig:
87
87
  type: Literal["object_removed"] = "object_removed"
88
88
  # filter: str | None = None
89
89
 
90
- FlowTriggerConfig = Annotated[
91
- FlowTriggerMqttConfig | FlowTriggerScheduleConfig | FlowTriggerDbusSignalConfig | FlowTriggerBusNameAddedConfig | FlowTriggerBusNameRemovedConfig | FlowTriggerObjectAddedConfig | FlowTriggerObjectRemovedConfig,
92
- Field(discriminator="type")
93
- ]
90
+ @dataclass
91
+ class FlowTriggerMqttMessageConfig:
92
+ topic: str
93
+ type: Literal["mqtt_message"] = "mqtt_message"
94
+ filter: str | None = None
95
+
96
+ def matches_filter(self, template_engine: TemplateEngine, trigger_context: dict[str, Any]) -> bool:
97
+ if self.filter:
98
+ return template_engine.render_template(self.filter, bool, trigger_context)
99
+ return True
100
+
101
+ FlowTriggerConfig = (
102
+ FlowTriggerScheduleConfig
103
+ | FlowTriggerDbusSignalConfig
104
+ | FlowTriggerBusNameAddedConfig
105
+ | FlowTriggerBusNameRemovedConfig
106
+ | FlowTriggerObjectAddedConfig
107
+ | FlowTriggerObjectRemovedConfig
108
+ | FlowTriggerMqttMessageConfig
109
+ )
94
110
 
95
111
  @dataclass
96
112
  class FlowActionContextSetConfig:
@@ -113,10 +129,11 @@ class FlowActionLogConfig:
113
129
  type: Literal["log"] = "log"
114
130
  level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
115
131
 
116
- FlowActionConfig = Annotated[
117
- FlowActionMqttPublishConfig | FlowActionContextSetConfig | FlowActionLogConfig,
118
- Field(discriminator="type")
119
- ]
132
+ FlowActionConfig = (
133
+ FlowActionMqttPublishConfig
134
+ | FlowActionContextSetConfig
135
+ | FlowActionLogConfig
136
+ )
120
137
 
121
138
  @dataclass
122
139
  class FlowConfig:
@@ -1,32 +1,38 @@
1
+ from typing import Any
2
+
1
3
  import jsonargparse
2
4
 
5
+ from yaml import YAMLError
3
6
 
4
- def _custom_yaml_load(stream):
5
- if isinstance(stream, str):
6
- v = stream.strip()
7
+ default_yaml_loader = jsonargparse.get_loader("yaml")
8
+ def _custom_yaml_load(stream: str) -> Any:
7
9
 
8
- # jsonargparse tries to parse yaml 1.1 boolean like values
9
- # Without this, str:"{'PlaybackStatus': 'Off'}" would become dict:{'PlaybackStatus': False}
10
- if v in ['on', 'On', 'off', 'Off', 'TRUE', 'FALSE', 'True', 'False']:
11
- return stream
10
+ v = stream.strip()
12
11
 
13
- # Anoyingly, values starting with {{ and ending with }} are working with the default yaml_loader
14
- # from jsonargparse. Somehow its not when we use the custom yaml loader.
15
- # This fixes it
16
- first_line = v.splitlines()[0].strip()
17
- if "#" not in first_line and ("{{" in first_line or first_line.startswith("{%")):
18
- return stream
12
+ # jsonargparse tries to parse yaml 1.1 boolean like values
13
+ # Without this, str:"{'PlaybackStatus': 'Off'}" would become dict:{'PlaybackStatus': False}
14
+ if v in ['on', 'On', 'off', 'Off', 'TRUE', 'FALSE', 'True', 'False']:
15
+ return stream
19
16
 
20
17
  # Delegate to default yaml loader from jsonargparse
21
- yaml_loader = jsonargparse.get_loader("yaml")
22
- return yaml_loader(stream)
18
+ return default_yaml_loader(stream)
23
19
 
24
20
  def new_argument_parser() -> jsonargparse.ArgumentParser:
25
21
 
26
22
  # register out custom yaml loader for jsonargparse
27
- jsonargparse.set_loader("yaml_custom", _custom_yaml_load)
23
+ jsonargparse.set_loader(
24
+ mode="yaml_custom",
25
+ loader_fn=_custom_yaml_load,
26
+ exceptions=(YAMLError,),
27
+ json_superset=True
28
+ )
28
29
 
29
30
  # unless specified otherwise, load config from config.yaml
30
- parser = jsonargparse.ArgumentParser(default_config_files=["config.yaml"], default_env=True, env_prefix=False, parser_mode="yaml_custom")
31
+ parser = jsonargparse.ArgumentParser(
32
+ default_config_files=["config.yaml"],
33
+ default_env=True,
34
+ env_prefix=False,
35
+ parser_mode="yaml_custom"
36
+ )
31
37
 
32
38
  return parser
@@ -7,11 +7,12 @@ from typing import Any
7
7
 
8
8
  import dbus_fast.aio as dbus_aio
9
9
  import dbus_fast.constants as dbus_constants
10
- import dbus_fast.errors as dbus_errors
11
10
  import dbus_fast.introspection as dbus_introspection
12
11
  import dbus_fast.message as dbus_message
13
12
  import janus
14
13
 
14
+ from dbus_fast import SignatureTree
15
+
15
16
  from dbus2mqtt import AppContext
16
17
  from dbus2mqtt.config import SubscriptionConfig
17
18
  from dbus2mqtt.dbus.dbus_types import (
@@ -21,6 +22,7 @@ from dbus2mqtt.dbus.dbus_types import (
21
22
  )
22
23
  from dbus2mqtt.dbus.dbus_util import (
23
24
  camel_to_snake,
25
+ convert_mqtt_args_to_dbus,
24
26
  unwrap_dbus_object,
25
27
  unwrap_dbus_objects,
26
28
  )
@@ -28,7 +30,7 @@ from dbus2mqtt.dbus.introspection_patches.mpris_playerctl import (
28
30
  mpris_introspection_playerctl,
29
31
  )
30
32
  from dbus2mqtt.dbus.introspection_patches.mpris_vlc import mpris_introspection_vlc
31
- from dbus2mqtt.event_broker import MqttMessage
33
+ from dbus2mqtt.event_broker import MqttMessage, MqttReceiveHints
32
34
  from dbus2mqtt.flow.flow_processor import FlowScheduler, FlowTriggerMessage
33
35
 
34
36
  logger = logging.getLogger(__name__)
@@ -636,11 +638,19 @@ class DbusClient:
636
638
 
637
639
  async def call_dbus_interface_method(self, interface: dbus_aio.proxy_object.ProxyInterface, method: str, method_args: list[Any]):
638
640
 
641
+ converted_args = convert_mqtt_args_to_dbus(method_args)
639
642
  call_method_name = "call_" + camel_to_snake(method)
643
+
644
+ # In case of a payload that doesn't match the dbus signature type, this prints a better error message
645
+ interface_method = next((m for m in interface.introspection.methods if m.name == method), None)
646
+ if interface_method:
647
+ in_signature_tree = SignatureTree(interface_method.in_signature)
648
+ in_signature_tree.verify(converted_args)
649
+
640
650
  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}")
651
+ res = await interface.__getattribute__(call_method_name)(*converted_args)
652
+ except Exception as e:
653
+ logger.debug(f"Error while calling dbus object, bus_name={interface.bus_name}, interface={interface.introspection.name}, method={method}, converted_args={converted_args}", exc_info=True)
644
654
  raise e
645
655
 
646
656
  if res:
@@ -672,9 +682,9 @@ class DbusClient:
672
682
  async def mqtt_receive_queue_processor_task(self):
673
683
  """Continuously processes messages from the async queue."""
674
684
  while True:
675
- msg = await self.event_broker.mqtt_receive_queue.async_q.get() # Wait for a message
685
+ msg, hints = await self.event_broker.mqtt_receive_queue.async_q.get() # Wait for a message
676
686
  try:
677
- await self._on_mqtt_msg(msg)
687
+ await self._on_mqtt_msg(msg, hints)
678
688
  except Exception as e:
679
689
  logger.warning(f"mqtt_receive_queue_processor_task: Exception {e}", exc_info=True)
680
690
  finally:
@@ -745,7 +755,7 @@ class DbusClient:
745
755
  path = message.body[0]
746
756
  await self._handle_interfaces_removed(bus_name, path)
747
757
 
748
- async def _on_mqtt_msg(self, msg: MqttMessage):
758
+ async def _on_mqtt_msg(self, msg: MqttMessage, hints: MqttReceiveHints):
749
759
  """Executes dbus method calls or property updates on objects when messages have
750
760
  1. a matching subscription configured
751
761
  2. a matching method
@@ -756,7 +766,6 @@ class DbusClient:
756
766
  found_matching_topic = False
757
767
  for subscription_configs in self.config.subscriptions:
758
768
  for interface_config in subscription_configs.interfaces:
759
- # TODO, performance improvement
760
769
  mqtt_topic = interface_config.render_mqtt_command_topic(self.templating, {})
761
770
  found_matching_topic |= mqtt_topic == msg.topic
762
771
 
@@ -777,7 +786,7 @@ class DbusClient:
777
786
  payload_value = msg.payload.get("value")
778
787
 
779
788
  if payload_method is None and (payload_property is None or payload_value is None):
780
- if msg.payload:
789
+ if msg.payload and hints.log_unmatched_message:
781
790
  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}")
782
791
  return
783
792
 
@@ -789,17 +798,32 @@ class DbusClient:
789
798
  for interface_config in subscription_configs.interfaces:
790
799
 
791
800
  for method in interface_config.methods:
792
-
793
801
  # filter configured method, configured topic, ...
794
802
  if method.method == payload_method:
795
803
  interface = proxy_object.get_interface(name=interface_config.interface)
796
804
  matched_method = True
797
805
 
806
+ result = None
807
+ error = None
798
808
  try:
799
809
  logger.info(f"on_mqtt_msg: method={method.method}, args={payload_method_args}, bus_name={bus_name}, path={path}, interface={interface_config.interface}")
800
- await self.call_dbus_interface_method(interface, method.method, payload_method_args)
810
+ result = await self.call_dbus_interface_method(interface, method.method, payload_method_args)
811
+
812
+ # Send response if configured
813
+ await self._send_mqtt_response(
814
+ interface_config, result, None, bus_name, path,
815
+ method=method.method, args=payload_method_args
816
+ )
817
+
801
818
  except Exception as e:
802
- logger.warning(f"on_mqtt_msg: method={method.method}, args={payload_method_args}, bus_name={bus_name} failed, exception={e}")
819
+ error = e
820
+ logger.warning(f"on_mqtt_msg: Failed calling method={method.method}, args={payload_method_args}, bus_name={bus_name}, exception={e}")
821
+
822
+ # Send error response if configured
823
+ await self._send_mqtt_response(
824
+ interface_config, None, error, bus_name, path,
825
+ method=method.method, args=payload_method_args
826
+ )
803
827
 
804
828
  for property in interface_config.properties:
805
829
  # filter configured property, configured topic, ...
@@ -810,11 +834,97 @@ class DbusClient:
810
834
  try:
811
835
  logger.info(f"on_mqtt_msg: property={property.property}, value={payload_value}, bus_name={bus_name}, path={path}, interface={interface_config.interface}")
812
836
  await self.set_dbus_interface_property(interface, property.property, payload_value)
837
+
838
+ # Send property set response if configured
839
+ await self._send_mqtt_response(
840
+ interface_config, payload_value, None, bus_name, path,
841
+ property=property.property, value=[payload_value]
842
+ )
843
+
813
844
  except Exception as e:
814
845
  logger.warning(f"on_mqtt_msg: property={property.property}, value={payload_value}, bus_name={bus_name} failed, exception={e}")
815
846
 
816
- if not matched_method and not matched_property:
847
+ # Send property set error response if configured
848
+ await self._send_mqtt_response(
849
+ interface_config, None, e, bus_name, path,
850
+ property=property.property, value=[payload_value],
851
+ )
852
+
853
+ if not matched_method and not matched_property and hints.log_unmatched_message:
817
854
  if payload_method:
818
- logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, method={payload_method}, bus_name={payload_bus_name}, path={payload_path or '*'}, active bus_names={list(self.subscriptions.keys())}")
855
+ logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, method={payload_method}, bus_name={payload_bus_name}, path={payload_path}, active bus_names={list(self.subscriptions.keys())}")
819
856
  if payload_property:
820
- logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, property={payload_property}, bus_name={payload_bus_name}, path={payload_path or '*'}, active bus_names={list(self.subscriptions.keys())}")
857
+ logger.info(f"No configured or active dbus subscriptions for topic={msg.topic}, property={payload_property}, bus_name={payload_bus_name}, path={payload_path}, active bus_names={list(self.subscriptions.keys())}")
858
+
859
+ async def _send_mqtt_response(self, interface_config, result: Any, error: Exception | None, bus_name: str, path: str, *args, **kwargs):
860
+ """Send MQTT response for a method call if response topic is configured
861
+
862
+ Args:
863
+ method (str, optional): The method to execute
864
+ args (list, optional): Arguments for the method
865
+ property (str, optional): The property to set
866
+ value (any, optional): The value to set for the property
867
+ """
868
+
869
+ if not interface_config.mqtt_response_topic:
870
+ return
871
+
872
+ try:
873
+ # Build response context
874
+ response_context = {
875
+ "bus_name": bus_name,
876
+ "path": path,
877
+ "interface": interface_config.interface,
878
+ "timestamp": datetime.now().isoformat()
879
+ }
880
+
881
+ # Check if 'method' and 'args' are provided
882
+ if 'method' in kwargs and 'args' in kwargs:
883
+ method = kwargs['method']
884
+ args = kwargs['args']
885
+ response_context.update({
886
+ "method": method,
887
+ "args": args,
888
+ })
889
+ # Check if 'property' and 'value' are provided
890
+ elif 'property' in kwargs and 'value' in kwargs:
891
+ property = kwargs['property']
892
+ value = kwargs['value']
893
+ response_context.update({
894
+ "property": property,
895
+ "value": value,
896
+ })
897
+ else:
898
+ return "Invalid arguments: Please provide either 'method' and 'args' or 'property' and 'value'"
899
+
900
+ # Add result or error to context
901
+ if error:
902
+ response_context.update({
903
+ "success": False,
904
+ "error": str(error),
905
+ "error_type": error.__class__.__name__
906
+ })
907
+ else:
908
+ response_context.update({
909
+ "success": True,
910
+ "result": result
911
+ })
912
+
913
+ # Render response topic
914
+ response_topic = interface_config.render_mqtt_response_topic(
915
+ self.templating, response_context
916
+ )
917
+
918
+ if response_topic:
919
+ # Send response via MQTT
920
+ response_msg = MqttMessage(
921
+ topic=response_topic,
922
+ payload=response_context,
923
+ payload_serialization_type="json"
924
+ )
925
+ await self.event_broker.publish_to_mqtt(response_msg)
926
+
927
+ logger.debug(f"Sent MQTT response: topic={response_topic}, success={response_context['success']}")
928
+
929
+ except Exception as e:
930
+ logger.warning(f"Failed to send MQTT response: {e}")
@@ -1,8 +1,14 @@
1
1
  import base64
2
+ import logging
2
3
  import re
3
4
 
5
+ from typing import Any
6
+
4
7
  import dbus_fast.signature as dbus_signature
5
8
 
9
+ from dbus_fast import Variant
10
+
11
+ logger = logging.getLogger(__name__)
6
12
 
7
13
  def unwrap_dbus_object(obj):
8
14
  if isinstance(obj, dict):
@@ -22,3 +28,155 @@ def unwrap_dbus_objects(args):
22
28
 
23
29
  def camel_to_snake(name):
24
30
  return re.sub(r'([a-z])([A-Z])', r'\1_\2', name).lower()
31
+
32
+ def _convert_value_to_dbus(value: Any) -> Any:
33
+ """
34
+ Recursively convert a single value to D-Bus compatible type.
35
+
36
+ Args:
37
+ value: The value to convert (can be dict, list, primitive, etc.)
38
+
39
+ Returns:
40
+ D-Bus compatible value
41
+ """
42
+ if value is None:
43
+ return value
44
+
45
+ elif isinstance(value, dict):
46
+ # Convert dict to D-Bus dictionary (a{sv} - dictionary of string to variant)
47
+ dbus_dict = {}
48
+ for k, v in value.items():
49
+ # Keys are typically strings in D-Bus dictionaries
50
+ key = str(k)
51
+ # Recursively convert the value first, then wrap in Variant
52
+ converted_value = _convert_value_to_dbus(v)
53
+ # Determine the appropriate D-Bus signature for the converted value
54
+ signature = _get_dbus_signature(converted_value)
55
+ dbus_dict[key] = Variant(signature, converted_value)
56
+ return dbus_dict
57
+
58
+ elif isinstance(value, list):
59
+ # Convert list to D-Bus array
60
+ converted_list = []
61
+ for item in value:
62
+ converted_list.append(_convert_value_to_dbus(item))
63
+ return converted_list
64
+
65
+ elif isinstance(value, bool):
66
+ # Boolean values are fine as-is for D-Bus
67
+ return value
68
+
69
+ elif isinstance(value, int):
70
+ # Integer values are fine as-is for D-Bus
71
+ return value
72
+
73
+ elif isinstance(value, float):
74
+ # Float values are fine as-is for D-Bus
75
+ return value
76
+
77
+ elif isinstance(value, str):
78
+ # String values are fine as-is for D-Bus
79
+ return value
80
+
81
+ else:
82
+ # For any other type, try to convert to string as fallback
83
+ logger.warning(f"Unknown type {type(value)} for D-Bus conversion, converting to string: {value}")
84
+ return str(value)
85
+
86
+ def _get_dbus_signature(value: Any) -> str:
87
+ """
88
+ Get the appropriate D-Bus signature for a value.
89
+
90
+ Args:
91
+ value: The value to get signature for
92
+
93
+ Returns:
94
+ D-Bus type signature string
95
+ """
96
+ if isinstance(value, bool):
97
+ return 'b' # boolean
98
+ elif isinstance(value, int):
99
+ uint16_min = 0
100
+ uint16_max = 0xFFFF
101
+ int16_min = -0x7FFF - 1
102
+ int16_max = 0x7FFF
103
+ uint32_min = 0
104
+ uint32_max = 0xFFFFFFFF
105
+ int32_min = -0x7FFFFFFF - 1
106
+ int32_max = 0x7FFFFFFF
107
+ uint64_min = 0
108
+ uint64_max = 18446744073709551615
109
+
110
+ if uint16_min <= value <= uint16_max:
111
+ return 'q' # 16-bit unsigned int
112
+ elif int16_min <= value <= int16_max:
113
+ return 'n' # 16-bit signed int
114
+ elif uint32_min <= value <= uint32_max:
115
+ return 'u' # 32-bit unsigned int
116
+ elif int32_min <= value <= int32_max:
117
+ return 'i' # 32-bit signed integer
118
+ elif uint64_min <= value <= uint64_max:
119
+ return 't' # 64-bit unsigned integer
120
+ else:
121
+ return 'x' # 64-bit signed integer
122
+ elif isinstance(value, float):
123
+ return 'd' # double
124
+ elif isinstance(value, str):
125
+ return 's' # string
126
+ elif isinstance(value, list):
127
+ if not value:
128
+ return 'as' # assume array of strings for empty arrays
129
+ # Get signature of first element and assume homogeneous array
130
+ element_sig = _get_dbus_signature(value[0])
131
+ return f'a{element_sig}' # array of elements
132
+ elif isinstance(value, dict):
133
+ return 'a{sv}' # dictionary of string to variant
134
+ else:
135
+ return 's' # fallback to string
136
+
137
+ # Wraps all complex types in VariantList
138
+ def convert_mqtt_args_to_dbus(args: list[Any]) -> list[Any]:
139
+ """
140
+ Convert MQTT/JSON arguments to D-Bus with explicit Variant wrapping for all complex types.
141
+
142
+ Args:
143
+ args: List of arguments from MQTT
144
+
145
+ Returns:
146
+ List of D-Bus compatible arguments with explicit Variants
147
+ """
148
+ converted_args = []
149
+
150
+ for arg in args:
151
+ converted_arg = _convert_and_wrap_in_variant(arg)
152
+ converted_args.append(converted_arg)
153
+
154
+ return converted_args
155
+
156
+ def _convert_and_wrap_in_variant(value: Any) -> Any:
157
+ """
158
+ Convert a value and wrap complex types in Variants.
159
+ """
160
+ if value is None:
161
+ return value
162
+ elif isinstance(value, bool | int | float | str):
163
+ # Primitive types can be used as-is or wrapped in Variant if needed
164
+ return value
165
+ elif isinstance(value, dict):
166
+ # Convert dict and wrap in Variant
167
+ converted_dict = {}
168
+ for k, v in value.items():
169
+ key = str(k)
170
+ converted_value = _convert_value_to_dbus(v)
171
+ signature = _get_dbus_signature(converted_value)
172
+ converted_dict[key] = Variant(signature, converted_value)
173
+ return converted_dict
174
+ elif isinstance(value, list):
175
+ # Convert list and potentially wrap in Variant
176
+ converted_list = []
177
+ for item in value:
178
+ converted_list.append(_convert_and_wrap_in_variant(item))
179
+ return converted_list
180
+ else:
181
+ # Fallback
182
+ return value
dbus2mqtt/event_broker.py CHANGED
@@ -18,6 +18,10 @@ class MqttMessage:
18
18
  payload: Any
19
19
  payload_serialization_type: str = "json"
20
20
 
21
+ @dataclass
22
+ class MqttReceiveHints:
23
+ log_unmatched_message: bool = True
24
+
21
25
  @dataclass
22
26
  class FlowTriggerMessage:
23
27
  flow_config: FlowConfig
@@ -27,7 +31,7 @@ class FlowTriggerMessage:
27
31
 
28
32
  class EventBroker:
29
33
  def __init__(self):
30
- self.mqtt_receive_queue = janus.Queue[MqttMessage]()
34
+ self.mqtt_receive_queue = janus.Queue[tuple[MqttMessage, MqttReceiveHints]]()
31
35
  self.mqtt_publish_queue = janus.Queue[MqttMessage]()
32
36
  self.flow_trigger_queue = janus.Queue[FlowTriggerMessage]()
33
37
  # self.dbus_send_queue: janus.Queue
@@ -40,9 +44,9 @@ class EventBroker:
40
44
  return_exceptions=True
41
45
  )
42
46
 
43
- def on_mqtt_receive(self, msg: MqttMessage):
47
+ def on_mqtt_receive(self, msg: MqttMessage, hints: MqttReceiveHints):
44
48
  # logger.debug("on_mqtt_receive")
45
- self.mqtt_receive_queue.sync_q.put(msg)
49
+ self.mqtt_receive_queue.sync_q.put((msg, hints))
46
50
 
47
51
  async def publish_to_mqtt(self, msg: MqttMessage):
48
52
  # logger.debug("publish_to_mqtt")
@@ -8,6 +8,9 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
8
8
 
9
9
  from dbus2mqtt import AppContext
10
10
  from dbus2mqtt.config import (
11
+ FlowActionContextSetConfig,
12
+ FlowActionLogConfig,
13
+ FlowActionMqttPublishConfig,
11
14
  FlowConfig,
12
15
  FlowTriggerConfig,
13
16
  FlowTriggerDbusSignalConfig,
@@ -101,11 +104,11 @@ class FlowActionContext:
101
104
  res = []
102
105
  for action_config in self.flow_config.actions:
103
106
  action = None
104
- if action_config.type == "context_set":
107
+ if action_config.type == FlowActionContextSetConfig.type:
105
108
  action = ContextSetAction(action_config, self.app_context)
106
- elif action_config.type == "mqtt_publish":
109
+ elif action_config.type == FlowActionMqttPublishConfig.type:
107
110
  action = MqttPublishAction(action_config, self.app_context)
108
- elif action_config.type == "log":
111
+ elif action_config.type == FlowActionLogConfig.type:
109
112
  action = LogAction(action_config, self.app_context)
110
113
 
111
114
  if action:
@@ -5,6 +5,7 @@ import logging
5
5
  import random
6
6
  import string
7
7
 
8
+ from datetime import datetime
8
9
  from typing import Any
9
10
  from urllib.parse import ParseResult
10
11
  from urllib.request import urlopen
@@ -18,13 +19,15 @@ from paho.mqtt.properties import Properties
18
19
  from paho.mqtt.subscribeoptions import SubscribeOptions
19
20
 
20
21
  from dbus2mqtt import AppContext
21
- from dbus2mqtt.event_broker import MqttMessage
22
+ from dbus2mqtt.config import FlowConfig, FlowTriggerMqttMessageConfig
23
+ from dbus2mqtt.event_broker import FlowTriggerMessage, MqttMessage, MqttReceiveHints
22
24
 
23
25
  logger = logging.getLogger(__name__)
24
26
 
25
27
  class MqttClient:
26
28
 
27
29
  def __init__(self, app_context: AppContext, loop):
30
+ self.app_context = app_context
28
31
  self.config = app_context.config.mqtt
29
32
  self.event_broker = app_context.event_broker
30
33
 
@@ -140,6 +143,51 @@ class MqttClient:
140
143
  try:
141
144
  json_payload = json.loads(payload) if payload else {}
142
145
  logger.debug(f"on_message: msg.topic={msg.topic}, msg.payload={json.dumps(json_payload)}")
143
- self.event_broker.on_mqtt_receive(MqttMessage(msg.topic, json_payload))
146
+
147
+ # publish to flow trigger queue for any configured mqtt_message triggers
148
+ flow_trigger_messages = self._trigger_flows(msg.topic, {
149
+ "topic": msg.topic,
150
+ "payload": json_payload
151
+ })
152
+
153
+ # publish on a queue that is being processed by dbus_client
154
+ self.event_broker.on_mqtt_receive(
155
+ MqttMessage(msg.topic, json_payload),
156
+ MqttReceiveHints(
157
+ log_unmatched_message=len(flow_trigger_messages) == 0
158
+ )
159
+ )
160
+
144
161
  except json.JSONDecodeError as e:
145
162
  logger.warning(f"on_message: Unexpected payload, expecting json, topic={msg.topic}, payload={payload}, error={e}")
163
+
164
+ def _trigger_flows(self, topic: str, trigger_context: dict) -> list[FlowTriggerMessage]:
165
+ """Triggers all flows that have a mqtt_trigger defined that matches the given topic
166
+ and configured filters."""
167
+
168
+ flow_trigger_messages = []
169
+
170
+ all_flows: list[FlowConfig] = []
171
+ all_flows.extend(self.app_context.config.flows)
172
+ for subscription in self.app_context.config.dbus.subscriptions:
173
+ all_flows.extend(subscription.flows)
174
+
175
+ for flow in all_flows:
176
+ for trigger in flow.triggers:
177
+ if trigger.type == FlowTriggerMqttMessageConfig.type:
178
+ matches_filter = trigger.topic == topic
179
+ if matches_filter and trigger.filter is not None:
180
+ matches_filter = trigger.matches_filter(self.app_context.templating, trigger_context)
181
+
182
+ if matches_filter:
183
+ trigger_message = FlowTriggerMessage(
184
+ flow,
185
+ trigger,
186
+ datetime.now(),
187
+ trigger_context=trigger_context,
188
+ )
189
+
190
+ flow_trigger_messages.append(trigger_message)
191
+ self.event_broker.flow_trigger_queue.sync_q.put(trigger_message)
192
+
193
+ return flow_trigger_messages
@@ -29,7 +29,7 @@ class DbusContext:
29
29
  async def async_dbus_call_fn(self, bus_name: str, path: str, interface: str, method:str, method_args: list[Any] = []):
30
30
 
31
31
  if not isinstance(method_args, list):
32
- # Pylance will mentiod this line is unreachable. It is not as jinja2 can pass in any type
32
+ # Pylance will mention this line is unreachable. It is not, jinja2 can pass in any type
33
33
  raise ValueError("method_args must be a list")
34
34
 
35
35
  proxy_object = self.dbus_client.get_subscribed_proxy_object(bus_name, path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbus2mqtt
3
- Version: 0.4.4
3
+ Version: 0.5.1
4
4
  Summary: General purpose DBus to MQTT bridge - expose signals, methods and properties over MQTT - featuring jinja based templating, payload enrichment and MPRIS / BlueZ / Home Assistant ready examples
5
5
  Project-URL: Documentation, https://jwnmulder.github.io/dbus2mqtt
6
6
  Project-URL: Source, https://github.com/jwnmulder/dbus2mqtt
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
20
21
  Classifier: Topic :: Home Automation
21
22
  Requires-Python: >=3.10
22
23
  Requires-Dist: apscheduler>=3.11.0
@@ -27,7 +28,6 @@ Requires-Dist: jinja2-ansible-filters>=1.3.2
27
28
  Requires-Dist: jinja2>=3.1.6
28
29
  Requires-Dist: jsonargparse>=4.38.0
29
30
  Requires-Dist: paho-mqtt>=2.1.0
30
- Requires-Dist: pydantic>=2.11.3
31
31
  Requires-Dist: python-dotenv>=1.1.0
32
32
  Requires-Dist: pyyaml>=6.0.2
33
33
  Description-Content-Type: text/markdown
@@ -51,7 +51,7 @@ This makes it easy to integrate Linux desktop services or system signals into MQ
51
51
 
52
52
  **dbus2mqtt** is considered stable for the use-cases it has been tested against, and is actively being developed. Documentation is continuously being improved.
53
53
 
54
- Initial testing has focused on MPRIS integration. A table of tested MPRIS players and their supported methods can be found here: [Mediaplayer integration with Home Assistant](https://jwnmulder.github.io/dbus2mqtt/examples/home_assistant_media_player/)
54
+ Initial testing has focused on MPRIS integration. A table of tested MPRIS players and their supported methods can be found on [Mediaplayer integration with Home Assistant](https://jwnmulder.github.io/dbus2mqtt/examples/home_assistant_media_player.html)
55
55
 
56
56
  ## Getting started with dbus2mqtt
57
57
 
@@ -94,7 +94,6 @@ MQTT__USERNAME=
94
94
  MQTT__PASSWORD=
95
95
  ```
96
96
 
97
-
98
97
  ### Install and run dbus2mqtt
99
98
 
100
99
  ```bash
@@ -102,136 +101,25 @@ python -m pip install dbus2mqtt
102
101
  dbus2mqtt --config config.yaml
103
102
  ```
104
103
 
105
-
106
- ### Run using docker with auto start behavior
107
-
108
- 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.
109
-
110
- ```bash
111
- # setup configuration
112
- mkdir -p $HOME/.config/dbus2mqtt
113
- cp docs/examples/home_assistant_media_player.yaml $HOME/.config/dbus2mqtt/config.yaml
114
- cp .env.example $HOME/.config/dbus2mqtt/.env
115
-
116
- # run image and automatically start on reboot
117
- sudo docker pull jwnmulder/dbus2mqtt
118
- sudo docker run --detach --name dbus2mqtt \
119
- --volume "$HOME"/.config/dbus2mqtt:"$HOME"/.config/dbus2mqtt \
120
- --volume /run/user:/run/user \
121
- --env DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \
122
- --env-file "$HOME"/.config/dbus2mqtt/.env \
123
- --user $(id -u):$(id -g) \
124
- --privileged \
125
- --restart unless-stopped \
126
- jwnmulder/dbus2mqtt \
127
- --config "$HOME"/.config/dbus2mqtt/config.yaml
128
-
129
- # view logs
130
- sudo docker logs dbus2mqtt -f
131
- ```
104
+ See [setup](https://jwnmulder.github.io/dbus2mqtt/setup.html) for more installation options and configuration details.
132
105
 
133
106
  ## Examples
134
107
 
135
- More dbus2mqtt examples can be found here: [examples](https://jwnmulder.github.io/dbus2mqtt/examples/).
136
- The most complete one being [MPRIS to Home Assistant Media Player integration](https://jwnmulder.github.io/dbus2mqtt/examples/home_assistant_media_player/)
137
-
138
- ## Configuration reference
139
-
140
- dbus2mqtt leverages [jsonargparse](https://jsonargparse.readthedocs.io/en/stable/) which allows configuration via either yaml configuration, CLI or environment variables. Until this is fully documented have a look at the examples in this repository.
141
-
142
- ### MQTT and D-Bus connection details
143
-
144
- ```bash
145
- # dbus_fast configuration
146
- export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
147
-
148
- # dbus2mqtt configuration
149
- MQTT__HOST=localhost
150
- MQTT__PORT=1883
151
- MQTT__USERNAME=
152
- MQTT__PASSWORD=
153
- ```
154
-
155
- or
156
-
157
- ```yaml
158
- mqtt:
159
- host: localhost
160
- port: 1883
161
- subscription_topics:
162
- - dbus2mqtt/#
163
- ```
164
-
165
- ### Exposing dbus methods
166
-
167
- ```yaml
168
- dbus:
169
- subscriptions:
170
- - bus_name: org.mpris.MediaPlayer2.*
171
- path: /org/mpris/MediaPlayer2
172
- interfaces:
173
- - interface: org.mpris.MediaPlayer2.Player
174
- mqtt_command_topic: dbus2mqtt/org.mpris.MediaPlayer2/command
175
- methods:
176
- - method: Pause
177
- - method: Play
178
- ```
179
-
180
- This configuration will expose 2 methods. Triggering methods can be done by publishing json messages to the `dbus2mqtt/org.mpris.MediaPlayer2/command` MQTT topic. Arguments can be passed along in `args`.
108
+ More dbus2mqtt examples can be found in the [examples](https://jwnmulder.github.io/dbus2mqtt/examples/index.html) section.
109
+ The most complete one being [Mediaplayer integration with Home Assistant](https://jwnmulder.github.io/dbus2mqtt/examples/home_assistant_media_player.html)
181
110
 
182
- Some examples that call methods on **all** bus_names matching the configured pattern
111
+ ## Exposing dbus methods, properties and signals
183
112
 
184
- ```json
185
- {
186
- "method": "Play",
187
- }
188
- ```
113
+ See [subscriptions](https://jwnmulder.github.io/dbus2mqtt/subscriptions.html) for documentation on calling methods, setting properties and exposing D-Bus signals to MQTT. When configured, D-Bus methods can be invoked by publishing a message like
189
114
 
190
115
  ```json
191
116
  {
192
- "method": "OpenUri",
193
- "args": []
117
+ "method": "Play"
194
118
  }
195
119
  ```
196
120
 
197
- To specifically target objects the properties `bus_name` and/or `path` can be used. Both properties support wildcards
198
-
199
- ```json
200
- {
201
- "method": "Play",
202
- "bus_name": "*.firefox",
203
- "path": "/org/mpris/MediaPlayer2"
204
- }
205
- ```
206
-
207
- ### Exposing dbus signals
208
-
209
- Publishing signals to MQTT topics works by subscribing to the relevant signal and using flows for publishing
210
-
211
- ```yaml
212
- dbus:
213
- subscriptions:
214
- - bus_name: org.mpris.MediaPlayer2.*
215
- path: /org/mpris/MediaPlayer2
216
- interfaces:
217
- - interface: org.freedesktop.DBus.Properties
218
- signals:
219
- - signal: PropertiesChanged
220
-
221
- flows:
222
- - name: "Property Changed flow"
223
- triggers:
224
- - type: on_signal
225
- actions:
226
- - type: mqtt_publish
227
- topic: dbus2mqtt/org.mpris.MediaPlayer2/signals/PropertiesChanged
228
- payload_type: json
229
- ```
230
-
231
- ## Flows
232
-
233
- A reference of all supported flow triggers and actions can be found on [Flows](https://jwnmulder.github.io/dbus2mqtt/flows/)
121
+ ## Flows and Jinja based templating
234
122
 
235
- ## Jinja templating
123
+ For more advanced use-cases, dbus2mqtt has support for flows and Jinja2 based templates. A reference of all supported flow triggers and actions can be found on [flows](https://jwnmulder.github.io/dbus2mqtt/flows/index.html)
236
124
 
237
- TODO: Document Jinja templating, for now see the [MPRIS to Home Assistant Media Player integration](https://jwnmulder.github.io/dbus2mqtt/examples/home_assistant_media_player/) example
125
+ Jinja templating documentation can be found here: [templating](https://jwnmulder.github.io/dbus2mqtt/templating/index.html)
@@ -1,24 +1,24 @@
1
1
  dbus2mqtt/__init__.py,sha256=VunubKEH4_lne9Ttd2YRpyXjZMIAzyD2eiJ2sfvvTFU,362
2
2
  dbus2mqtt/__main__.py,sha256=NAoa3nwgBSQI22eWzzRx61SIDThDwXmUofWWZU3_4-Q,71
3
- dbus2mqtt/event_broker.py,sha256=Hp5yurhP8FkAO-y0l2grygHxR2e37ZQ7QScjcQDA2UU,1334
3
+ dbus2mqtt/event_broker.py,sha256=8Iw1PNpH4IxQbpcFtNvPDc8_M8kuGarN6Kvz_a2aFfc,1468
4
4
  dbus2mqtt/main.py,sha256=Kr2LRVxWcPDtNwCj8Eqz9TxtGLAVrV4q0nizKh1pLXc,4539
5
- dbus2mqtt/config/__init__.py,sha256=947_dLVt8qq7P3p15jGIy5Vf1jUUMGrh1xr_DVhZclQ,5372
6
- dbus2mqtt/config/jsonarparse.py,sha256=XK7CrMCGiHZK2tq1IqSpzJ3hdV0gCOixJq3LXUDiEjg,1298
7
- dbus2mqtt/dbus/dbus_client.py,sha256=WHsmOiaJ5SY6zk-C99m83MEpycnUyGpsYmGopJJbPE8,39690
5
+ dbus2mqtt/config/__init__.py,sha256=daqVzuSxNnUNKtMoMYs73AdlkfS6goXUsxF1wnKi7y0,5894
6
+ dbus2mqtt/config/jsonarparse.py,sha256=-wcJW-O-Coqs0uqr5VVvk9mj6DWEm45NylSLkOhCECs,1084
7
+ dbus2mqtt/dbus/dbus_client.py,sha256=PDZiscCF4imn7cxo5bDLmhQ8g9ZL-6RbGu5had1xiso,44832
8
8
  dbus2mqtt/dbus/dbus_types.py,sha256=NmPD9um499e49Pk8DWH4IrIPQh1BinHYQgoXllCNiDw,777
9
- dbus2mqtt/dbus/dbus_util.py,sha256=h-1Y8Mvz9bj9X7mPZ8LghkvXDrujdJHK0__AOW373hE,697
9
+ dbus2mqtt/dbus/dbus_util.py,sha256=NUe_9Aohcib_bU8RUa19UPFteDZ0_WsmgCbbnmTUvcY,5814
10
10
  dbus2mqtt/dbus/introspection_patches/mpris_playerctl.py,sha256=q93d_Yp93u3Y-9q0dRdKW5hrij9GK3CFqKhUWVE8uw4,5883
11
11
  dbus2mqtt/dbus/introspection_patches/mpris_vlc.py,sha256=Cf-o-05W6gUoKpcYR7n0dRi-CrbeASPTwkyEzZGnU3Y,4241
12
12
  dbus2mqtt/flow/__init__.py,sha256=tAL-CjXQHq_tGTKctIdOZ5teVKBtcJs6Astq_RdruV8,1540
13
- dbus2mqtt/flow/flow_processor.py,sha256=N-btGap1wqnM4zKyulH_5KQhGi0IRlsK2cHrrnomMQQ,8922
13
+ dbus2mqtt/flow/flow_processor.py,sha256=UUdnIeVKXFRTJDBHLein6riHAh7UOKPdT4mTsnAmVV8,9067
14
14
  dbus2mqtt/flow/actions/context_set.py,sha256=dIT39MJJVb0wuRI_ZM3ssnXYfa-iyB4o_UZD-1BZL2g,1087
15
15
  dbus2mqtt/flow/actions/log_action.py,sha256=2_-YEKkX5kvFzK6x4v-Hx3u2PEM8fip_4buMg_ij-oI,1156
16
16
  dbus2mqtt/flow/actions/mqtt_publish.py,sha256=psNkTvaR3JZwAwpM4AqiZTDnA5UQX9r4CUZ1LA7iRW4,2366
17
- dbus2mqtt/mqtt/mqtt_client.py,sha256=bVAPbjgIi8ZQZlYVLe-Mo_A6NW6oQYEw1EYMOJYoS7w,6057
18
- dbus2mqtt/template/dbus_template_functions.py,sha256=UEoXK2PqDKF6jR4vTFHQwq58f5APnOJr7B1_I1zW8yM,2449
17
+ dbus2mqtt/mqtt/mqtt_client.py,sha256=qvhaxtftdE7uS5Vhw-Dvhaicro4pWGtGlk254m2nEtI,8076
18
+ dbus2mqtt/template/dbus_template_functions.py,sha256=oYXJ4HC1XCFGarf_tRzNGhvx2ECXDoT9J4Mz-cxqoJg,2447
19
19
  dbus2mqtt/template/templating.py,sha256=QLar09NinZO8rYGbVs9EThX-SOyTeBVCOppueU7VYdo,4483
20
- dbus2mqtt-0.4.4.dist-info/METADATA,sha256=aEi1sQg8JVDc6A1_uavQC6lsQljSnIoQuxUUgwsj8bI,8069
21
- dbus2mqtt-0.4.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- dbus2mqtt-0.4.4.dist-info/entry_points.txt,sha256=pmmacoHCsvTTUB5dIPaY4i0H9gX7BQlpd3rBU-Jbv3A,50
23
- dbus2mqtt-0.4.4.dist-info/licenses/LICENSE,sha256=a4bIEgyA9rrnAfUN90CgbgZ6BQIFHeABkk0JihiBaxM,1074
24
- dbus2mqtt-0.4.4.dist-info/RECORD,,
20
+ dbus2mqtt-0.5.1.dist-info/METADATA,sha256=bvfnVoBAYjKCvTYwqyBpVlX98ObW9gjg1bWhYtfUBWg,5393
21
+ dbus2mqtt-0.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
+ dbus2mqtt-0.5.1.dist-info/entry_points.txt,sha256=pmmacoHCsvTTUB5dIPaY4i0H9gX7BQlpd3rBU-Jbv3A,50
23
+ dbus2mqtt-0.5.1.dist-info/licenses/LICENSE,sha256=a4bIEgyA9rrnAfUN90CgbgZ6BQIFHeABkk0JihiBaxM,1074
24
+ dbus2mqtt-0.5.1.dist-info/RECORD,,