Compare commits

...

2 Commits

Author SHA1 Message Date
marcel
0804f82057 MQTT to APRS weather report bridge added 2025-09-18 14:46:43 +02:00
marcel
ce1b59fb6f Alpha version of ModBus-MQTT and MQTT-APRS software. 2025-08-14 15:39:50 +02:00
11 changed files with 475 additions and 72 deletions

View File

@@ -6,6 +6,7 @@ global:
#program-log: /home/marcel/rs458.log # All program output will be written to this file (0 = do not log to file) #program-log: /home/marcel/rs458.log # All program output will be written to this file (0 = do not log to file)
mqtt-server: mqtt.meezenest.nl mqtt-server: mqtt.meezenest.nl
mqtt-port: 1883 mqtt-port: 1883
telemetry-interval: 10 # number of seconds between transmissions
# APRS settings # APRS settings
aprs: aprs:
@@ -14,12 +15,10 @@ aprs:
to_call: PE1RXF-1 # Call of the receiver to_call: PE1RXF-1 # Call of the receiver
destination: APZMDM # APRS destination destination: APZMDM # APRS destination
digipath: 0 # Digipeater path for weather reports (0 = no path) digipath: 0 # Digipeater path for weather reports (0 = no path)
interval: 30
# - port: ax1 # Linux AX.25 port to which APRS weather report is sent # - port: ax1 # Linux AX.25 port to which APRS weather report is sent
# call: PE1RXF-13 # Call from which transmissions are made (can be a different call from the call assigned to the AX.25 port) # call: PE1RXF-13 # Call from which transmissions are made (can be a different call from the call assigned to the AX.25 port)
# destination: APZMDM # APRS destination # destination: APZMDM # APRS destination
# digipath: WIDE2-1 # Digipeater path for weather reports (0 = no path) # digipath: WIDE2-1 # Digipeater path for weather reports (0 = no path)
# interval: 30
# Define the MQTT data to transmit over AX25. The order of the items is the order the data is combined to the telemetry string. # Define the MQTT data to transmit over AX25. The order of the items is the order the data is combined to the telemetry string.
mqtt: mqtt:

View File

@@ -58,6 +58,7 @@ class config_reader:
tmp = self.config_file_settings['global']['program-log'] tmp = self.config_file_settings['global']['program-log']
tmp = self.config_file_settings['global']['mqtt-server'] tmp = self.config_file_settings['global']['mqtt-server']
tmp = self.config_file_settings['global']['mqtt-port'] tmp = self.config_file_settings['global']['mqtt-port']
tmp = self.config_file_settings['global']['telemetry-interval']
except: except:
print ("Error in the global section of the configuration file.") print ("Error in the global section of the configuration file.")
return 0 return 0
@@ -72,7 +73,6 @@ class config_reader:
tmp = entry['to_call'] tmp = entry['to_call']
tmp = entry['destination'] tmp = entry['destination']
tmp = entry['digipath'] tmp = entry['digipath']
tmp = entry['interval']
except: except:
print ("Error in the aprs section of the configuration file.") print ("Error in the aprs section of the configuration file.")
return 0 return 0

View File

@@ -43,7 +43,7 @@ class MqttHandler:
self.config = config self.config = config
# Define list with length equal to the number of subscriptions defined in the config file # Define list with length equal to the number of subscriptions defined in the config file
self.number_of_mqtt_subscriptions = len(config['mqtt']['subscribe']) self.number_of_mqtt_subscriptions = len(config['mqtt']['subscribe'])
self.aprs_telemetry_data = ['0.0']*self.number_of_mqtt_subscriptions self.aprs_telemetry_data = [bytes(b'0.0')]*self.number_of_mqtt_subscriptions
def process_message(self, client, userdata, message): def process_message(self, client, userdata, message):
@@ -191,16 +191,13 @@ def send_data_to_aprs(weather_data, configuration):
logging.error("Failed to send data to APRS radio.") logging.error("Failed to send data to APRS radio.")
logging.error(f"Command returned: {e}") logging.error(f"Command returned: {e}")
def main():
Configuration, MqttClient, mqtt_handler = setup() Configuration, MqttClient, mqtt_handler = setup()
LoopCounter = 0 LoopCounter = 0
while (1): while (1):
time.sleep(3) # Sleep for 3 seconds time.sleep(Configuration.config_file_settings['global']['telemetry-interval']) # Sleep for number of seconds set in config.yaml
# Send APRS telemetry every 10 cycles = every 10 minutes
LoopCounter = LoopCounter + 1
if LoopCounter >= 1:
# Send data to LoRa radio via external program (/usr/sbin/beacon). Make sure we use all radios defined in the configuration file. # Send data to LoRa radio via external program (/usr/sbin/beacon). Make sure we use all radios defined in the configuration file.
for entry in Configuration.config_file_settings['aprs']: for entry in Configuration.config_file_settings['aprs']:
@@ -209,7 +206,6 @@ while (1):
# We cannot send multiple APRS messages in a short period of time, so we wait 3 deconds between messages. # We cannot send multiple APRS messages in a short period of time, so we wait 3 deconds between messages.
time.sleep(3) time.sleep(3)
LoopCounter = 0 if __name__ == '__main__':
main()

