modpoll 1.2.0__tar.gz → 1.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modpoll
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: A New Command-line Tool for Modbus and MQTT
5
5
  Home-page: https://github.com/gavinying/modpoll
6
6
  Author: Ying Shaodong
@@ -12,7 +12,6 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Provides-Extra: serial
15
- Requires-Dist: importlib-metadata (>=8.3.0,<9.0.0)
16
15
  Requires-Dist: paho-mqtt (>=2.1.0,<3.0.0)
17
16
  Requires-Dist: prettytable (>=3.9.0,<4.0.0)
18
17
  Requires-Dist: pymodbus (>=3.6.9,<3.7.0)
@@ -124,7 +123,7 @@ modpoll --once \
124
123
  The reason we can magically poll data from the online device *modsim* is because we have already provided the [Modbus configure file](https://raw.githubusercontent.com/gavinying/modpoll/master/examples/modsim.csv) for *modsim* device as following,
125
124
 
126
125
  ```CSV
127
- device,modsim001,1,,
126
+ device,modsim01,1,,
128
127
  poll,coil,0,16,BE_BE
129
128
  ref,coil01-08,0,bool8,rw
130
129
  ref,coil09-16,1,bool8,rw
@@ -222,11 +221,17 @@ modpoll \
222
221
  --config https://raw.githubusercontent.com/gavinying/modpoll/master/examples/modsim.csv
223
222
  ```
224
223
 
225
- With successful data polling and publishing, you can subscribe the topic `modpoll/modsim` on the same MQTT broker `mqtt.eclipseprojects.io` to view the collected data.
224
+ With successful data polling and publishing, you can subscribe the default data topic `modpoll/modsim01/data` on the same MQTT broker `mqtt.eclipseprojects.io` to view the collected data.
226
225
 
227
- The MQTT topic uses `<mqtt_topic_prefix>/<device_name>` pattern, <mqtt_topic_prefix> is provided by `--mqtt-topic-prefix` argument, the default value is `modpoll` and <device_name> is provided by the Modbus configure file.
226
+ The MQTT topics can be customized by providing the following arguments,
228
227
 
229
- > ⚠️ **Note:** The `--mqtt-topic-prefix` argument is deprecated and will be removed in the future release. Use `--mqtt-publish-topic-pattern` and `--mqtt-subscribe-topic-pattern` instead. If both are used, `--mqtt-topic-prefix` argument will take precedence in order to keep backward compatibility. See [document](https://gavinying.github.io/modpoll/usage.html#Named%20Arguments) for details.
228
+ - `mqtt-publish-topic-pattern`
229
+ - `mqtt-subscribe-topic-pattern`
230
+ - `mqtt-diagnostics-topic-pattern`
231
+
232
+ See [document](https://gavinying.github.io/modpoll/usage.html#Named%20Arguments) for details.
233
+
234
+ > ⚠️ **Note:** The old `--mqtt-topic-prefix` argument is now deprecated and will be removed in the future release. Suggest to use `--mqtt-xxx-topic-pattern` in new project. If both are used, `--mqtt-topic-prefix` argument will take precedence in order to keep backward compatibility until it is removed.
230
235
 
231
236
 
232
237
  <p align="center">
@@ -289,7 +294,7 @@ for example, if the child folder `examples` contains the config file `modsim.csv
289
294
  ```
290
295
 
291
296
 
292
- ## Basic Usage
297
+ ## Example Use Cases
293
298
 
294
299
  - Connect to Modbus TCP device
295
300
 
@@ -313,7 +318,6 @@ for example, if the child folder `examples` contains the config file `modsim.csv
313
318
  ```bash
314
319
  modpoll \
315
320
  --tcp modsim.topmaker.net \
316
- --tcp-port 5020 \
317
321
  --mqtt-host mqtt.eclipseprojects.io \
318
322
  --config examples/modsim.csv
319
323
  ```
@@ -323,11 +327,18 @@ for example, if the child folder `examples` contains the config file `modsim.csv
323
327
  ```bash
324
328
  modpoll \
325
329
  --tcp modsim.topmaker.net \
326
- --tcp-port 5020 \
327
330
  --export data.csv \
328
331
  --config examples/modsim.csv
329
332
  ```
330
333
 
334
+ - Connect to Modbus TCP devices using multiple config files
335
+
336
+ ```bash
337
+ modpoll \
338
+ --tcp modsim.topmaker.net \
339
+ --config examples/modsim.csv examples/modsim2.csv
340
+ ```
341
+
331
342
 
332
343
  > *Refer to the [documentation](https://gavinying.github.io/modpoll) site for more details about the configuration and examples.*
333
344
 
@@ -100,7 +100,7 @@ modpoll --once \
100
100
  The reason we can magically poll data from the online device *modsim* is because we have already provided the [Modbus configure file](https://raw.githubusercontent.com/gavinying/modpoll/master/examples/modsim.csv) for *modsim* device as following,
101
101
 
102
102
  ```CSV
103
- device,modsim001,1,,
103
+ device,modsim01,1,,
104
104
  poll,coil,0,16,BE_BE
105
105
  ref,coil01-08,0,bool8,rw
106
106
  ref,coil09-16,1,bool8,rw
@@ -198,11 +198,17 @@ modpoll \
198
198
  --config https://raw.githubusercontent.com/gavinying/modpoll/master/examples/modsim.csv
199
199
  ```
200
200
 
201
- With successful data polling and publishing, you can subscribe the topic `modpoll/modsim` on the same MQTT broker `mqtt.eclipseprojects.io` to view the collected data.
201
+ With successful data polling and publishing, you can subscribe the default data topic `modpoll/modsim01/data` on the same MQTT broker `mqtt.eclipseprojects.io` to view the collected data.
202
202
 
203
- The MQTT topic uses `<mqtt_topic_prefix>/<device_name>` pattern, <mqtt_topic_prefix> is provided by `--mqtt-topic-prefix` argument, the default value is `modpoll` and <device_name> is provided by the Modbus configure file.
203
+ The MQTT topics can be customized by providing the following arguments,
204
204
 
205
- > ⚠️ **Note:** The `--mqtt-topic-prefix` argument is deprecated and will be removed in the future release. Use `--mqtt-publish-topic-pattern` and `--mqtt-subscribe-topic-pattern` instead. If both are used, `--mqtt-topic-prefix` argument will take precedence in order to keep backward compatibility. See [document](https://gavinying.github.io/modpoll/usage.html#Named%20Arguments) for details.
205
+ - `mqtt-publish-topic-pattern`
206
+ - `mqtt-subscribe-topic-pattern`
207
+ - `mqtt-diagnostics-topic-pattern`
208
+
209
+ See [document](https://gavinying.github.io/modpoll/usage.html#Named%20Arguments) for details.
210
+
211
+ > ⚠️ **Note:** The old `--mqtt-topic-prefix` argument is now deprecated and will be removed in the future release. Suggest to use `--mqtt-xxx-topic-pattern` in new project. If both are used, `--mqtt-topic-prefix` argument will take precedence in order to keep backward compatibility until it is removed.
206
212
 
207
213
 
208
214
  <p align="center">
@@ -265,7 +271,7 @@ for example, if the child folder `examples` contains the config file `modsim.csv
265
271
  ```
266
272
 
267
273
 
268
- ## Basic Usage
274
+ ## Example Use Cases
269
275
 
270
276
  - Connect to Modbus TCP device
271
277
 
@@ -289,7 +295,6 @@ for example, if the child folder `examples` contains the config file `modsim.csv
289
295
  ```bash
290
296
  modpoll \
291
297
  --tcp modsim.topmaker.net \
292
- --tcp-port 5020 \
293
298
  --mqtt-host mqtt.eclipseprojects.io \
294
299
  --config examples/modsim.csv
295
300
  ```
@@ -299,11 +304,18 @@ for example, if the child folder `examples` contains the config file `modsim.csv
299
304
  ```bash
300
305
  modpoll \
301
306
  --tcp modsim.topmaker.net \
302
- --tcp-port 5020 \
303
307
  --export data.csv \
304
308
  --config examples/modsim.csv
305
309
  ```
306
310
 
311
+ - Connect to Modbus TCP devices using multiple config files
312
+
313
+ ```bash
314
+ modpoll \
315
+ --tcp modsim.topmaker.net \
316
+ --config examples/modsim.csv examples/modsim2.csv
317
+ ```
318
+
307
319
 
308
320
  > *Refer to the [documentation](https://gavinying.github.io/modpoll) site for more details about the configuration and examples.*
309
321
 
@@ -0,0 +1,3 @@
1
+ import importlib.metadata as importlib_metadata
2
+
3
+ __version__ = importlib_metadata.version(__name__)
@@ -5,19 +5,20 @@ from . import __version__
5
5
 
6
6
  def get_parser():
7
7
  parser = argparse.ArgumentParser(
8
- description=f"modpoll v{__version__} - A New Command-line Tool for Modbus and MQTT"
8
+ description=f"Modpoll v{__version__} - A New Command-line Tool for Modbus and MQTT"
9
9
  )
10
10
  parser.add_argument(
11
11
  "-v",
12
12
  "--version",
13
13
  action="version",
14
- version=f"modpoll v{__version__}",
14
+ version=f"v{__version__}",
15
15
  )
16
16
  parser.add_argument(
17
17
  "-f",
18
18
  "--config",
19
19
  required=True,
20
20
  help="A local path or URL of Modbus configuration file. Required!",
21
+ nargs="+",
21
22
  )
22
23
  parser.add_argument(
23
24
  "-d",
@@ -107,18 +108,18 @@ def get_parser():
107
108
  )
108
109
  parser.add_argument(
109
110
  "--mqtt-publish-topic-pattern",
110
- default="modpoll/<device_name>",
111
+ default="modpoll/<device_name>/data",
111
112
  help='Topic pattern for MQTT publish. Use <device_name> as placeholder for the device names in Modbus config. Defaults to "modpoll/<device_name>"',
112
113
  )
113
114
  parser.add_argument(
114
115
  "--mqtt-subscribe-topic-pattern",
115
- default="modpoll/<device_name>/set",
116
- help='Topic pattern for MQTT subscribe. Use <device_name> as placeholder for the device names in Modbus config. Defaults to "modpoll/<device_name>/set"',
116
+ default="modpoll/+/set",
117
+ help='Topic pattern for MQTT subscribe. Defaults to "modpoll/+/set"',
117
118
  )
118
119
  parser.add_argument(
119
120
  "--mqtt-diagnostics-topic-pattern",
120
- default=None,
121
- help="Topic pattern for MQTT diagnostics. Use <device_name> as placeholder for the device names in Modbus config. Defaults to None",
121
+ default="modpoll/<device_name>/diagnostics",
122
+ help="Topic pattern for MQTT diagnostics. Use <device_name> as placeholder for the device names in Modbus config. Defaults to modpoll/<device_name>/diagnostics",
122
123
  )
123
124
  parser.add_argument(
124
125
  "--mqtt-qos",
@@ -0,0 +1,199 @@
1
+ import json
2
+ import logging
3
+ import re
4
+ import signal
5
+ import sys
6
+
7
+ from .arg_parser import get_parser
8
+ from .mqtt_task import MqttHandler
9
+ from .modbus_task import setup_modbus_handlers
10
+
11
+ from . import __version__
12
+ from .utils import set_threading_event, delay_thread, on_threading_event, get_utc_time
13
+
14
+
15
+ LOG_SIMPLE = "%(asctime)s | %(levelname).1s | %(name)s | %(message)s"
16
+ logger = None
17
+
18
+
19
+ def _signal_handler(signal, frame):
20
+ logger.info(f"Exiting {sys.argv[0]}")
21
+ set_threading_event()
22
+
23
+
24
+ def setup_logging(level, format):
25
+ logging.basicConfig(level=level, format=format)
26
+
27
+
28
+ def app(name="modpoll"):
29
+ mqtt_handler = None
30
+ modbus_handlers = []
31
+
32
+ print(
33
+ f"\nModpoll v{__version__} - A New Command-line Tool for Modbus and MQTT\n",
34
+ flush=True,
35
+ )
36
+
37
+ signal.signal(signal.SIGINT, _signal_handler)
38
+
39
+ # parse args
40
+ args = get_parser().parse_args()
41
+
42
+ # get logger
43
+ setup_logging(args.loglevel, LOG_SIMPLE)
44
+ global logger
45
+ logger = logging.getLogger(__name__)
46
+
47
+ # Overwrite topic pattern if topic prefix is specified for backward compatibility
48
+ if args.mqtt_topic_prefix:
49
+ logger.warning(
50
+ "(DEPRECATED): The `--mqtt-topic-prefix` argument is deprecated and will be removed in the future release. Use `--mqtt-publish-topic-pattern` and `--mqtt-subscribe-topic-pattern` instead. If both are used, `--mqtt-topic-prefix` argument will take precedence in order to keep backward compatibility."
51
+ )
52
+ if args.mqtt_topic_prefix.endswith("/"):
53
+ args.mqtt_topic_prefix = args.mqtt_topic_prefix[:-1]
54
+ args.mqtt_publish_topic_pattern = f"{args.mqtt_topic_prefix}/<device_name>"
55
+ args.mqtt_subscribe_topic_pattern = f"{args.mqtt_topic_prefix}/#/set"
56
+
57
+ # setup mqtt
58
+ if not args.mqtt_host:
59
+ logger.info("No MQTT host specified, skip MQTT setup.")
60
+ else:
61
+ logger.info(f"Setup MQTT connection to {args.mqtt_host}:{args.mqtt_port}")
62
+ try:
63
+ mqtt_handler = MqttHandler(
64
+ "MqttHandler",
65
+ args.mqtt_host,
66
+ args.mqtt_port,
67
+ args.mqtt_user,
68
+ args.mqtt_pass,
69
+ args.mqtt_clientid,
70
+ args.mqtt_qos,
71
+ subscribe_topics=[args.mqtt_subscribe_topic_pattern],
72
+ log_level=args.loglevel,
73
+ )
74
+ if mqtt_handler.setup() and mqtt_handler.connect():
75
+ logger.info("Connected to MQTT broker.")
76
+ else:
77
+ logger.error("Failed to connect with MQTT broker, exiting...")
78
+ mqtt_handler.close()
79
+ exit(1)
80
+ except Exception as e:
81
+ logger.error(f"Error setting up MQTT input: {e}, exiting...")
82
+ mqtt_handler.close()
83
+ exit(1)
84
+
85
+ # setup modbus tasks
86
+ modbus_handlers = setup_modbus_handlers(args, mqtt_handler)
87
+ if modbus_handlers:
88
+ logger.info(f"Loaded {len(modbus_handlers)} Modbus config(s).")
89
+ delay_thread(args.delay)
90
+ else:
91
+ logger.error("No Modbus config(s) defined. Exiting...")
92
+ if mqtt_handler:
93
+ mqtt_handler.close()
94
+ exit(1)
95
+
96
+ # main loop
97
+ last_check = 0
98
+ last_diag = 0
99
+ while not on_threading_event():
100
+ now = get_utc_time()
101
+ # routine check
102
+ if now > last_check + args.rate:
103
+ if last_check == 0:
104
+ elapsed = args.rate
105
+ else:
106
+ elapsed = round(now - last_check, 6)
107
+ last_check = now
108
+ logger.info(
109
+ f" === Modpoll is polling at rate:{args.rate}s, actual:{elapsed}s ==="
110
+ )
111
+ for modbus_handler in modbus_handlers:
112
+ modbus_handler.poll()
113
+ if on_threading_event():
114
+ break
115
+ if args.mqtt_host:
116
+ if args.timestamp:
117
+ modbus_handler.publish_data(timestamp=now)
118
+ else:
119
+ modbus_handler.publish_data()
120
+ if args.export:
121
+ if args.timestamp:
122
+ modbus_handler.export(args.export, timestamp=now)
123
+ else:
124
+ modbus_handler.export(args.export)
125
+ if args.diagnostics_rate > 0 and now > last_diag + args.diagnostics_rate:
126
+ last_diag = now
127
+ for modbus_handler in modbus_handlers:
128
+ modbus_handler.publish_diagnostics()
129
+ if on_threading_event():
130
+ break
131
+ # Check if receive mqtt request
132
+ if mqtt_handler:
133
+ topic, payload = mqtt_handler.receive()
134
+ if topic and payload:
135
+ # extract device_name
136
+ topic_regex = args.mqtt_subscribe_topic_pattern.replace(
137
+ "+", "([^/\n]*)"
138
+ )
139
+ match = re.search(topic_regex, topic)
140
+ if match:
141
+ device_name = match.group(1)
142
+ logger.info(
143
+ f"Received request to write data for device {device_name}"
144
+ )
145
+ try:
146
+ reg = json.loads(payload)
147
+ object_type = reg["object_type"]
148
+ address = reg["address"]
149
+ value = reg["value"]
150
+
151
+ device_found = False
152
+ for modbus_handler in modbus_handlers:
153
+ if device_name in [
154
+ dev.name for dev in modbus_handler.get_device_list()
155
+ ]:
156
+ device_found = True
157
+ write_success = False
158
+ if object_type == "coil":
159
+ write_success = modbus_handler.write_coil(
160
+ device_name, address, value
161
+ )
162
+ elif object_type == "holding_register":
163
+ write_success = modbus_handler.write_register(
164
+ device_name, address, value
165
+ )
166
+
167
+ if write_success:
168
+ logger.info(
169
+ f"Successfully wrote {object_type}: device={device_name}, address={address}, value={value}"
170
+ )
171
+ else:
172
+ logger.warning(
173
+ f"Failed to write {object_type}: device={device_name}, address={address}, value={value}"
174
+ )
175
+ break
176
+
177
+ if not device_found:
178
+ logger.error(f"No device found with name: {device_name}")
179
+
180
+ except KeyError as e:
181
+ logger.error(f"Missing required key in payload: {e}")
182
+ except json.JSONDecodeError:
183
+ logger.error(f"Failed to parse JSON message: {payload}")
184
+ else:
185
+ logger.error(f"Failed to extract device name from topic: {topic}")
186
+ if args.once:
187
+ set_threading_event()
188
+ break
189
+
190
+ delay_thread(0.01)
191
+
192
+ for modbus_handler in modbus_handlers:
193
+ modbus_handler.close()
194
+ if mqtt_handler:
195
+ mqtt_handler.close()
196
+
197
+
198
+ if __name__ == "__main__":
199
+ app()