ModBus software works, MQTT bridge software added.
This commit is contained in:
33
software/rs485_client_to_mqtt/README.md
Normal file
33
software/rs485_client_to_mqtt/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# RS485 ModBus client to MQTT bridge
|
||||
|
||||
Scans the configured ModBus for Mees Electronics sensors and publishes the sensor data to the configured MQTT broker.
|
||||
|
||||
The Mees Electronics sensors are almost plug-and-play. You just have to set the sensor to a unique address and add this address to the config.yaml file. The description entry in the YAML file is only there for convenience.
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit config.yaml.
|
||||
|
||||
The file modbus_registers.yaml contains the Mees Electronics register definitions of the various sensors. The newest definition file can be downloaded from the git repository.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python3
|
||||
- minimalmodbus
|
||||
- json
|
||||
- time
|
||||
- sys
|
||||
- logging
|
||||
- os
|
||||
- pathlib
|
||||
- paho.mqtt.client
|
||||
- pyyaml
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2025 M.T. Konstapel (https://meezenest.nl/mees)
|
||||
|
||||
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.
|
22
software/rs485_client_to_mqtt/config.yaml
Normal file
22
software/rs485_client_to_mqtt/config.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
# 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)
|
||||
data-log: /home/marcel/weather_station_data.log # All sensor data will be written to this file (0 = do not log to file). Every day the current date is added to the end of the filename, so every dat a new file is created.
|
||||
mqtt-server: mqtt.meezenest.nl
|
||||
mqtt-port: 1883
|
||||
mqtt-root-topic: mees_electronics
|
||||
|
||||
# ModBus hardware settings
|
||||
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/ttyUSB0
|
||||
|
||||
# Modbus servers settings
|
||||
modbus_servers:
|
||||
- address: 14 # ModBus address of weather station
|
||||
description: Dual temperature sensor
|
||||
- address: 1
|
||||
description: Dummy
|
88
software/rs485_client_to_mqtt/config_reader.py
Normal file
88
software/rs485_client_to_mqtt/config_reader.py
Normal file
@@ -0,0 +1,88 @@
|
||||
""""
|
||||
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_modbus_settings() == 0:
|
||||
return 0
|
||||
|
||||
if self.test_modbus_servers_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']['data-log']
|
||||
tmp = self.config_file_settings['global']['mqtt-server']
|
||||
tmp = self.config_file_settings['global']['mqtt-port']
|
||||
tmp = self.config_file_settings['global']['mqtt-root-topic']
|
||||
except:
|
||||
print ("Error in the global section of the configuration file.")
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
# Test if all settings are pressent
|
||||
def test_modbus_settings(self):
|
||||
# Test is all expected settings are present
|
||||
try:
|
||||
tmp = self.config_file_settings['modbus']['port']
|
||||
except:
|
||||
print ("Error in the modbus section of the configuration file.")
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def test_modbus_servers_settings(self):
|
||||
for entry in self.config_file_settings['modbus_servers']:
|
||||
try:
|
||||
tmp = entry['address']
|
||||
tmp = entry['description']
|
||||
except:
|
||||
print ("Error in the modbus_servers section of the configuration file.")
|
||||
return 0
|
||||
else:
|
||||
return 1
|
107
software/rs485_client_to_mqtt/modbus_control.py
Normal file
107
software/rs485_client_to_mqtt/modbus_control.py
Normal file
@@ -0,0 +1,107 @@
|
||||
""""
|
||||
ModBus control 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 minimalmodbus
|
||||
import serial
|
||||
|
||||
class ModBusController(minimalmodbus.Instrument):
|
||||
"""Instrument class for Epever Charge Controllers.
|
||||
|
||||
Args:
|
||||
* portname (str): port name
|
||||
* slaveaddress (int): slave address in the range 1 to 247
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, portname, slaveaddress):
|
||||
minimalmodbus.Instrument.__init__(self, portname, slaveaddress)
|
||||
self.serial.baudrate = 9600
|
||||
self.serial.bytesize = 8
|
||||
self.serial.parity = serial.PARITY_NONE
|
||||
self.serial.stopbits = 1
|
||||
self.serial.timeout = 0.3
|
||||
self.mode = minimalmodbus.MODE_RTU
|
||||
self.clear_buffers_before_each_transaction = True
|
||||
self.close_port_after_each_call = False
|
||||
|
||||
self.watchdog_toggle_bit = 1
|
||||
|
||||
# Read the device ID
|
||||
def get_id(self):
|
||||
device_id = [None]*4
|
||||
device_id[0] = self.read_register(1000, 0, 4, False)
|
||||
device_id[1] = self.read_register(1001, 0, 4, False)
|
||||
device_id[2] = self.read_register(1002, 0, 4, False)
|
||||
device_id[3] = self.read_register(1003, 0, 4, False)
|
||||
|
||||
return device_id
|
||||
|
||||
# Read the device type
|
||||
def get_type(self):
|
||||
device_type = self.read_register(1004, 0, 4, False)
|
||||
|
||||
return device_type
|
||||
|
||||
# Get the 40 character long description string from ModBus device
|
||||
def get_type_string(self):
|
||||
register_offset = 1006 # ModBus register for text string starts at 1006
|
||||
device_type_register = [None]*20 # ModBus register for text string is 20 integers long
|
||||
device_type_string = "" # Store the decoded string here
|
||||
|
||||
for index, entry in enumerate(device_type_register):
|
||||
# Read Type string registers
|
||||
device_type_register[index] = self.read_register(index + register_offset, 0, 4, False)
|
||||
# Convert to ASCII string
|
||||
device_type_string += chr((device_type_register[index] & 0xFF00) >> 8)
|
||||
device_type_string += chr(device_type_register[index] & 0xFF)
|
||||
|
||||
return device_type_string.strip()
|
||||
|
||||
# Ask the device how many input registers it has
|
||||
def get_number_of_input_registers(self):
|
||||
device_number_of_input_registers = self.read_register(1026, 0, 4, False)
|
||||
|
||||
return device_number_of_input_registers
|
||||
|
||||
# Read all the input registers
|
||||
def read_all_input_registers(self):
|
||||
|
||||
number_of_input_registers = self.get_number_of_input_registers()
|
||||
input_register = [None]*number_of_input_registers
|
||||
|
||||
for index in range(number_of_input_registers):
|
||||
input_register[index] = self.read_register(index, 0, 4, False)
|
||||
|
||||
return input_register
|
||||
|
||||
def set_watchdog(self):
|
||||
self.write_bit(0, 1, 5)
|
||||
|
||||
def reset_watchdog(self):
|
||||
self.write_bit(0, 0, 5)
|
||||
|
||||
# Toggle the devices watchdog
|
||||
def toggle_watchdog(self):
|
||||
if self.watchdog_toggle_bit:
|
||||
self.set_watchdog()
|
||||
self.watchdog_toggle_bit = 0
|
||||
else:
|
||||
self.reset_watchdog()
|
||||
self.watchdog_toggle_bit = 1
|
||||
|
||||
|
@@ -0,0 +1,56 @@
|
||||
""""
|
||||
ModBus definition file reader routines
|
||||
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 yaml
|
||||
from yaml.loader import SafeLoader
|
||||
|
||||
class definition_file_reader:
|
||||
|
||||
# initiate class: define name configuration files
|
||||
def __init__(self, definition_file):
|
||||
self.definition_file = definition_file
|
||||
|
||||
def read_settings(self):
|
||||
|
||||
if self.read_definition_file() == 0:
|
||||
return 0
|
||||
|
||||
if self.test_definition_file() == 0:
|
||||
return 0
|
||||
|
||||
return 1
|
||||
|
||||
def read_definition_file (self):
|
||||
try:
|
||||
with open(self.definition_file) as f:
|
||||
self.definition_file_data = yaml.load(f, Loader=SafeLoader)
|
||||
except:
|
||||
print ("Definition file ./" + self.definition_file + " not found or syntax error in file.")
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
# Test if all settings are pressent
|
||||
def test_definition_file(self):
|
||||
# Test is all expected settings are present
|
||||
try:
|
||||
tmp = self.definition_file_data['devices']
|
||||
except:
|
||||
print ("Error in the ModBus definition file.")
|
||||
return 0
|
||||
else:
|
||||
return 1
|
30
software/rs485_client_to_mqtt/modbus_registers.yaml
Normal file
30
software/rs485_client_to_mqtt/modbus_registers.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
# This file defines the Mees Electronincs ModBus device registers
|
||||
|
||||
devices:
|
||||
- device_type: 1
|
||||
input_registers: 6 # The number of available input registers, starting from offset 40
|
||||
input_register_names: # Description, unit
|
||||
- [Temperature A, °C]
|
||||
- [Temperature B , °C]
|
||||
- [Minimum temperature A, °C]
|
||||
- [Minimum temperature B, °C]
|
||||
- [Maximum temperature A, °C]
|
||||
- [Maximum temperature B, °C]
|
||||
- device_type: 2
|
||||
input_registers: 15 # The number of available input registers, starting from offset 40
|
||||
input_register_names: # Description, unit, scaling 0 = as is, 1 = decimal one position to the left, 2 = decimal two positions to the left, enz.
|
||||
- [weater_station_id, '', 0]
|
||||
- [wind_direction, °, 1]
|
||||
- [wind_speed, 'km/h', 2]
|
||||
- [wind_gust, 'km/h', 2]
|
||||
- [temperature, °C, 2]
|
||||
- [rain_last_hour, 'l/m2', 2]
|
||||
- [rain_last_24 hours, 'l/m2', 2]
|
||||
- [rain_since_midnight, 'l/m2', 2]
|
||||
- [humidity, '%', 2]
|
||||
- [barometric_pressure, hPa, 1]
|
||||
- [luminosity, 'W/m2', 0]
|
||||
- [snow_fall, NA, 0]
|
||||
- [raw_rainfall_counter, mm, 0]
|
||||
- [temperature_pressure_sensor, °C, 2]
|
||||
- [status_bits, '', 0]
|
10
software/rs485_client_to_mqtt/mqtt_callbacks.py
Normal file
10
software/rs485_client_to_mqtt/mqtt_callbacks.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import logging
|
||||
|
||||
def on_connect(client, userdata, flags, reason_code, properties=None):
|
||||
logging.info("Connected to MQTT broker.")
|
||||
|
||||
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}")
|
301
software/rs485_client_to_mqtt/rs485_client_to_mqtt.py
Normal file
301
software/rs485_client_to_mqtt/rs485_client_to_mqtt.py
Normal file
@@ -0,0 +1,301 @@
|
||||
""""
|
||||
ModBus client program. Scans the RS-485 ModBus for Mees Electronics
|
||||
sensors and publishes the sensor data to an MQTT broker.
|
||||
|
||||
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
|
||||
import sys
|
||||
import logging
|
||||
import minimalmodbus
|
||||
import datetime
|
||||
import os
|
||||
import random
|
||||
|
||||
from pathlib import Path
|
||||
from modbus_control import ModBusController
|
||||
from config_reader import config_reader
|
||||
from modbus_definition_file_reader import definition_file_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
|
||||
|
||||
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")
|
||||
|
||||
import mqtt_callbacks
|
||||
client.on_message = mqtt_callbacks.on_message;
|
||||
client.on_connect = mqtt_callbacks.on_connect;
|
||||
#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['mqtt-server'],
|
||||
port=config['mqtt-port'],
|
||||
clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY,
|
||||
properties=properties,
|
||||
keepalive=60);
|
||||
|
||||
elif version == '3':
|
||||
client.connect(config['mqtt-server'], port=config['mqtt-port'], keepalive=60);
|
||||
|
||||
client.loop_start();
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def setup():
|
||||
config_file = "config.yaml"
|
||||
definition_file = "modbus_registers.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'])
|
||||
|
||||
if configuration.config_file_settings['global']['data-log'] == 0:
|
||||
print("Sensor data will not be logged to a file.")
|
||||
else:
|
||||
print("Sensor data will be logged to file: " + configuration.config_file_settings['global']['data-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("Using RS485 port : " + configuration.config_file_settings['modbus']['port'])
|
||||
logging.info("Publishing sensor data on MQTT broker: " + configuration.config_file_settings['global']['mqtt-server'])
|
||||
|
||||
# Start MQTT section
|
||||
mqtt_connected = False
|
||||
while not mqtt_connected:
|
||||
try:
|
||||
mqtt_client = start_mqtt(configuration.config_file_settings['global'])
|
||||
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
|
||||
|
||||
# End MQTT section
|
||||
|
||||
# Start ModBus section
|
||||
|
||||
# Read the ModBus definition file
|
||||
modbus_registers = definition_file_reader(definition_file)
|
||||
|
||||
if modbus_registers.read_settings() == 0:
|
||||
sys.exit()
|
||||
|
||||
logging.info("Succesfully read the ModBus register definitions from file: " + definition_file)
|
||||
|
||||
controller = []
|
||||
modbus_addresses = []
|
||||
for index, entry in enumerate(configuration.config_file_settings['modbus_servers']):
|
||||
modbus_addresses.append(entry['address'])
|
||||
logging.info("Using device: " + entry['description'] + " on ModBus address: " + str(entry['address']))
|
||||
|
||||
# Open serial port and try to connect to all the ModBus devices from the configuration file
|
||||
try:
|
||||
controller.append(ModBusController(configuration.config_file_settings['modbus']['port'], modbus_addresses[index]))
|
||||
except:
|
||||
logging.error("Could not open serial port " + configuration.config_file_settings['modbus']['port'])
|
||||
sys.exit("Exiting")
|
||||
|
||||
# End ModBus section
|
||||
|
||||
return configuration, controller, modbus_addresses, modbus_registers, mqtt_client
|
||||
|
||||
def data_logger(data, configuration):
|
||||
|
||||
if configuration.config_file_settings['global']['data-log'] != 0:
|
||||
|
||||
# Add date to file name, so every day a new file is created
|
||||
filename = Path(configuration.config_file_settings['global']['data-log'])
|
||||
new_filename = '_'.join([filename.stem, datetime.datetime.today().strftime('%Y-%m-%d')])
|
||||
new_filename = ''.join([new_filename, filename.suffix])
|
||||
new_filename = '/'.join([str(filename.parent), new_filename])
|
||||
logging.debug(new_filename)
|
||||
|
||||
try:
|
||||
f = open(new_filename, 'a')
|
||||
json.dump(data, f)
|
||||
f.write("\n")
|
||||
f.close()
|
||||
except:
|
||||
logging.warning("Could not write to file: " + new_filename)
|
||||
|
||||
def send_data_to_mqtt(data, configuration, modbus_registers, mqtt_client):
|
||||
|
||||
mqtt_top_topic = []
|
||||
mqtt_full_topic = []
|
||||
#logging.debug(modbus_registers)
|
||||
# Match actual device on ModBus with definition in modbus_registers.yaml
|
||||
for index1, entry1 in enumerate(modbus_registers['devices']):
|
||||
if entry1['device_type'] == data['Type']:
|
||||
|
||||
# Format serial number for unique MQTT ID
|
||||
mqtt_type_topic = hex(data['ID'][0])[2:].zfill(4) + hex(data['ID'][1])[2:].zfill(4) + hex(data['ID'][2])[2:].zfill(4) + hex(data['ID'][3])[2:].zfill(4)
|
||||
mqtt_top_topic = configuration.config_file_settings['global']['mqtt-root-topic'] + "/" + mqtt_type_topic + "/"
|
||||
message_topic = "id"
|
||||
mqtt_message = "mees_electronics_" + mqtt_type_topic
|
||||
mqtt_full_topic = mqtt_top_topic + message_topic
|
||||
MqttClient.publish(mqtt_full_topic, mqtt_message, 2, properties = properties);
|
||||
logging.debug("Published message to MQTT broker: " + mqtt_full_topic + " = " + mqtt_message)
|
||||
|
||||
message_topic = "type"
|
||||
mqtt_message = str(data['Type'])
|
||||
mqtt_full_topic = mqtt_top_topic + message_topic
|
||||
MqttClient.publish(mqtt_full_topic, mqtt_message, 2, properties = properties);
|
||||
logging.debug("Published message to MQTT broker: " + mqtt_full_topic + " = " + mqtt_message)
|
||||
|
||||
message_topic = "type_string"
|
||||
mqtt_message = data['TypeString']
|
||||
mqtt_full_topic = mqtt_top_topic + message_topic
|
||||
MqttClient.publish(mqtt_full_topic, mqtt_message, 2, properties = properties);
|
||||
logging.debug("Published message to MQTT broker: " + mqtt_full_topic + " = " + mqtt_message)
|
||||
|
||||
# Go through every input register and match the unit and description with the value
|
||||
for index2, entry2 in enumerate(data['InputRegisters']):
|
||||
# Scale values
|
||||
entry2 = entry2/ (10^entry1['input_register_names'][index2][2])
|
||||
|
||||
message_topic = entry1['input_register_names'][index2][0]
|
||||
mqtt_message = str(round(entry2,1))
|
||||
mqtt_full_topic = mqtt_top_topic + message_topic
|
||||
MqttClient.publish(mqtt_full_topic, mqtt_message, 2, properties = properties);
|
||||
logging.debug("Published message to MQTT broker: " + mqtt_full_topic + " = " + mqtt_message)
|
||||
|
||||
logging.debug("Send data to MQTT broker.")
|
||||
|
||||
|
||||
# Lost serial port, try to reconnect. We can try to connect to any ModBus address, even if it does not exists. We are only trying to reconnect to the RS485 serial/USB dongle.
|
||||
def ReconnectModBus(configuration):
|
||||
Retrying = True
|
||||
while (Retrying):
|
||||
|
||||
logging.error("Try to reconnect to ModBus dongle")
|
||||
try:
|
||||
controller = ModBusController(configuration.config_file_settings['modbus']['port'], 0)
|
||||
Retrying = False
|
||||
except:
|
||||
logging.error("Serial port " + configuration.config_file_settings['modbus']['port'] + " not available. Retry in 10 seconds.")
|
||||
time.sleep(10)
|
||||
|
||||
logging.info("Reconnected to ModBus dongle.")
|
||||
|
||||
Configuration, Controller, ModbusAddresses, ModbusRegisters, MqttClient = setup()
|
||||
LoopCounter = 0
|
||||
|
||||
while (1):
|
||||
time.sleep(3) # Sleep for 3 seconds
|
||||
|
||||
ModBusData={}
|
||||
|
||||
# Loop through all configured ModBus devices and try to read the sensor data
|
||||
for index, current_modbus_device in enumerate(ModbusAddresses):
|
||||
logging.debug("Reading device on ModBus address: " + str(current_modbus_device))
|
||||
|
||||
try:
|
||||
ModBusData['DateTime'] = datetime.datetime.now().replace(microsecond=0).isoformat()
|
||||
ModBusData['ID'] = Controller[index].get_id()
|
||||
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:
|
||||
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)
|
||||
|
||||
|
Reference in New Issue
Block a user