View File

@@ -0,0 +1,27 @@
# Python MQTT to APRS weather report bridge
Forwards MQTT messages to AX.25 according to the APRS weather report protocol
## Configuration
Edit config.yaml.
## Requirements
- Python3
- minimalmodbus
- json
- time
- sys
- logging
AX.25 stack on Linux. "/usr/sbin/beacon" installed.
## License
Copyright (C) 2025 M.T. Konstapel
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

View File

@@ -0,0 +1,44 @@
# This is the configuration file for the Mees Electronics weather station MK2
# Global settings
global:
program-log: 0 # All program output will be written to this file (0 = do not log to file)
#program-log: /home/marcel/rs458.log # All program output will be written to this file (0 = do not log to file)
mqtt-server: mqtt.meezenest.nl
mqtt-port: 1883
telemetry-interval: 1100 # number of seconds between transmissions
# APRS settings
aprs:
- port: ax1 # Linux AX.25 port to which APRS weather report is sent
from_call: PE1RXF-13 # Call from which transmissions are made (can be a different call from the call assigned to the AX.25 port)
destination: APZMDM # APRS destination
digipath: 0 # Digipeater path for weather reports (0 = no path)
position: 5302.76N/00707.85E_
# - port: ax1 # Linux AX.25 port to which APRS weather report is sent
# call: PE1RXF-13 # Call from which transmissions are made (can be a different call from the call assigned to the AX.25 port)
# destination: APZMDM # APRS destination
# digipath: WIDE2-1 # Digipeater path for weather reports (0 = no path)
# Define the MQTT subjects for the weather report. The order is important, as this is the order in which the data is interpreted and put in the weather report.
# - wind_direction
# - wind_speed
# - wind_gust
# - rain_lasthour
# - rain_24hour
# - temperature
# - humidity
# - pressure
# - luminosity
mqtt:
subscribe:
- mees_electronics/4d45000000000002/wind_direction
- mees_electronics/4d45000000000002/wind_speed
- mees_electronics/4d45000000000002/wind_gust
- mees_electronics/4d45000000000002/rain_last_hour
- mees_electronics/4d45000000000002/rain_last_24 hours
- mees_electronics/4d45000000000002/temperature
- mees_electronics/4d45000000000002/humidity
- mees_electronics/4d45000000000002/barometric_pressure
- mees_electronics/4d45000000000002/luminosity

View File

