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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modpoll
3
- Version: 1.3.0
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
  [![License](https://img.shields.io/github/license/gavinying/modpoll)](https://img.shields.io/github/license/gavinying/modpoll)
29
30
  [![Downloads](https://static.pepy.tech/badge/modpoll/week)](https://pepy.tech/project/modpoll)
30
31
 
31
- Show your love for this project by starring our repo 🌟, so we are encouraged to continue improving.
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 helloysd/modsim
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 helloysd/modsim
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/<device_name>/set` by default once it successfully connected to MQTT broker, user can write device register(s) via MQTT publish,
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 helloysd/modpoll
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 helloysd/modpoll \
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 helloysd/modpoll \
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
- > *Refer to the [documentation](https://gavinying.github.io/modpoll) site for more details about the configuration and examples.*
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
  [![License](https://img.shields.io/github/license/gavinying/modpoll)](https://img.shields.io/github/license/gavinying/modpoll)
6
6
  [![Downloads](https://static.pepy.tech/badge/modpoll/week)](https://pepy.tech/project/modpoll)
7
7
 
8
- Show your love for this project by starring our repo 🌟, so we are encouraged to continue improving.
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 helloysd/modsim
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 helloysd/modsim
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/<device_name>/set` by default once it successfully connected to MQTT broker, user can write device register(s) via MQTT publish,
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 helloysd/modpoll
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 helloysd/modpoll \
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 helloysd/modpoll \
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
- > *Refer to the [documentation](https://gavinying.github.io/modpoll) site for more details about the configuration and examples.*
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=1.0,
43
- help="The time interval in seconds between two polling, Defaults to 1.0",
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/<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>"',
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/<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",
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}/<device_name>"
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 = 1.0,
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
- value = (
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
- payload[ref.name] = value
524
- if payload:
525
- if timestamp:
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
- "<device_name>", dev.name
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
- "<device_name>", dev.name
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 = 0, retain: bool = False
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(
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "modpoll"
3
- version = "v1.3.0"
3
+ version = "v1.4.0"
4
4
  description = "A New Command-line Tool for Modbus and MQTT"
5
5
  authors = ["Ying Shaodong <helloysd@gmail.com>"]
6
6
  repository = "https://github.com/gavinying/modpoll"
File without changes
File without changes
File without changes
File without changes