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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modpoll
3
- Version: 1.2.0
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
  [![License](https://img.shields.io/github/license/gavinying/modpoll)](https://img.shields.io/github/license/gavinying/modpoll)
30
30
  [![Downloads](https://static.pepy.tech/badge/modpoll/week)](https://pepy.tech/project/modpoll)
31
31
 
32
- 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.
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,modsim001,1,,
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 helloysd/modsim
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 helloysd/modsim
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/modsim` on the same MQTT broker `mqtt.eclipseprojects.io` to view the collected data.
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 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.
227
+ The MQTT topics can be customized by providing the following arguments,
228
228
 
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.
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/<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,
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 helloysd/modpoll
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 helloysd/modpoll \
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 helloysd/modpoll \
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
- ## Basic Usage
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
- > *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.
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
  [![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
 
@@ -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
@@ -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,
@@ -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">
@@ -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/<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,
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 helloysd/modpoll
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 helloysd/modpoll \
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 helloysd/modpoll \
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
- ## 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,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
- > *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.
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
 
@@ -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
- 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"',
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()