@@ -0,0 +1,90 @@
""""
ModBus configuration file reader routines
Copyright (C) 2023-2025 M.T. Konstapel
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import yaml
from yaml.loader import SafeLoader
class config_reader:
# initiate class: define name configuration files
def __init__(self, config_file):
self.config_file = config_file
def read_settings(self):
if self.read_config_file() == 0:
return 0
if self.test_global_settings() == 0:
return 0
if self.test_aprs_settings() == 0:
return 0
if self.test_mqtt_settings() == 0:
return 0
return 1
def read_config_file (self):
try:
with open(self.config_file) as f:
self.config_file_settings = yaml.load(f, Loader=SafeLoader)
except:
print ("Configuration file ./" + self.config_file + " not found or syntax error in file.")
return 0
else:
return 1
# Test if all settings are pressent
def test_global_settings(self):
# Test is all expected settings are present
try:
tmp = self.config_file_settings['global']['program-log']
tmp = self.config_file_settings['global']['mqtt-server']
tmp = self.config_file_settings['global']['mqtt-port']
tmp = self.config_file_settings['global']['telemetry-interval']
except:
print ("Error in the global section of the configuration file.")
return 0
else:
return 1
def test_aprs_settings(self):
for entry in self.config_file_settings['aprs']:
try:
tmp = entry['port']
tmp = entry['from_call']
tmp = entry['destination']
tmp = entry['digipath']
tmp = entry['position']
except:
print ("Error in the aprs section of the configuration file.")
return 0
else:
return 1
def test_mqtt_settings(self):
# Test is all expected settings are present
try:
tmp = self.config_file_settings['mqtt']['subscribe']
except:
print ("Error in the mqtt section of the configuration file.")
return 0
else:
return 1

View File

@@ -0,0 +1,10 @@
import logging
def on_message(client, userdata, message, properties=None):
logging.info(
f"Received message {message.payload} on topic '{message.topic}' with QoS {message.qos}"
)
def on_subscribe(client, userdata, mid, qos, properties=None):
logging.info(f"Subscribed with QoS {qos}")

View File

@@ -0,0 +1,233 @@
""""
MQTT to aprs weather report for Mees Electronics sensors.
Subscribes to MQTT broker entries, combines the values and sends
the data as an aprs weather report over an AX.25 port.
Copyright (C) 2025 M.T. Konstapel
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import json
import time
from time import gmtime, strftime
import sys
import logging
import datetime
import subprocess
import os
import random
from pathlib import Path
from config_reader import config_reader
import paho.mqtt.client as mqtt
from paho.mqtt.client import CallbackAPIVersion
from paho.mqtt.properties import Properties
from paho.mqtt.packettypes import PacketTypes
properties=Properties(PacketTypes.PUBLISH)
properties.MessageExpiryInterval=30 # in seconds
#import ssl
class MqttHandler:
def __init__(self, config):
self.config = config
# Define list with length equal to the number of subscriptions defined in the config file
self.number_of_mqtt_subscriptions = len(config['mqtt']['subscribe'])
self.aprs_telemetry_data = [bytes(b'0')]*self.number_of_mqtt_subscriptions
def process_message(self, client, userdata, message):
# Loop through all mqtt:subscribe: entries in config.yaml and see which mqtt message we have. Put in in the correct position of the APRS telemetry string
for index, entry in enumerate(self.config['mqtt']['subscribe']):
if entry == message.topic:
self.aprs_telemetry_data[index] = message.payload
logging.info(f"Received message {message.payload} on topic '{message.topic}' with QoS {message.qos}")
def on_connect(self, client, userdata, flags, reason_code, properties=None):
logging.info('Connected to MQTT broker.')
# Returns the weather data
def get_aprs_telemetry(self):
return self.aprs_telemetry_data
def start_mqtt(config):
version = '3' # or '5'
mqtt_transport = 'tcp' # or 'websockets'
client_id = f'mees_electronics-mqtt-{random.randint(0, 1000)}'
if version == '5':
client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=client_id,
transport=mqtt_transport,
protocol=mqtt.MQTTv5)
if version == '3':
client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=client_id,
transport=mqtt_transport,
protocol=mqtt.MQTTv311,
clean_session=True)
#client.username_pw_set("user", "password")
mqtt_handler = MqttHandler(config)
client.on_connect = mqtt_handler.on_connect;
client.on_message = mqtt_handler.process_message
#client.on_message = mqtt_callbacks.on_message;
#client.on_publish = mqtt_callbacks.on_publish;
#client.on_subscribe = mqtt_callbacks.on_subscribe;
if version == '5':
from paho.mqtt.properties import Properties
from paho.mqtt.packettypes import PacketTypes
properties=Properties(PacketTypes.CONNECT)
properties.SessionExpiryInterval=30*60 # in seconds
client.connect(config['global']['mqtt-server'],
port=config['global']['mqtt-port'],
clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY,
properties=properties,
keepalive=60);
elif version == '3':
client.connect(config['global']['mqtt-server'], port=config['global']['mqtt-port'], keepalive=60);
client.loop_start();
return client, mqtt_handler
def setup():
config_file = "config.yaml"
# Read the configuration file
configuration = config_reader(config_file)
if configuration.read_settings() == 0:
sys.exit()
print("Succesfully read the configuration from file: " + config_file)
# show values from config file
if configuration.config_file_settings['global']['program-log'] == 0:
print("Program output will not be logged to a file.")
else:
print("Program output will be logged to file: " + configuration.config_file_settings['global']['program-log'])
# Enable the logging module. Log to file if enabled in configuration file, otherwise log to stand i/o (probably the screen)
logging.basicConfig(
level=logging.DEBUG,
format="{asctime} - {levelname} - {message}",
style="{",
datefmt="%Y-%m-%d %H:%M",
filename=configuration.config_file_settings['global']['program-log'],
)
logging.info("Connecting to MQTT broker: " + configuration.config_file_settings['global']['mqtt-server'])
# Start MQTT section
mqtt_connected = False
while not mqtt_connected:
try:
mqtt_client, mqtt_handler = start_mqtt(configuration.config_file_settings)
mqtt_connected = True
except:
logging.error("Could not connect to MQTT broker. Retry until success (of until CTRL-C is pressed).")
time.sleep(3) # Sleep for 3 seconds
subscribe_to_mqtt(configuration.config_file_settings, mqtt_client)
# End MQTT section
return configuration, mqtt_client, mqtt_handler
def subscribe_to_mqtt(configuration, client):
for index, entry in enumerate(configuration['mqtt']['subscribe']):
logging.debug("Subscribed to MQTT topic: " + str(entry) )
client.subscribe(topic=str(entry));
def send_aprs_weather_report(WxData, configuration):
# Convert sensible SI values to freedom units for APRS weather report
wind_direction = int(float(WxData[0].decode('utf-8')))
wind_speed = int(2.2369 * float(WxData[1].decode('utf-8')))
wind_gust = int(2.2369 * float(WxData[2].decode('utf-8')))
rain_lasthour = int(3.93700787 * float(WxData[3].decode('utf-8')))
#rain_lasthour = 0
rain_24hour = int(3.93700787 * float(WxData[4].decode('utf-8')))
#rain_24hour = 0
temperature = int(float(WxData[5].decode('utf-8')) * 1.8 + 32)
humidity = int(float(WxData[6].decode('utf-8')))
if (humidity == 100):
humidity = 0;
pressure =int(10 * float(WxData[7].decode('utf-8')))
luminosity = int(0.0079 * float(WxData[8].decode('utf-8'))) # W/m2: various sources give 0.0079 or 0.0083 as an approxmation for sunlight
if (luminosity <= 999):
APRS_Lum = 'L'
else:
APRS_Lum = 'l'
luminosity = luminosity-1000
# Get date and time
timestamp = time.strftime("%d%H%M", gmtime())
# Construct APRS weather report
aprs_position = configuration['position']
aprs_wx_report = '@' + timestamp + 'z' + configuration['position'] + "{:03d}".format(wind_direction) + '/' + "{:03d}".format(wind_speed) + 'g' + "{:03d}".format(wind_gust) + 't' + "{:03d}".format(temperature) + 'r' + "{:03d}".format(rain_lasthour) + 'p' + "{:03d}".format(rain_24hour) + 'h' + "{:02d}".format(humidity) + 'b' + "{:05d}".format(pressure) + APRS_Lum + "{:03d}".format(luminosity)
# Send it
logging.debug(aprs_wx_report)
# Define the Bash script and its arguments as a list
script = "/usr/sbin/beacon"
if configuration['digipath'] == 0:
arguments = ["-c", f"{configuration['from_call']}", "-d", f"{configuration['destination']}", "-s", f"{configuration['port']}", f"{aprs_wx_report}"]
else:
#arguments = ["-d", f"{configuration['destination']} {configuration['digipath']}", "-s", f"{configuration['port']}", f"{aprs_wx_report}"]
arguments = ["-c", f"{configuration['from_call']}", "-d", f"{configuration['destination']} {configuration['digipath']}", "-s", f"{configuration['port']}", f"{aprs_wx_report}"]
# Combine the script and its arguments into a single list
command = [script] + arguments
# Run the script
logging.debug(command)
try:
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Check the result
if result.returncode == 0:
logging.info("Send data to APRS radio.")
else:
logging.error("Failed to send data to APRS radio.")
logging.error(f"Reason: {result.stderr}")
except Exception as e:
logging.error("Failed to send data to APRS radio.")
logging.error(f"Command returned: {e}")
def main():
Configuration, MqttClient, mqtt_handler = setup()
LoopCounter = 0
while (1):
time.sleep(Configuration.config_file_settings['global']['telemetry-interval']) # Sleep for number of seconds set in config.yaml
# Send data to LoRa radio via external program (/usr/sbin/beacon). Make sure we use all radios defined in the configuration file.
for entry in Configuration.config_file_settings['aprs']:
send_aprs_weather_report(mqtt_handler.get_aprs_telemetry(), entry)
# We cannot send multiple APRS messages in a short period of time, so we wait 3 deconds between messages.
time.sleep(3)
if __name__ == '__main__':
main()

