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.
- {modpoll-1.2.0 → modpoll-1.3.0}/PKG-INFO +20 -9
- {modpoll-1.2.0 → modpoll-1.3.0}/README.md +19 -7
- modpoll-1.3.0/modpoll/__init__.py +3 -0
- {modpoll-1.2.0 → modpoll-1.3.0}/modpoll/arg_parser.py +8 -7
- modpoll-1.3.0/modpoll/main.py +199 -0
- modpoll-1.3.0/modpoll/modbus_task.py +646 -0
- modpoll-1.3.0/modpoll/mqtt_task.py +246 -0
- modpoll-1.3.0/modpoll/utils.py +23 -0
- {modpoll-1.2.0 → modpoll-1.3.0}/pyproject.toml +3 -5
- modpoll-1.2.0/modpoll/__init__.py +0 -5
- modpoll-1.2.0/modpoll/main.py +0 -191
- modpoll-1.2.0/modpoll/modbus_task.py +0 -624
- modpoll-1.2.0/modpoll/mqtt_task.py +0 -226
- {modpoll-1.2.0 → modpoll-1.3.0}/LICENSE +0 -0
- {modpoll-1.2.0 → modpoll-1.3.0}/modpoll/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: modpoll
|
|
3
|
-
Version: 1.
|
|
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,
|
|
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/
|
|
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
|
|
226
|
+
The MQTT topics can be customized by providing the following arguments,
|
|
228
227
|
|
|
229
|
-
|
|
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
|
-
##
|
|
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,
|
|
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/
|
|
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
|
|
203
|
+
The MQTT topics can be customized by providing the following arguments,
|
|
204
204
|
|
|
205
|
-
|
|
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
|
-
##
|
|
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
|
|
|
@@ -5,19 +5,20 @@ from . import __version__
|
|
|
5
5
|
|
|
6
6
|
def get_parser():
|
|
7
7
|
parser = argparse.ArgumentParser(
|
|
8
|
-
description=f"
|
|
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"
|
|
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
|
|
116
|
-
help='Topic pattern for MQTT subscribe.
|
|
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=
|
|
121
|
-
help="Topic pattern for MQTT diagnostics. Use <device_name> as placeholder for the device names in Modbus config. Defaults to
|
|
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()
|