From 6cd51911389bc8d5a5a9607cd54bfc45a1919d83 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 28 May 2025 15:40:53 +0200 Subject: [PATCH] update --- MPPT/read.py | 281 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 166 insertions(+), 115 deletions(-) diff --git a/MPPT/read.py b/MPPT/read.py index f2a2887..5727920 100644 --- a/MPPT/read.py +++ b/MPPT/read.py @@ -1,11 +1,12 @@ -''' +#!/usr/bin/env python3 +""" __ __ ____ ____ _____ | \/ | _ \| _ \_ _| | |\/| | |_) | |_) || | | | | | __/| __/ | | |_| |_|_| |_| |_| - -Chargeur solaire Victron MPPT interface UART + +MPPT Chargeur solaire Victron interface UART MPPT connections 5V / Rx / TX / GND @@ -13,113 +14,125 @@ RPI connection -- / GPIO9 / GPIO8 / GND * pas besoin de connecter le 5V (le GND uniquement) -typical response from uart: +Fixed version - properly handles continuous data stream +""" -PID 0xA075 ->product ID -FW 164 ->firmware version -SER# HQ2249VJV9W ->serial num - -V 13310 ->Battery voilatage in mV -I -130 ->Battery current in mA (negative means its discharging) -VPV 10 ->Solar Panel voltage -PPV 0 ->Solar Panel power (in W) -CS 0 ->Charger status: - 0=off (no charging), - 2=Bulk (Max current is being delivered to the battery), - 3=Absorbtion (battery is nearly full,voltage is held constant.), - 4=Float (Battery is fully charged, only maintaining charge) -MPPT 0 ->MPPT (Maximum Power Point Tracking) state: 0 = Off, 1 = Active, 2 = Not tracking -OR 0x00000001 -ERR 0 -LOAD ON -IL 100 -H19 18 ->historical data (Total energy absorbed in kWh) -H20 0 -> Total energy discharged in kWh -H21 0 -H22 9 -H23 92 -HSDS 19 -Checksum u - -sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/MPPT/read.py - -''' import serial import time import sqlite3 +import os + +# ===== LOGGING CONFIGURATION ===== +# Set to True to enable all print statements, False to run silently +DEBUG_MODE = False + +# Alternative: Use environment variable (can be set in systemd service) +# DEBUG_MODE = os.environ.get('MPPT_DEBUG', 'false').lower() == 'true' + +# Alternative: Check if running under systemd +# DEBUG_MODE = os.isatty(1) # True if running in terminal, False if systemd/cron + +# Alternative: Use different log levels +# LOG_LEVEL = "ERROR" # Options: "DEBUG", "INFO", "ERROR", "NONE" +# ================================= # Connect to the SQLite database conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db") cursor = conn.cursor() +# Logging function +def log(message, level="INFO"): + """Print message only if DEBUG_MODE is True""" + if DEBUG_MODE: + print(message) + # Alternative: could write to a log file instead + # with open('/var/log/mppt.log', 'a') as f: + # f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} [{level}] {message}\n") -def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=20, max_attempts=3): + +def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=10): """ - Read and parse data from Victron MPPT controller with retry logic - Returns parsed data as a dictionary or None if all attempts fail + Read and parse data from Victron MPPT controller + Returns parsed data as a dictionary """ required_keys = ['V', 'I', 'VPV', 'PPV', 'CS'] # Essential keys we need - for attempt in range(max_attempts): - try: - print(f"Attempt {attempt+1} of {max_attempts}...") - ser = serial.Serial(port, baudrate, timeout=1) - - # Initialize data dictionary and tracking variables - data = {} - start_time = time.time() - checksum_found = False - - while time.time() - start_time < timeout: + try: + log(f"Opening serial port {port} at {baudrate} baud...") + ser = serial.Serial(port, baudrate, timeout=1) + + # Clear any buffered data + ser.reset_input_buffer() + time.sleep(0.5) + + # Initialize data dictionary + data = {} + start_time = time.time() + lines_read = 0 + blocks_seen = 0 + + while time.time() - start_time < timeout: + try: line = ser.readline().decode('utf-8', errors='ignore').strip() if not line: continue - - # Check if line contains a key-value pair - if '\t' in line: - key, value = line.split('\t', 1) - data[key] = value - print(f"{key}: {value}") - else: - print(f"Info: {line}") - # Check if this is the checksum line (end of data block) - if line.lower().startswith('checksum') or 'checksum' in line.lower(): - checksum_found = True - # Check if we have a complete data block - if checksum_found: + lines_read += 1 + + # Check if this line contains tab-separated key-value pair + if '\t' in line: + parts = line.split('\t', 1) + if len(parts) == 2: + key, value = parts + data[key] = value + log(f"{key}: {value}") + + # Check for checksum line (end of block) + elif line.startswith('Checksum'): + blocks_seen += 1 + log(f"--- End of block {blocks_seen} ---") + # Check if we have all required keys missing_keys = [key for key in required_keys if key not in data] + if not missing_keys: - print("✓ Complete data block received!") + log(f"✓ Complete data block received after {lines_read} lines!") ser.close() return data else: - print(f"Incomplete data, missing: {', '.join(missing_keys)}") - # Reset and continue reading - data = {} - checksum_found = False - - # Timeout occurred - print(f"Timeout on attempt {attempt+1}: Could not get complete data") - ser.close() - - # Add small delay between attempts - if attempt < max_attempts - 1: - print("Waiting before next attempt...") - time.sleep(2) + log(f"Block {blocks_seen} incomplete, missing: {', '.join(missing_keys)}") + # Don't clear data, maybe we missed the beginning of first block + if blocks_seen > 1: + # If we've seen multiple blocks and still missing data, + # something is wrong + log("Multiple incomplete blocks, clearing data...") + data = {} - except Exception as e: - print(f"Error on attempt {attempt+1}: {e}") - try: - ser.close() - except: - pass + except UnicodeDecodeError as e: + log(f"Decode error: {e}", "ERROR") + continue + except Exception as e: + log(f"Error reading line: {e}", "ERROR") + continue + + # Timeout reached + log(f"Timeout after {timeout}s, read {lines_read} lines, saw {blocks_seen} blocks") + ser.close() + + # If we have some data but not all required keys, return what we have + if data and len(data) >= len(required_keys) - 1: + log("Returning partial data...") + return data + + except serial.SerialException as e: + log(f"Serial port error: {e}", "ERROR") + except Exception as e: + log(f"Unexpected error: {e}", "ERROR") - print("All attempts failed") return None + def parse_values(data): """Convert string values to appropriate types""" if not data: @@ -141,13 +154,13 @@ def parse_values(data): 'OR': str, 'ERR': int, 'LOAD': str, - 'IL': int, - 'H19': int, # Total energy absorbed in kWh - 'H20': int, # Total energy discharged in kWh - 'H21': int, - 'H22': int, - 'H23': int, - 'HSDS': int + 'IL': lambda x: float(x)/1000, # Convert mA to A + 'H19': float, # Total energy absorbed in kWh (already in kWh) + 'H20': float, # Total energy discharged in kWh + 'H21': int, # Maximum power today (W) + 'H22': float, # Energy generated today (kWh) + 'H23': int, # Maximum power yesterday (W) + 'HSDS': int # Day sequence number } # Convert values according to their type @@ -155,18 +168,19 @@ def parse_values(data): if key in conversions: try: parsed[key] = conversions[key](value) - except (ValueError, TypeError): + except (ValueError, TypeError) as e: + log(f"Conversion error for {key}={value}: {e}", "ERROR") parsed[key] = value # Keep as string if conversion fails else: parsed[key] = value return parsed + def get_charger_status(cs_value): """Convert CS numeric value to human-readable status""" status_map = { 0: "Off", - 1: "Low power mode", 2: "Fault", 3: "Bulk", 4: "Absorption", @@ -181,8 +195,22 @@ def get_charger_status(cs_value): } return status_map.get(cs_value, f"Unknown ({cs_value})") + +def get_mppt_status(mppt_value): + """Convert MPPT value to human-readable status""" + mppt_map = { + 0: "Off", + 1: "Voltage or current limited", + 2: "MPP Tracker active" + } + return mppt_map.get(mppt_value, f"Unknown ({mppt_value})") + + if __name__ == "__main__": - # Read data (with retry logic) + log("=== Victron MPPT Reader ===") + log(f"Started at: {time.strftime('%Y-%m-%d %H:%M:%S')}") + + # Read data raw_data = read_vedirect() if raw_data: @@ -190,42 +218,65 @@ if __name__ == "__main__": parsed_data = parse_values(raw_data) if parsed_data: - # Check if we have valid battery voltage - if parsed_data.get('V', 0) > 0: - print("\n===== MPPT Summary =====") - print(f"Battery: {parsed_data.get('V', 0):.2f}V, {parsed_data.get('I', 0):.2f}A") - print(f"Solar: {parsed_data.get('VPV', 0):.2f}V, {parsed_data.get('PPV', 0)}W") - print(f"Charger status: {get_charger_status(parsed_data.get('CS', 0))}") - - # Save to SQLite - cursor.execute("SELECT * FROM timestamp_table LIMIT 1") - row = cursor.fetchone() - rtc_time_str = row[1] - - # Extract values - battery_voltage = parsed_data.get('V', 0) + # Display summary + log("\n===== MPPT Status Summary =====") + log(f"Product: {parsed_data.get('PID', 'Unknown')} (FW: {parsed_data.get('FW', '?')})") + log(f"Serial: {parsed_data.get('SER#', 'Unknown')}") + log(f"\nBattery: {parsed_data.get('V', 0):.2f}V, {parsed_data.get('I', 0):.2f}A") + log(f"Solar Panel: {parsed_data.get('VPV', 0):.2f}V, {parsed_data.get('PPV', 0)}W") + log(f"Charger Status: {get_charger_status(parsed_data.get('CS', 0))}") + log(f"MPPT Status: {get_mppt_status(parsed_data.get('MPPT', 0))}") + log(f"Load Output: {parsed_data.get('LOAD', 'Unknown')}, {parsed_data.get('IL', 0):.2f}A") + log(f"\nToday's Energy: {parsed_data.get('H22', 0)}kWh (Max: {parsed_data.get('H21', 0)}W)") + log(f"Total Energy: {parsed_data.get('H19', 0)}kWh") + + # Validate critical values + battery_voltage = parsed_data.get('V', 0) + + if battery_voltage > 0: + # Get timestamp + try: + cursor.execute("SELECT * FROM timestamp_table LIMIT 1") + row = cursor.fetchone() + rtc_time_str = row[1] if row else time.strftime('%Y-%m-%d %H:%M:%S') + except: + rtc_time_str = time.strftime('%Y-%m-%d %H:%M:%S') + + # Extract values for database battery_current = parsed_data.get('I', 0) solar_voltage = parsed_data.get('VPV', 0) solar_power = parsed_data.get('PPV', 0) charger_status = parsed_data.get('CS', 0) - + + # Save to database try: cursor.execute(''' INSERT INTO data_MPPT (timestamp, battery_voltage, battery_current, solar_voltage, solar_power, charger_status) VALUES (?, ?, ?, ?, ?, ?)''', (rtc_time_str, battery_voltage, battery_current, solar_voltage, solar_power, charger_status)) - + conn.commit() - print("MPPT data saved successfully!") - - except Exception as e: - print(f"Database error: {e}") + log(f"\n✓ Data saved to database at {rtc_time_str}") + + except sqlite3.Error as e: + # Always log database errors regardless of DEBUG_MODE + if not DEBUG_MODE: + print(f"Database error: {e}") + else: + log(f"\n✗ Database error: {e}", "ERROR") + conn.rollback() else: - print("Invalid data: Battery voltage is zero or missing") + log("\n✗ Invalid data: Battery voltage is zero or missing", "ERROR") else: - print("Failed to parse data") + log("\n✗ Failed to parse data", "ERROR") else: - print("No valid data received from MPPT controller") + log("\n✗ No valid data received from MPPT controller", "ERROR") + log("\nPossible issues:") + log("- Check serial connection (TX/RX/GND)") + log("- Verify port is /dev/ttyAMA4") + log("- Ensure MPPT is powered on") + log("- Check baudrate (should be 19200)") # Always close the connection - conn.close() \ No newline at end of file + conn.close() + log("\nDone.") \ No newline at end of file