View File

@@ -13,6 +13,7 @@ global:
modbus: modbus:
#port: /dev/serial/by-path/platform-3f980000.usb-usb-0:1.3:1.0-port0 # USB port to which RS-485 dongle is connected #port: /dev/serial/by-path/platform-3f980000.usb-usb-0:1.3:1.0-port0 # USB port to which RS-485 dongle is connected
port: /dev/ttyUSB0 port: /dev/ttyUSB0
poll_speed: 60 # Polling speed in seconds
# Modbus servers settings # Modbus servers settings
modbus_servers: modbus_servers:

View File

@@ -70,6 +70,7 @@ class config_reader:
# Test is all expected settings are present # Test is all expected settings are present
try: try:
tmp = self.config_file_settings['modbus']['port'] tmp = self.config_file_settings['modbus']['port']
tmp = self.config_file_settings['modbus']['poll_speed']
except: except:
print ("Error in the modbus section of the configuration file.") print ("Error in the modbus section of the configuration file.")
return 0 return 0

View File

@@ -175,7 +175,7 @@ def data_logger(data, configuration):
except: except:
logging.warning("Could not write to file: " + new_filename) logging.warning("Could not write to file: " + new_filename)
def send_data_to_mqtt(data, configuration, modbus_registers, mqtt_client): def send_data_to_mqtt(data, configuration, modbus_registers, MqttClient):
mqtt_top_topic = [] mqtt_top_topic = []
mqtt_full_topic = [] mqtt_full_topic = []
@@ -208,8 +208,7 @@ def send_data_to_mqtt(data, configuration, modbus_registers, mqtt_client):
# Go through every input register and match the unit and description with the value # Go through every input register and match the unit and description with the value
for index2, entry2 in enumerate(data['InputRegisters']): for index2, entry2 in enumerate(data['InputRegisters']):
# Scale values # Scale values
entry2 = entry2/ (10^entry1['input_register_names'][index2][2]) entry2 = entry2/ pow(10, entry1['input_register_names'][index2][2])
message_topic = entry1['input_register_names'][index2][0] message_topic = entry1['input_register_names'][index2][0]
mqtt_message = str(round(entry2,1)) mqtt_message = str(round(entry2,1))
mqtt_full_topic = mqtt_top_topic + message_topic mqtt_full_topic = mqtt_top_topic + message_topic
@@ -234,68 +233,71 @@ def ReconnectModBus(configuration):
logging.info("Reconnected to ModBus dongle.") logging.info("Reconnected to ModBus dongle.")
Configuration, Controller, ModbusAddresses, ModbusRegisters, MqttClient = setup() def main():
LoopCounter = 0
while (1): Configuration, Controller, ModbusAddresses, ModbusRegisters, MqttClient = setup()
time.sleep(3) # Sleep for 3 seconds LoopCounter = 0
ModBusData={} while (1):
time.sleep(Configuration.config_file_settings['modbus']['poll_speed']) # Sleep for number of seconds given in config.yaml
# Loop through all configured ModBus devices and try to read the sensor data ModBusData={}
for index, current_modbus_device in enumerate(ModbusAddresses):
logging.debug("Reading device on ModBus address: " + str(current_modbus_device))
try: # Loop through all configured ModBus devices and try to read the sensor data
ModBusData['DateTime'] = datetime.datetime.now().replace(microsecond=0).isoformat() for index, current_modbus_device in enumerate(ModbusAddresses):
ModBusData['ID'] = Controller[index].get_id() logging.debug("Reading device on ModBus address: " + str(current_modbus_device))
if ModBusData['ID'][0] == 0x4D45:
ModBusData['Type'] = Controller[index].get_type()
ModBusData['TypeString'] = Controller[index].get_type_string()
ModBusData['InputRegisters'] = Controller[index].read_all_input_registers()
# Keep the watchdog from resetting the ModBusdevice
Controller[index].toggle_watchdog()
NoError = True
except minimalmodbus.NoResponseError:
# Handle communication timeout
logging.warning("No response from the instrument on ModBus address: " + str(current_modbus_device))
NoError = False
except minimalmodbus.InvalidResponseError:
# Handle invalid Modbus responses
logging.warning("Invalid response from the instrument on ModBus address: " + str(current_modbus_device))
NoError = False
except minimalmodbus.SlaveReportedException as e:
# Handle errors reported by the slave device
logging.error(f"The instrument reported an error: {e}")
NoError = False
except minimalmodbus.ModbusException as e:
# Handle all other Modbus-related errors
logging.error(f"Modbus error: {e}")
NoError = False
except Exception as e:
# Handle unexpected errors, probably I/O error in USB/serial
logging.error(f"Unexpected error: {e}" + ", serial port " + Configuration.config_file_settings['modbus']['port'] + " not available while reading device on ModBus address " + str(ModbusAddresses[index]) + ". Try to reconnect.")
Controller[index].serial.close()
ReconnectModBus(Configuration)
NoError = False
if NoError == True:
try: try:
logging.debug("Found Mees Electronics sensor on ModBus address " + str(current_modbus_device)) ModBusData['DateTime'] = datetime.datetime.now().replace(microsecond=0).isoformat()
logging.debug("Serial number: " + hex(ModBusData['ID'][1]) + " " + hex(ModBusData['ID'][2]) + " " + hex(ModBusData['ID'][3])) ModBusData['ID'] = Controller[index].get_id()
logging.debug("Device type: " + str(ModBusData['Type']) + " (" + ModBusData['TypeString'] + ")") if ModBusData['ID'][0] == 0x4D45:
logging.debug (ModBusData['InputRegisters']) ModBusData['Type'] = Controller[index].get_type()
#logging.debug (json.dumps(ModBusData, indent=1, sort_keys=False)) ModBusData['TypeString'] = Controller[index].get_type_string()
ModBusData['InputRegisters'] = Controller[index].read_all_input_registers()
except: # Keep the watchdog from resetting the ModBusdevice
logging.warning("Modbus device type " + str(ModBusData['Type']) + " not found in register definition file. Ignoring sensor data.") Controller[index].toggle_watchdog()
# Log sensor data to file if enabled in configuration file NoError = True
data_logger(ModBusData, Configuration) except minimalmodbus.NoResponseError:
# Handle communication timeout
logging.warning("No response from the instrument on ModBus address: " + str(current_modbus_device))
NoError = False
except minimalmodbus.InvalidResponseError:
# Handle invalid Modbus responses
logging.warning("Invalid response from the instrument on ModBus address: " + str(current_modbus_device))
NoError = False
except minimalmodbus.SlaveReportedException as e:
# Handle errors reported by the slave device
logging.error(f"The instrument reported an error: {e}")
NoError = False
except minimalmodbus.ModbusException as e:
# Handle all other Modbus-related errors
logging.error(f"Modbus error: {e}")
NoError = False
except Exception as e:
# Handle unexpected errors, probably I/O error in USB/serial
logging.error(f"Unexpected error: {e}" + ", serial port " + Configuration.config_file_settings['modbus']['port'] + " not available while reading device on ModBus address " + str(ModbusAddresses[index]) + ". Try to reconnect.")
Controller[index].serial.close()
ReconnectModBus(Configuration)
NoError = False
# Send sensor data to MQTT broker if NoError == True:
send_data_to_mqtt(ModBusData, Configuration, ModbusRegisters.definition_file_data, MqttClient) try:
logging.debug("Found Mees Electronics sensor on ModBus address " + str(current_modbus_device))
logging.debug("Serial number: " + hex(ModBusData['ID'][1]) + " " + hex(ModBusData['ID'][2]) + " " + hex(ModBusData['ID'][3]))
logging.debug("Device type: " + str(ModBusData['Type']) + " (" + ModBusData['TypeString'] + ")")
#logging.debug (ModBusData['InputRegisters'])
#logging.debug (json.dumps(ModBusData, indent=1, sort_keys=False))
except:
logging.warning("Modbus device type " + str(ModBusData['Type']) + " not found in register definition file. Ignoring sensor data.")
# Log sensor data to file if enabled in configuration file
data_logger(ModBusData, Configuration)
# Send sensor data to MQTT broker
send_data_to_mqtt(ModBusData, Configuration, ModbusRegisters.definition_file_data, MqttClient)
if __name__ == '__main__':
main()