modpoll 1.2.0__tar.gz → 1.3.3__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.3}/PKG-INFO +30 -17
- {modpoll-1.2.0 → modpoll-1.3.3}/README.md +28 -15
- modpoll-1.3.3/modpoll/__init__.py +3 -0
- {modpoll-1.2.0 → modpoll-1.3.3}/modpoll/arg_parser.py +9 -8
- modpoll-1.3.3/modpoll/main.py +199 -0
- modpoll-1.3.3/modpoll/modbus_task.py +646 -0
- modpoll-1.3.3/modpoll/mqtt_task.py +249 -0
- modpoll-1.3.3/modpoll/utils.py +23 -0
- {modpoll-1.2.0 → modpoll-1.3.3}/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.3}/LICENSE +0 -0
- {modpoll-1.2.0 → modpoll-1.3.3}/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.3
|
|
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
|
|
@@ -11,8 +11,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
15
|
Provides-Extra: serial
|
|
15
|
-
Requires-Dist: importlib-metadata (>=8.3.0,<9.0.0)
|
|
16
16
|
Requires-Dist: paho-mqtt (>=2.1.0,<3.0.0)
|
|
17
17
|
Requires-Dist: prettytable (>=3.9.0,<4.0.0)
|
|
18
18
|
Requires-Dist: pymodbus (>=3.6.9,<3.7.0)
|
|
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
[](https://img.shields.io/github/license/gavinying/modpoll)
|
|
30
30
|
[](https://pepy.tech/project/modpoll)
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
> 📢 **Announcement:** Since the release of v1.3.0, our official dockerhub namespace has been changed to `topmaker`, you can pull the latest images from [topmaker/modpoll](https://hub.docker.com/repository/docker/topmaker/modpoll) at dockerhub.
|
|
33
33
|
|
|
34
34
|
> Learn more about *modpoll* usage at [documentation](https://gavinying.github.io/modpoll) site.
|
|
35
35
|
|
|
@@ -124,7 +124,7 @@ modpoll --once \
|
|
|
124
124
|
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
125
|
|
|
126
126
|
```CSV
|
|
127
|
-
device,
|
|
127
|
+
device,modsim01,1,,
|
|
128
128
|
poll,coil,0,16,BE_BE
|
|
129
129
|
ref,coil01-08,0,bool8,rw
|
|
130
130
|
ref,coil09-16,1,bool8,rw
|
|
@@ -183,7 +183,7 @@ The configuration can be either a local file or a remote public URL resource.
|
|
|
183
183
|
If you are blocked by company firewall for online device or prefer a local test, you can launch your own device simulator by running *modsim* locally,
|
|
184
184
|
|
|
185
185
|
```bash
|
|
186
|
-
docker run --rm -p 5020:5020
|
|
186
|
+
docker run --rm -p 5020:5020 topmaker/modsim
|
|
187
187
|
```
|
|
188
188
|
|
|
189
189
|
It will create a virtual Modbus TCP device running at `localhost:5020`, and you can open a new terminal, poll the virtual device using *modpoll* tool,
|
|
@@ -198,7 +198,7 @@ modpoll \
|
|
|
198
198
|
> Use `sudo` before the docker command if you want to use the standard port `502`.
|
|
199
199
|
|
|
200
200
|
```bash
|
|
201
|
-
sudo docker run --rm -p 502:5020
|
|
201
|
+
sudo docker run --rm -p 502:5020 topmaker/modsim
|
|
202
202
|
```
|
|
203
203
|
|
|
204
204
|
In a new terminal,
|
|
@@ -222,11 +222,17 @@ modpoll \
|
|
|
222
222
|
--config https://raw.githubusercontent.com/gavinying/modpoll/master/examples/modsim.csv
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
-
With successful data polling and publishing, you can subscribe the topic `modpoll/
|
|
225
|
+
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
226
|
|
|
227
|
-
The MQTT
|
|
227
|
+
The MQTT topics can be customized by providing the following arguments,
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
- `mqtt-publish-topic-pattern`
|
|
230
|
+
- `mqtt-subscribe-topic-pattern`
|
|
231
|
+
- `mqtt-diagnostics-topic-pattern`
|
|
232
|
+
|
|
233
|
+
See [document](https://gavinying.github.io/modpoll/usage.html#Named%20Arguments) for details.
|
|
234
|
+
|
|
235
|
+
> ⚠️ **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
236
|
|
|
231
237
|
|
|
232
238
|
<p align="center">
|
|
@@ -236,7 +242,7 @@ The MQTT topic uses `<mqtt_topic_prefix>/<device_name>` pattern, <mqtt_topic_pre
|
|
|
236
242
|
|
|
237
243
|
### Write registers via MQTT publish
|
|
238
244
|
|
|
239
|
-
The *modpoll* tool will subscribe to the topic `modpoll
|
|
245
|
+
The *modpoll* tool will subscribe to the topic `modpoll/{{device_name}}/set` by default once it successfully connected to MQTT broker, user can write device register(s) via MQTT publish,
|
|
240
246
|
|
|
241
247
|
- To write a single holding register (address at `40001`)
|
|
242
248
|
|
|
@@ -264,7 +270,7 @@ The *modpoll* tool will subscribe to the topic `modpoll/<device_name>/set` by de
|
|
|
264
270
|
A docker image has been provided for user to directly run the tool without local installation,
|
|
265
271
|
|
|
266
272
|
```bash
|
|
267
|
-
docker run --rm
|
|
273
|
+
docker run --rm topmaker/modpoll
|
|
268
274
|
```
|
|
269
275
|
|
|
270
276
|
It shows the version of the tool by default.
|
|
@@ -272,7 +278,7 @@ It shows the version of the tool by default.
|
|
|
272
278
|
Similar to the above *modsim* test, we can poll data with `docker run`, in order to avoid printing out received data, the argument `--daemon` or `-d` is recommended to use with docker.
|
|
273
279
|
|
|
274
280
|
```bash
|
|
275
|
-
docker run --rm
|
|
281
|
+
docker run --rm topmaker/modpoll \
|
|
276
282
|
modpoll -d \
|
|
277
283
|
--tcp modsim.topmaker.net \
|
|
278
284
|
--config https://raw.githubusercontent.com/gavinying/modpoll/master/examples/modsim.csv
|
|
@@ -282,14 +288,14 @@ If you want to load a local configure file, you need to mount a local folder ont
|
|
|
282
288
|
for example, if the child folder `examples` contains the config file `modsim.csv`, we can use it via the following command,
|
|
283
289
|
|
|
284
290
|
```bash
|
|
285
|
-
docker run --rm -v $(pwd)/examples:/app/examples
|
|
291
|
+
docker run --rm -v $(pwd)/examples:/app/examples topmaker/modpoll \
|
|
286
292
|
modpoll -d \
|
|
287
293
|
--tcp modsim.topmaker.net \
|
|
288
294
|
--config /app/examples/modsim.csv
|
|
289
295
|
```
|
|
290
296
|
|
|
291
297
|
|
|
292
|
-
##
|
|
298
|
+
## Example Use Cases
|
|
293
299
|
|
|
294
300
|
- Connect to Modbus TCP device
|
|
295
301
|
|
|
@@ -313,7 +319,6 @@ for example, if the child folder `examples` contains the config file `modsim.csv
|
|
|
313
319
|
```bash
|
|
314
320
|
modpoll \
|
|
315
321
|
--tcp modsim.topmaker.net \
|
|
316
|
-
--tcp-port 5020 \
|
|
317
322
|
--mqtt-host mqtt.eclipseprojects.io \
|
|
318
323
|
--config examples/modsim.csv
|
|
319
324
|
```
|
|
@@ -323,14 +328,22 @@ for example, if the child folder `examples` contains the config file `modsim.csv
|
|
|
323
328
|
```bash
|
|
324
329
|
modpoll \
|
|
325
330
|
--tcp modsim.topmaker.net \
|
|
326
|
-
--tcp-port 5020 \
|
|
327
331
|
--export data.csv \
|
|
328
332
|
--config examples/modsim.csv
|
|
329
333
|
```
|
|
330
334
|
|
|
335
|
+
- Connect to Modbus TCP devices using multiple config files
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
modpoll \
|
|
339
|
+
--tcp modsim.topmaker.net \
|
|
340
|
+
--config examples/modsim.csv examples/modsim2.csv
|
|
341
|
+
```
|
|
342
|
+
|
|
331
343
|
|
|
332
|
-
>
|
|
344
|
+
> Refer to the [documentation](https://gavinying.github.io/modpoll) site for more details about the configuration and examples.
|
|
333
345
|
|
|
346
|
+
> *If you find this tool useful, please support us by starring 🌟 our repository to encourage further improvements.*
|
|
334
347
|
|
|
335
348
|
## Credits
|
|
336
349
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://img.shields.io/github/license/gavinying/modpoll)
|
|
6
6
|
[](https://pepy.tech/project/modpoll)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
> 📢 **Announcement:** Since the release of v1.3.0, our official dockerhub namespace has been changed to `topmaker`, you can pull the latest images from [topmaker/modpoll](https://hub.docker.com/repository/docker/topmaker/modpoll) at dockerhub.
|
|
9
9
|
|
|
10
10
|
> Learn more about *modpoll* usage at [documentation](https://gavinying.github.io/modpoll) site.
|
|
11
11
|
|
|
@@ -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
|
|
@@ -159,7 +159,7 @@ The configuration can be either a local file or a remote public URL resource.
|
|
|
159
159
|
If you are blocked by company firewall for online device or prefer a local test, you can launch your own device simulator by running *modsim* locally,
|
|
160
160
|
|
|
161
161
|
```bash
|
|
162
|
-
docker run --rm -p 5020:5020
|
|
162
|
+
docker run --rm -p 5020:5020 topmaker/modsim
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
It will create a virtual Modbus TCP device running at `localhost:5020`, and you can open a new terminal, poll the virtual device using *modpoll* tool,
|
|
@@ -174,7 +174,7 @@ modpoll \
|
|
|
174
174
|
> Use `sudo` before the docker command if you want to use the standard port `502`.
|
|
175
175
|
|
|
176
176
|
```bash
|
|
177
|
-
sudo docker run --rm -p 502:5020
|
|
177
|
+
sudo docker run --rm -p 502:5020 topmaker/modsim
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
In a new terminal,
|
|
@@ -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">
|
|
@@ -212,7 +218,7 @@ The MQTT topic uses `<mqtt_topic_prefix>/<device_name>` pattern, <mqtt_topic_pre
|
|
|
212
218
|
|
|
213
219
|
### Write registers via MQTT publish
|
|
214
220
|
|
|
215
|
-
The *modpoll* tool will subscribe to the topic `modpoll
|
|
221
|
+
The *modpoll* tool will subscribe to the topic `modpoll/{{device_name}}/set` by default once it successfully connected to MQTT broker, user can write device register(s) via MQTT publish,
|
|
216
222
|
|
|
217
223
|
- To write a single holding register (address at `40001`)
|
|
218
224
|
|
|
@@ -240,7 +246,7 @@ The *modpoll* tool will subscribe to the topic `modpoll/<device_name>/set` by de
|
|
|
240
246
|
A docker image has been provided for user to directly run the tool without local installation,
|
|
241
247
|
|
|
242
248
|
```bash
|
|
243
|
-
docker run --rm
|
|
249
|
+
docker run --rm topmaker/modpoll
|
|
244
250
|
```
|
|
245
251
|
|
|
246
252
|
It shows the version of the tool by default.
|
|
@@ -248,7 +254,7 @@ It shows the version of the tool by default.
|
|
|
248
254
|
Similar to the above *modsim* test, we can poll data with `docker run`, in order to avoid printing out received data, the argument `--daemon` or `-d` is recommended to use with docker.
|
|
249
255
|
|
|
250
256
|
```bash
|
|
251
|
-
docker run --rm
|
|
257
|
+
docker run --rm topmaker/modpoll \
|
|
252
258
|
modpoll -d \
|
|
253
259
|
--tcp modsim.topmaker.net \
|
|
254
260
|
--config https://raw.githubusercontent.com/gavinying/modpoll/master/examples/modsim.csv
|
|
@@ -258,14 +264,14 @@ If you want to load a local configure file, you need to mount a local folder ont
|
|
|
258
264
|
for example, if the child folder `examples` contains the config file `modsim.csv`, we can use it via the following command,
|
|
259
265
|
|
|
260
266
|
```bash
|
|
261
|
-
docker run --rm -v $(pwd)/examples:/app/examples
|
|
267
|
+
docker run --rm -v $(pwd)/examples:/app/examples topmaker/modpoll \
|
|
262
268
|
modpoll -d \
|
|
263
269
|
--tcp modsim.topmaker.net \
|
|
264
270
|
--config /app/examples/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,14 +304,22 @@ 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
|
|
|
322
|
+
> *If you find this tool useful, please support us by starring 🌟 our repository to encourage further improvements.*
|
|
310
323
|
|
|
311
324
|
## Credits
|
|
312
325
|
|
|
@@ -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
|
|
111
|
-
help='Topic pattern for MQTT publish. Use
|
|
111
|
+
default="modpoll/{{device_name}}/data",
|
|
112
|
+
help='Topic pattern for MQTT publish. Use {{device_name}} as placeholder for the device names in Modbus config. Defaults to "modpoll/{{device_name}}/data"',
|
|
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
|
|
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()
|