Enhance POE project with health check server, sensor tracking, and dynamic MQTT topic structure. Updated configuration for multiple Modbus hosts and added alerting system for sensor failures and recoveries. Improved logging and error handling throughout the application.
This commit is contained in:
305
sensor_bridge.py
305
sensor_bridge.py
@ -1,151 +1,282 @@
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
from pymodbus.exceptions import ModbusException
|
||||
|
||||
# Add MQTT client
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
# Import configuration
|
||||
from config import (
|
||||
MODBUS_HOST, MODBUS_PORT, UNIT_ID,
|
||||
MODBUS_HOSTS, MODBUS_PORT, UNIT_ID,
|
||||
MQTT_BROKER, MQTT_PORT, MQTT_TOPIC, MQTT_CLIENT_ID,
|
||||
MQTT_USERNAME, MQTT_PASSWORD, LOCATION, PUBLISH_INTERVAL
|
||||
MQTT_USERNAME, MQTT_PASSWORD, PUBLISH_INTERVAL
|
||||
)
|
||||
from sensor_tracker import get_sensor_tracker
|
||||
from health_check import HealthCheckServer
|
||||
|
||||
# Setting logging basic to see output
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# MQTT callbacks
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
"""Callback when MQTT client connects to broker"""
|
||||
if rc == 0:
|
||||
logging.info("Connected to MQTT Broker!")
|
||||
else:
|
||||
logging.error(f"Cannot connect to MQTT Broker. Return code: {rc}")
|
||||
|
||||
def on_publish(client, userdata, mid):
|
||||
"""Callback when MQTT message is published"""
|
||||
logging.info(f"Successfully sent message with ID: {mid}")
|
||||
|
||||
def read_and_publish_data(mqtt_client, modbus_client):
|
||||
def read_and_publish_data(mqtt_client, modbus_client, host_info):
|
||||
"""Read data from Modbus and publish to MQTT"""
|
||||
try:
|
||||
# Check connection to Modbus server
|
||||
# Check and establish Modbus connection
|
||||
if not modbus_client.is_socket_open():
|
||||
if not modbus_client.connect():
|
||||
logging.error("Cannot connect to Modbus server.")
|
||||
logging.info(f"Attempting to connect to {host_info['ip']}:{MODBUS_PORT}")
|
||||
connection_result = modbus_client.connect()
|
||||
if not connection_result:
|
||||
logging.error(f"Failed to connect to Modbus server {host_info['ip']}. Connection returned: {connection_result}")
|
||||
return False
|
||||
logging.info(f"Successfully connected to {host_info['ip']}:{MODBUS_PORT}")
|
||||
|
||||
# Handle different sensor types
|
||||
if host_info["type"] == "cwt_co2":
|
||||
return read_and_publish_cwt_co2(mqtt_client, modbus_client, host_info)
|
||||
else:
|
||||
return read_and_publish_temperature_humidity(mqtt_client, modbus_client, host_info)
|
||||
|
||||
except ModbusException as e:
|
||||
logging.error(f"Modbus error from {host_info['ip']}: {e}", exc_info=True)
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error from {host_info['ip']}: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def read_and_publish_temperature_humidity(mqtt_client, modbus_client, host_info):
|
||||
"""Read temperature and humidity sensors"""
|
||||
try:
|
||||
# Read temperature (register 0)
|
||||
result_temp = modbus_client.read_holding_registers(address=0, count=1, slave=UNIT_ID)
|
||||
if not hasattr(result_temp, 'registers') or not result_temp.registers:
|
||||
logging.error(f"Error reading temperature from {host_info['ip']}: {result_temp}")
|
||||
return False
|
||||
raw_temp = result_temp.registers[0]
|
||||
temperature = (125 - (-40)) * raw_temp / 1650 - 40 # Correct formula: -40°C to 125°C
|
||||
logging.info(f"Raw temperature from {host_info['ip']}: {raw_temp}, Corrected: {temperature:.1f}°C")
|
||||
|
||||
# Read humidity (register 1)
|
||||
result_hum = modbus_client.read_holding_registers(address=1, count=1, slave=UNIT_ID)
|
||||
|
||||
# Initialize data to publish
|
||||
data = {
|
||||
"time": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"location": LOCATION
|
||||
}
|
||||
|
||||
# Process temperature
|
||||
if hasattr(result_temp, 'registers') and result_temp.registers:
|
||||
raw_temp = result_temp.registers[0]
|
||||
raw_temperature = raw_temp * 0.1
|
||||
temperature = raw_temperature - 40
|
||||
logging.info(f"Raw temperature: {raw_temperature:.1f}°C (raw: {raw_temp})")
|
||||
logging.info(f"Corrected temperature: {temperature:.1f}°C")
|
||||
data["temperature"] = round(temperature, 1)
|
||||
else:
|
||||
logging.error(f"Error reading temperature: {result_temp}")
|
||||
if not hasattr(result_hum, 'registers') or not result_hum.registers:
|
||||
logging.error(f"Error reading humidity from {host_info['ip']}: {result_hum}")
|
||||
return False
|
||||
raw_hum = result_hum.registers[0]
|
||||
humidity = raw_hum * 100 / 1000 # Correct formula: 0% to 100% RH
|
||||
logging.info(f"Raw humidity from {host_info['ip']}: {raw_hum}, Corrected: {humidity:.1f}%RH")
|
||||
|
||||
# Xử lý độ ẩm
|
||||
if hasattr(result_hum, 'registers') and result_hum.registers:
|
||||
raw_hum = result_hum.registers[0]
|
||||
humidity = raw_hum * 0.1
|
||||
logging.info(f"Humidity: {humidity:.1f}%RH (raw: {raw_hum})")
|
||||
data["humidity"] = round(humidity, 1)
|
||||
else:
|
||||
logging.error(f"Error reading humidity: {result_hum}")
|
||||
return False
|
||||
|
||||
# Convert data to JSON with a better format
|
||||
# indent=2 creates whitespace and newlines for JSON
|
||||
payload = json.dumps(data, indent=2)
|
||||
logging.info(f"Publishing data: {payload}")
|
||||
result = mqtt_client.publish(MQTT_TOPIC, payload)
|
||||
# Prepare new topic structure: Location/{location_name}/{sensor_type}/data
|
||||
location = host_info["location"]
|
||||
sensor_type = "temperature-humidity"
|
||||
current_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Ensure data is sent
|
||||
# Create topic for combined data
|
||||
data_topic = f"Location/{location}/{sensor_type}/data"
|
||||
|
||||
# Create JSON payload with all data and status
|
||||
payload = {
|
||||
"timestamp": current_time,
|
||||
"location": location,
|
||||
"sensor_type": sensor_type,
|
||||
"ip": host_info["ip"],
|
||||
"status": "online",
|
||||
"data": {
|
||||
"temperature": round(temperature, 1),
|
||||
"humidity": round(humidity, 1)
|
||||
}
|
||||
}
|
||||
|
||||
# Publish combined data as JSON
|
||||
result = mqtt_client.publish(data_topic, json.dumps(payload))
|
||||
result.wait_for_publish()
|
||||
|
||||
logging.info(f"Published to '{data_topic}': {json.dumps(payload, indent=2)}")
|
||||
|
||||
# Check if published successfully
|
||||
if result.is_published():
|
||||
logging.info(f"Successfully published data to topic '{MQTT_TOPIC}'")
|
||||
logging.info(f"Successfully published temperature-humidity data for {location}")
|
||||
return True
|
||||
else:
|
||||
logging.error("Cannot publish data")
|
||||
logging.error(f"Failed to publish temperature-humidity data from {host_info['ip']}")
|
||||
return False
|
||||
|
||||
except ModbusException as e:
|
||||
logging.error(f"Modbus error from {host_info['ip']}: {e}", exc_info=True)
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Error in reading/writing data: {e}", exc_info=True)
|
||||
logging.error(f"Unexpected error from {host_info['ip']}: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def read_and_publish_cwt_co2(mqtt_client, modbus_client, host_info):
|
||||
"""Read CWT CO2 sensor (humidity, temperature, CO2)"""
|
||||
try:
|
||||
# Read all 3 registers at once (registers 0, 1, 2)
|
||||
# According to CWT manual: register 0=humidity, 1=temperature, 2=CO2
|
||||
result = modbus_client.read_holding_registers(address=0, count=3, slave=UNIT_ID)
|
||||
|
||||
if not hasattr(result, 'registers') or len(result.registers) != 3:
|
||||
logging.error(f"Error reading CWT registers from {host_info['ip']}: {result}")
|
||||
return False
|
||||
|
||||
raw_humidity = result.registers[0] # Register 0: Humidity (0.1%RH)
|
||||
raw_temperature = result.registers[1] # Register 1: Temperature (0.1°C)
|
||||
raw_co2 = result.registers[2] # Register 2: CO2 (1ppm)
|
||||
|
||||
logging.info(f"Raw CWT values from {host_info['ip']} - Humidity: {raw_humidity}, Temperature: {raw_temperature}, CO2: {raw_co2}")
|
||||
|
||||
# Process values according to CWT manual
|
||||
# Humidity: 0.1%RH resolution
|
||||
humidity = raw_humidity / 10.0
|
||||
|
||||
# Temperature: 0.1°C resolution, handle negative values (2's complement)
|
||||
if raw_temperature > 32767: # Negative temperature in 2's complement
|
||||
temperature = (raw_temperature - 65536) / 10.0
|
||||
else:
|
||||
temperature = raw_temperature / 10.0
|
||||
|
||||
# CO2: 1ppm resolution for standard sensor
|
||||
co2_ppm = raw_co2
|
||||
|
||||
logging.info(f"Processed CWT values from {host_info['ip']} - Humidity: {humidity:.1f}%RH, Temperature: {temperature:.1f}°C, CO2: {co2_ppm}ppm")
|
||||
|
||||
# Prepare new topic structure: Location/{location_name}/{sensor_type}/data
|
||||
location = host_info["location"]
|
||||
sensor_type = "CO2-gas"
|
||||
current_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Create topic for combined data
|
||||
data_topic = f"Location/{location}/{sensor_type}/data"
|
||||
|
||||
# Create JSON payload with all data and status
|
||||
payload = {
|
||||
"timestamp": current_time,
|
||||
"location": location,
|
||||
"sensor_type": sensor_type,
|
||||
"ip": host_info["ip"],
|
||||
"status": "online",
|
||||
"data": {
|
||||
"co2": co2_ppm,
|
||||
"temperature": round(temperature, 1),
|
||||
"humidity": round(humidity, 1)
|
||||
}
|
||||
}
|
||||
|
||||
# Publish combined data as JSON
|
||||
result = mqtt_client.publish(data_topic, json.dumps(payload))
|
||||
result.wait_for_publish()
|
||||
|
||||
logging.info(f"Published to '{data_topic}': {json.dumps(payload, indent=2)}")
|
||||
|
||||
# Check if published successfully
|
||||
if result.is_published():
|
||||
logging.info(f"Successfully published CO2-gas data for {location}")
|
||||
return True
|
||||
else:
|
||||
logging.error(f"Failed to publish CO2-gas data from {host_info['ip']}")
|
||||
return False
|
||||
|
||||
except ModbusException as e:
|
||||
logging.error(f"Modbus error from CWT sensor {host_info['ip']}: {e}", exc_info=True)
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error from CWT sensor {host_info['ip']}: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def main_loop():
|
||||
"""Main function to connect and publish data in cycles"""
|
||||
# Initialize sensor tracker and health check server
|
||||
sensor_tracker = get_sensor_tracker()
|
||||
health_server = HealthCheckServer()
|
||||
|
||||
# Initialize MQTT client
|
||||
mqtt_client = mqtt.Client(client_id=MQTT_CLIENT_ID)
|
||||
mqtt_client.on_connect = on_connect
|
||||
mqtt_client.on_publish = on_publish
|
||||
|
||||
# Set username and password if needed
|
||||
|
||||
if MQTT_USERNAME and MQTT_PASSWORD:
|
||||
mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
|
||||
|
||||
# Initialize Modbus TCP client
|
||||
modbus_client = ModbusTcpClient(
|
||||
host=MODBUS_HOST,
|
||||
port=MODBUS_PORT,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
# Start health check server
|
||||
health_server.start()
|
||||
|
||||
# Connect to MQTT broker
|
||||
logging.info(f"Connecting to MQTT broker {MQTT_BROKER}:{MQTT_PORT}...")
|
||||
mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)
|
||||
mqtt_client.loop_start()
|
||||
logging.info(f"Attempting to connect to Modbus TCP server: {MODBUS_HOST}:{MODBUS_PORT}...")
|
||||
|
||||
# Connect to Modbus server
|
||||
if not modbus_client.connect():
|
||||
logging.error("Cannot connect to Modbus server initially.")
|
||||
return
|
||||
|
||||
logging.info("Successfully connected to Modbus server.")
|
||||
logging.info(f"Starting data reading cycle every {PUBLISH_INTERVAL} seconds...")
|
||||
|
||||
try:
|
||||
# Main loop to read and publish data
|
||||
while True:
|
||||
success = read_and_publish_data(mqtt_client, modbus_client)
|
||||
logging.info(f"Starting monitoring of {len(MODBUS_HOSTS)} sensors")
|
||||
logging.info("System status can be monitored at:")
|
||||
logging.info(f" - Health: http://localhost:8080/health")
|
||||
logging.info(f" - Sensors: http://localhost:8080/sensors")
|
||||
|
||||
# Main loop to read and publish data from all hosts
|
||||
while True:
|
||||
for host_info in MODBUS_HOSTS:
|
||||
error_message = None
|
||||
try:
|
||||
modbus_client = ModbusTcpClient(
|
||||
host=host_info["ip"],
|
||||
port=MODBUS_PORT,
|
||||
timeout=10 # Reduced timeout from 30 to 10 seconds
|
||||
)
|
||||
logging.info(f"Processing channel {host_info['location']} at {host_info['ip']}:{MODBUS_PORT}")
|
||||
success = read_and_publish_data(mqtt_client, modbus_client, host_info)
|
||||
|
||||
if success:
|
||||
# Record successful reading
|
||||
sensor_tracker.record_success(host_info, mqtt_client)
|
||||
logging.info(f"Successfully processed {host_info['location']} ({host_info['ip']})")
|
||||
else:
|
||||
error_message = f"Failed to read/publish data from {host_info['ip']}"
|
||||
sensor_tracker.record_failure(host_info, error_message, mqtt_client)
|
||||
logging.warning(f"Failed to process {host_info['location']} ({host_info['ip']}), will retry next cycle.")
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Exception processing {host_info['ip']}: {str(e)}"
|
||||
sensor_tracker.record_failure(host_info, error_message, mqtt_client)
|
||||
logging.error(f"Error processing {host_info['location']} ({host_info['ip']}): {e}", exc_info=True)
|
||||
finally:
|
||||
try:
|
||||
modbus_client.close()
|
||||
logging.debug(f"Closed connection to {host_info['ip']}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Add small delay between processing each sensor
|
||||
time.sleep(1)
|
||||
|
||||
# Log system summary every cycle
|
||||
summary = sensor_tracker.get_summary()
|
||||
logging.info(f"Cycle completed - Online: {summary['online_sensors']}/{summary['total_sensors']} sensors "
|
||||
f"({summary['health_percentage']:.1f}% health), "
|
||||
f"Alerts: {summary['alerted_sensors']}")
|
||||
|
||||
if not success:
|
||||
logging.warning("Error occurred in current cycle, will retry in next cycle.")
|
||||
|
||||
# Wait for next publish cycle
|
||||
logging.info(f"Waiting {PUBLISH_INTERVAL} seconds until next publish...")
|
||||
time.sleep(PUBLISH_INTERVAL)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Received stop signal from user.")
|
||||
|
||||
logging.info(f"Waiting {PUBLISH_INTERVAL} seconds until next cycle...")
|
||||
time.sleep(PUBLISH_INTERVAL)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Received stop signal from user, shutting down...")
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error in main loop: {e}", exc_info=True)
|
||||
|
||||
finally:
|
||||
# Close Modbus connection
|
||||
modbus_client.close()
|
||||
logging.info("Successfully closed Modbus connection.")
|
||||
|
||||
# Cleanup
|
||||
try:
|
||||
health_server.stop()
|
||||
except:
|
||||
pass
|
||||
mqtt_client.loop_stop()
|
||||
mqtt_client.disconnect()
|
||||
logging.info("Successfully closed MQTT connection.")
|
||||
logging.info("Successfully closed all connections.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main_loop()
|
||||
|
Reference in New Issue
Block a user