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:
Naab2k3
2025-06-23 09:14:40 +07:00
parent ad87c01f34
commit 81ab6191da
15 changed files with 752 additions and 222 deletions

View File

@ -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()