modpoll 1.3.0__tar.gz → 1.4.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.3.0 → modpoll-1.4.0}/PKG-INFO +11 -9
- {modpoll-1.3.0 → modpoll-1.4.0}/README.md +9 -8
- {modpoll-1.3.0 → modpoll-1.4.0}/modpoll/arg_parser.py +6 -6
- {modpoll-1.3.0 → modpoll-1.4.0}/modpoll/main.py +1 -1
- {modpoll-1.3.0 → modpoll-1.4.0}/modpoll/modbus_task.py +32 -14
- {modpoll-1.3.0 → modpoll-1.4.0}/modpoll/mqtt_task.py +4 -1
- {modpoll-1.3.0 → modpoll-1.4.0}/pyproject.toml +1 -1
- {modpoll-1.3.0 → modpoll-1.4.0}/LICENSE +0 -0
- {modpoll-1.3.0 → modpoll-1.4.0}/modpoll/__init__.py +0 -0
- {modpoll-1.3.0 → modpoll-1.4.0}/modpoll/__main__.py +0 -0
- {modpoll-1.3.0 → modpoll-1.4.0}/modpoll/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: modpoll
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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
|
|
@@ -11,6 +11,7 @@ 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
16
|
Requires-Dist: paho-mqtt (>=2.1.0,<3.0.0)
|
|
16
17
|
Requires-Dist: prettytable (>=3.9.0,<4.0.0)
|
|
@@ -28,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
28
29
|
[](https://img.shields.io/github/license/gavinying/modpoll)
|
|
29
30
|
[](https://pepy.tech/project/modpoll)
|
|
30
31
|
|
|
31
|
-
|
|
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.
|
|
32
33
|
|
|
33
34
|
> Learn more about *modpoll* usage at [documentation](https://gavinying.github.io/modpoll) site.
|
|
34
35
|
|
|
@@ -182,7 +183,7 @@ The configuration can be either a local file or a remote public URL resource.
|
|
|
182
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,
|
|
183
184
|
|
|
184
185
|
```bash
|
|
185
|
-
docker run --rm -p 5020:5020
|
|
186
|
+
docker run --rm -p 5020:5020 topmaker/modsim
|
|
186
187
|
```
|
|
187
188
|
|
|
188
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,
|
|
@@ -197,7 +198,7 @@ modpoll \
|
|
|
197
198
|
> Use `sudo` before the docker command if you want to use the standard port `502`.
|
|
198
199
|
|
|
199
200
|
```bash
|
|
200
|
-
sudo docker run --rm -p 502:5020
|
|
201
|
+
sudo docker run --rm -p 502:5020 topmaker/modsim
|
|
201
202
|
```
|
|
202
203
|
|
|
203
204
|
In a new terminal,
|
|
@@ -241,7 +242,7 @@ See [document](https://gavinying.github.io/modpoll/usage.html#Named%20Arguments)
|
|
|
241
242
|
|
|
242
243
|
### Write registers via MQTT publish
|
|
243
244
|
|
|
244
|
-
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,
|
|
245
246
|
|
|
246
247
|
- To write a single holding register (address at `40001`)
|
|
247
248
|
|
|
@@ -269,7 +270,7 @@ The *modpoll* tool will subscribe to the topic `modpoll/<device_name>/set` by de
|
|
|
269
270
|
A docker image has been provided for user to directly run the tool without local installation,
|
|
270
271
|
|
|
271
272
|
```bash
|
|
272
|
-
docker run --rm
|
|
273
|
+
docker run --rm topmaker/modpoll
|
|
273
274
|
```
|
|
274
275
|
|
|
275
276
|
It shows the version of the tool by default.
|
|
@@ -277,7 +278,7 @@ It shows the version of the tool by default.
|
|
|
277
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.
|
|
278
279
|
|
|
279
280
|
```bash
|
|
280
|
-
docker run --rm
|
|
281
|
+
docker run --rm topmaker/modpoll \
|
|
281
282
|
modpoll -d \
|
|
282
283
|
--tcp modsim.topmaker.net \
|
|
283
284
|
--config https://raw.githubusercontent.com/gavinying/modpoll/master/examples/modsim.csv
|
|
@@ -287,7 +288,7 @@ If you want to load a local configure file, you need to mount a local folder ont
|
|
|
287
288
|
for example, if the child folder `examples` contains the config file `modsim.csv`, we can use it via the following command,
|
|
288
289
|
|
|
289
290
|
```bash
|
|
290
|
-
docker run --rm -v $(pwd)/examples:/app/examples
|
|
291
|
+
docker run --rm -v $(pwd)/examples:/app/examples topmaker/modpoll \
|
|
291
292
|
modpoll -d \
|
|
292
293
|
--tcp modsim.topmaker.net \
|
|
293
294
|
--config /app/examples/modsim.csv
|
|
@@ -340,8 +341,9 @@ for example, if the child folder `examples` contains the config file `modsim.csv
|
|
|
340
341
|
```
|
|
341
342
|
|
|
342
343
|
|
|
343
|
-
>
|
|
344
|
+
> Refer to the [documentation](https://gavinying.github.io/modpoll) site for more details about the configuration and examples.
|
|
344
345
|
|
|
346
|
+
> *If you find this tool useful, please support us by starring 🌟 our repository to encourage further improvements.*
|
|
345
347
|
|
|
346
348
|
## Credits
|
|
347
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
|
|
|
@@ -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,
|
|
@@ -218,7 +218,7 @@ See [document](https://gavinying.github.io/modpoll/usage.html#Named%20Arguments)
|
|
|
218
218
|
|
|
219
219
|
### Write registers via MQTT publish
|
|
220
220
|
|
|
221
|
-
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,
|
|
222
222
|
|
|
223
223
|
- To write a single holding register (address at `40001`)
|
|
224
224
|
|
|
@@ -246,7 +246,7 @@ The *modpoll* tool will subscribe to the topic `modpoll/<device_name>/set` by de
|
|
|
246
246
|
A docker image has been provided for user to directly run the tool without local installation,
|
|
247
247
|
|
|
248
248
|
```bash
|
|
249
|
-
docker run --rm
|
|
249
|
+
docker run --rm topmaker/modpoll
|
|
250
250
|
```
|
|
251
251
|
|
|
252
252
|
It shows the version of the tool by default.
|
|
@@ -254,7 +254,7 @@ It shows the version of the tool by default.
|
|
|
254
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.
|
|
255
255
|
|
|
256
256
|
```bash
|
|
257
|
-
docker run --rm
|
|
257
|
+
docker run --rm topmaker/modpoll \
|
|
258
258
|
modpoll -d \
|
|
259
259
|
--tcp modsim.topmaker.net \
|
|
260
260
|
--config https://raw.githubusercontent.com/gavinying/modpoll/master/examples/modsim.csv
|
|
@@ -264,7 +264,7 @@ If you want to load a local configure file, you need to mount a local folder ont
|
|
|
264
264
|
for example, if the child folder `examples` contains the config file `modsim.csv`, we can use it via the following command,
|
|
265
265
|
|
|
266
266
|
```bash
|
|
267
|
-
docker run --rm -v $(pwd)/examples:/app/examples
|
|
267
|
+
docker run --rm -v $(pwd)/examples:/app/examples topmaker/modpoll \
|
|
268
268
|
modpoll -d \
|
|
269
269
|
--tcp modsim.topmaker.net \
|
|
270
270
|
--config /app/examples/modsim.csv
|
|
@@ -317,8 +317,9 @@ for example, if the child folder `examples` contains the config file `modsim.csv
|
|
|
317
317
|
```
|
|
318
318
|
|
|
319
319
|
|
|
320
|
-
>
|
|
320
|
+
> Refer to the [documentation](https://gavinying.github.io/modpoll) site for more details about the configuration and examples.
|
|
321
321
|
|
|
322
|
+
> *If you find this tool useful, please support us by starring 🌟 our repository to encourage further improvements.*
|
|
322
323
|
|
|
323
324
|
## Credits
|
|
324
325
|
|
|
@@ -39,8 +39,8 @@ def get_parser():
|
|
|
39
39
|
parser.add_argument(
|
|
40
40
|
"--interval",
|
|
41
41
|
type=float,
|
|
42
|
-
default=
|
|
43
|
-
help="The time interval in seconds between two polling, Defaults to
|
|
42
|
+
default=0.5,
|
|
43
|
+
help="The time interval in seconds between two polling, Defaults to 0.5",
|
|
44
44
|
)
|
|
45
45
|
parser.add_argument(
|
|
46
46
|
"--tcp", help="Act as a Modbus TCP master, connecting to host TCP"
|
|
@@ -108,8 +108,8 @@ def get_parser():
|
|
|
108
108
|
)
|
|
109
109
|
parser.add_argument(
|
|
110
110
|
"--mqtt-publish-topic-pattern",
|
|
111
|
-
default="modpoll
|
|
112
|
-
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"',
|
|
113
113
|
)
|
|
114
114
|
parser.add_argument(
|
|
115
115
|
"--mqtt-subscribe-topic-pattern",
|
|
@@ -118,8 +118,8 @@ def get_parser():
|
|
|
118
118
|
)
|
|
119
119
|
parser.add_argument(
|
|
120
120
|
"--mqtt-diagnostics-topic-pattern",
|
|
121
|
-
default="modpoll
|
|
122
|
-
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",
|
|
123
123
|
)
|
|
124
124
|
parser.add_argument(
|
|
125
125
|
"--mqtt-qos",
|
|
@@ -51,7 +51,7 @@ def app(name="modpoll"):
|
|
|
51
51
|
)
|
|
52
52
|
if args.mqtt_topic_prefix.endswith("/"):
|
|
53
53
|
args.mqtt_topic_prefix = args.mqtt_topic_prefix[:-1]
|
|
54
|
-
args.mqtt_publish_topic_pattern = f"{args.mqtt_topic_prefix}
|
|
54
|
+
args.mqtt_publish_topic_pattern = f"{args.mqtt_topic_prefix}/{{device_name}}"
|
|
55
55
|
args.mqtt_subscribe_topic_pattern = f"{args.mqtt_topic_prefix}/#/set"
|
|
56
56
|
|
|
57
57
|
# setup mqtt
|
|
@@ -264,10 +264,11 @@ class ModbusHandler:
|
|
|
264
264
|
config_file: str,
|
|
265
265
|
mqtt_handler: Optional[MqttHandler] = None,
|
|
266
266
|
timeout: float = 3.0,
|
|
267
|
-
interval: float =
|
|
267
|
+
interval: float = 0.5,
|
|
268
268
|
daemon: bool = False,
|
|
269
269
|
mqtt_publish_topic_pattern: Optional[str] = None,
|
|
270
270
|
mqtt_diagnostics_topic_pattern: Optional[str] = None,
|
|
271
|
+
mqtt_single_publish: bool = False,
|
|
271
272
|
):
|
|
272
273
|
self.modbus_client = modbus_client
|
|
273
274
|
self.config_file = config_file
|
|
@@ -277,6 +278,7 @@ class ModbusHandler:
|
|
|
277
278
|
self.daemon = daemon
|
|
278
279
|
self.mqtt_publish_topic_pattern = mqtt_publish_topic_pattern
|
|
279
280
|
self.mqtt_diagnostics_topic_pattern = mqtt_diagnostics_topic_pattern
|
|
281
|
+
self.mqtt_single_publish = mqtt_single_publish
|
|
280
282
|
self.connected = False
|
|
281
283
|
self.deviceList: List[Device] = []
|
|
282
284
|
self.logger = logging.getLogger(__name__)
|
|
@@ -319,7 +321,7 @@ class ModbusHandler:
|
|
|
319
321
|
continue
|
|
320
322
|
device_name = row[1].strip()
|
|
321
323
|
try:
|
|
322
|
-
device_id = int(row[2])
|
|
324
|
+
device_id = int(row[2], 0)
|
|
323
325
|
except ValueError:
|
|
324
326
|
self.logger.error(f"Invalid device ID for {device_name}")
|
|
325
327
|
continue
|
|
@@ -357,8 +359,8 @@ class ModbusHandler:
|
|
|
357
359
|
return None
|
|
358
360
|
fc = row[1].lower()
|
|
359
361
|
try:
|
|
360
|
-
start_address = int(row[2])
|
|
361
|
-
size = int(row[3])
|
|
362
|
+
start_address = int(row[2], 0)
|
|
363
|
+
size = int(row[3], 0)
|
|
362
364
|
except ValueError:
|
|
363
365
|
self.logger.error("Invalid start address or size for poller")
|
|
364
366
|
return None
|
|
@@ -401,7 +403,7 @@ class ModbusHandler:
|
|
|
401
403
|
return None
|
|
402
404
|
ref_name = row[1].replace(" ", "_")
|
|
403
405
|
try:
|
|
404
|
-
address = int(row[2])
|
|
406
|
+
address = int(row[2], 0)
|
|
405
407
|
except ValueError:
|
|
406
408
|
self.logger.error(f"Invalid address for reference {ref_name}")
|
|
407
409
|
return None
|
|
@@ -507,25 +509,40 @@ class ModbusHandler:
|
|
|
507
509
|
print(table)
|
|
508
510
|
|
|
509
511
|
def publish_data(self, timestamp=None, on_change=False):
|
|
510
|
-
if not self.mqtt_handler:
|
|
511
|
-
return
|
|
512
|
-
if not self.mqtt_publish_topic_pattern:
|
|
512
|
+
if not self.mqtt_handler or not self.mqtt_publish_topic_pattern:
|
|
513
513
|
return
|
|
514
|
+
|
|
514
515
|
for dev in self.deviceList:
|
|
516
|
+
if not dev.pollSuccess:
|
|
517
|
+
self.logger.debug(
|
|
518
|
+
f"Skip publishing for disconnected device: {dev.name}"
|
|
519
|
+
)
|
|
520
|
+
continue
|
|
521
|
+
|
|
515
522
|
payload = {}
|
|
516
523
|
for ref in dev.references.values():
|
|
517
524
|
if not on_change or ref.val != ref.last_val:
|
|
518
|
-
|
|
525
|
+
ref_val = (
|
|
519
526
|
round(ref.val, FLOAT_TYPE_PRECISION)
|
|
520
527
|
if isinstance(ref.val, float)
|
|
521
528
|
else ref.val
|
|
522
529
|
)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
530
|
+
key = f"{ref.name}|{ref.unit}" if ref.unit else ref.name
|
|
531
|
+
payload[key] = ref_val
|
|
532
|
+
|
|
533
|
+
if self.mqtt_single_publish:
|
|
534
|
+
topic = f"{self.mqtt_publish_topic_pattern.replace('{{device_name}}', dev.name)}/{ref.name}"
|
|
535
|
+
if isinstance(ref_val, list):
|
|
536
|
+
for i, entry in enumerate(ref_val):
|
|
537
|
+
self.mqtt_handler.publish(f"{topic}/{i}", entry)
|
|
538
|
+
else:
|
|
539
|
+
self.mqtt_handler.publish(topic, ref_val)
|
|
540
|
+
|
|
541
|
+
if payload and not self.mqtt_single_publish:
|
|
542
|
+
if timestamp is not None:
|
|
526
543
|
payload["timestamp"] = timestamp
|
|
527
544
|
topic = self.mqtt_publish_topic_pattern.replace(
|
|
528
|
-
"
|
|
545
|
+
"{{device_name}}", dev.name
|
|
529
546
|
)
|
|
530
547
|
self.mqtt_handler.publish(topic, json.dumps(payload))
|
|
531
548
|
|
|
@@ -541,7 +558,7 @@ class ModbusHandler:
|
|
|
541
558
|
"last_poll_success": dev.pollSuccess,
|
|
542
559
|
}
|
|
543
560
|
topic = self.mqtt_diagnostics_topic_pattern.replace(
|
|
544
|
-
"
|
|
561
|
+
"{{device_name}}", dev.name
|
|
545
562
|
)
|
|
546
563
|
self.mqtt_handler.publish(topic, json.dumps(payload))
|
|
547
564
|
|
|
@@ -581,6 +598,7 @@ def setup_modbus_handlers(args, mqtt_handler: Optional[MqttHandler] = None):
|
|
|
581
598
|
daemon=args.daemon,
|
|
582
599
|
mqtt_publish_topic_pattern=args.mqtt_publish_topic_pattern,
|
|
583
600
|
mqtt_diagnostics_topic_pattern=args.mqtt_diagnostics_topic_pattern,
|
|
601
|
+
mqtt_single_publish=args.mqtt_single,
|
|
584
602
|
)
|
|
585
603
|
if modbus_handler.load_config():
|
|
586
604
|
modbus_handlers.append(modbus_handler)
|
|
@@ -197,11 +197,14 @@ class MqttHandler:
|
|
|
197
197
|
return False
|
|
198
198
|
|
|
199
199
|
def publish(
|
|
200
|
-
self, topic: str, msg: str, qos: int =
|
|
200
|
+
self, topic: str, msg: str, qos: Optional[int] = None, retain: bool = False
|
|
201
201
|
) -> Optional[MQTTMessageInfo]:
|
|
202
202
|
if not self.mqtt_client:
|
|
203
203
|
self.logger.error("MQTT client not initialized. Call setup() first.")
|
|
204
204
|
return None
|
|
205
|
+
|
|
206
|
+
qos = self.qos if qos is None else qos
|
|
207
|
+
|
|
205
208
|
if not self.mqtt_client.is_connected():
|
|
206
209
|
if qos == 0:
|
|
207
210
|
self.logger.warning(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|