From 1037207df3b7651e6867740200d8624033b3c87f Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 13 Mar 2025 11:39:40 +0100 Subject: [PATCH] update --- MPPT/read.py | 219 ++++++++++++++++++++++++++++++++++++-- config.json.dist | 2 + loop/SARA_send_data_v2.py | 41 ++++++- master.py | 1 + sqlite/create_db.py | 22 +++- sqlite/read.py | 1 + 6 files changed, 272 insertions(+), 14 deletions(-) diff --git a/MPPT/read.py b/MPPT/read.py index 45cbf09..c3bd578 100644 --- a/MPPT/read.py +++ b/MPPT/read.py @@ -1,12 +1,215 @@ -import serial +''' + __ __ ____ ____ _____ + | \/ | _ \| _ \_ _| + | |\/| | |_) | |_) || | + | | | | __/| __/ | | + |_| |_|_| |_| |_| + +Chargeur solaire Victron MPPT interface UART -def read_vedirect(port='/dev/serial0', baudrate=19200): - ser = serial.Serial(port, baudrate, timeout=1) +typical response from uart: + +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, 2=Bulk, 3=Absorbtion, 4=Float) +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 + +# Connect to the SQLite database +conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db") +cursor = conn.cursor() + + +def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=20, max_attempts=3): + """ + Read and parse data from Victron MPPT controller with retry logic + Returns parsed data as a dictionary or None if all attempts fail + """ + required_keys = ['V', 'I', 'VPV', 'PPV', 'CS'] # Essential keys we need - while True: - line = ser.readline().decode('utf-8', errors='ignore').strip() - if line: - print(line) # Raw data from Victron + 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() + + while time.time() - start_time < timeout: + 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 we have a complete data block + if 'Checksum' in data: + # Check if we have all required keys + missing_keys = [key for key in required_keys if key not in data] + if not missing_keys: + ser.close() + return data + else: + print(f"Incomplete data, missing: {', '.join(missing_keys)}") + # Clear data and continue reading + data = {} + + # 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) + + except Exception as e: + print(f"Error on attempt {attempt+1}: {e}") + try: + ser.close() + except: + pass + + print("All attempts failed") + return None + +def parse_values(data): + """Convert string values to appropriate types""" + if not data: + return None + + parsed = {} + + # Define conversions for each key + conversions = { + 'PID': str, + 'FW': int, + 'SER#': str, + 'V': lambda x: float(x)/1000, # Convert mV to V + 'I': lambda x: float(x)/1000, # Convert mA to A + 'VPV': lambda x: float(x)/1000 if x != '---' else 0, # Convert mV to V + 'PPV': int, + 'CS': int, + 'MPPT': int, + '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 + } + + # Convert values according to their type + for key, value in data.items(): + if key in conversions: + try: + parsed[key] = conversions[key](value) + except (ValueError, TypeError): + 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", + 5: "Float", + 6: "Storage", + 7: "Equalize", + 9: "Inverting", + 11: "Power supply", + 245: "Starting-up", + 247: "Repeated absorption", + 252: "External control" + } + return status_map.get(cs_value, f"Unknown ({cs_value})") if __name__ == "__main__": - read_vedirect() + # Read data (with retry logic) + raw_data = read_vedirect() + + if raw_data: + # Parse data + 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) + 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) + + 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}") + else: + print("Invalid data: Battery voltage is zero or missing") + else: + print("Failed to parse data") + else: + print("No valid data received from MPPT controller") + + # Always close the connection + conn.close() \ No newline at end of file diff --git a/config.json.dist b/config.json.dist index bd614f1..b30e1af 100755 --- a/config.json.dist +++ b/config.json.dist @@ -5,6 +5,8 @@ "RTC/save_to_db.py": true, "BME280/get_data_v2.py": true, "envea/read_value_v2.py": false, + "MPPT/read.py": false, + "windMeter/read.py": false, "sqlite/flush_old_data.py": true, "deviceID": "XXXX", "npm_5channel": false, diff --git a/loop/SARA_send_data_v2.py b/loop/SARA_send_data_v2.py index da806c1..26029c5 100755 --- a/loop/SARA_send_data_v2.py +++ b/loop/SARA_send_data_v2.py @@ -49,6 +49,11 @@ CSV PAYLOAD (AirCarto Servers) 17 -> PM 5.0μm to 10μm quantity (Nb/L) 18 -> NPM temp inside 19 -> NPM hum inside + 20 -> battery_voltage + 21 -> battery_current + 22 -> solar_voltage + 23 -> solar_power + 24 -> charger_status JSON PAYLOAD (Micro-Spot Servers) Same as NebuleAir wifi @@ -115,7 +120,7 @@ if uptime_seconds < 120: sys.exit() #Payload CSV to be sent to data.nebuleair.fr -payload_csv = [None] * 25 +payload_csv = [None] * 30 #Payload JSON to be sent to uSpot payload_json = { "nebuleairid": "XXX", @@ -210,6 +215,9 @@ baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du device_id = config.get('deviceID', '').upper() #device ID en maj bme_280_config = config.get('BME280/get_data_v2.py', False) #présence du BME280 envea_cairsens= config.get('envea/read_value_v2.py', False) +mppt_charger= config.get('MPPT/read.py', False) +wind_meter= config.get('windMeter/read.py', False) + send_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr) send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot () selected_networkID = int(config.get('SARA_R4_neworkID', 0)) @@ -355,7 +363,7 @@ try: #NextPM 5 channels if npm_5channel: print("➡️Getting NextPM 5 channels values (last 6 measures)") - cursor.execute("SELECT * FROM data_NPM_5channels ORDER BY timestamp DESC LIMIT 6") + cursor.execute("SELECT * FROM data_NPM_5channels ORDER BY rowid DESC LIMIT 6") rows = cursor.fetchall() # Exclude the timestamp column (assuming first column is timestamp) data_values = [row[1:] for row in rows] # Exclude timestamp @@ -373,7 +381,7 @@ try: #BME280 if bme_280_config: print("➡️Getting BME280 values") - cursor.execute("SELECT * FROM data_BME280 ORDER BY timestamp DESC LIMIT 1") + cursor.execute("SELECT * FROM data_BME280 ORDER BY rowid DESC LIMIT 1") last_row = cursor.fetchone() if last_row: print("SQLite DB last available row:", last_row) @@ -396,7 +404,7 @@ try: #envea if envea_cairsens: print("➡️Getting envea cairsens values") - cursor.execute("SELECT * FROM data_envea ORDER BY timestamp DESC LIMIT 6") + cursor.execute("SELECT * FROM data_envea ORDER BY rowid DESC LIMIT 6") rows = cursor.fetchall() # Exclude the timestamp column (assuming first column is timestamp) data_values = [row[1:] for row in rows] # Exclude timestamp @@ -420,6 +428,31 @@ try: payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(averages[1])}) payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NH3", "value": str(averages[2])}) + #Wind meter + if wind_meter: + print("➡️Getting wind meter values") + + #MPPT charger + if mppt_charger: + print("➡️Getting MPPT charger values") + cursor.execute("SELECT * FROM data_MPPT ORDER BY rowid DESC LIMIT 1") + last_row = cursor.fetchone() + if last_row: + print("SQLite DB last available row:", last_row) + battery_voltage = last_row[1] + battery_current = last_row[2] + solar_voltage = last_row[3] + solar_power = last_row[4] + charger_status = last_row[5] + + #Add data to payload CSV + payload_csv[20] = battery_voltage + payload_csv[21] = battery_current + payload_csv[22] = solar_voltage + payload_csv[23] = solar_power + payload_csv[24] = charger_status + else: + print("No data available in the database.") print("Verify SARA R4 connection") diff --git a/master.py b/master.py index 9ffc07d..24a94ce 100755 --- a/master.py +++ b/master.py @@ -86,6 +86,7 @@ SCRIPTS = [ ("envea/read_value_v2.py", 10, 0), # Get NPM data (modbus 5 channels) every 10s, with 2s delay ("loop/SARA_send_data_v2.py", 60, 1), # Send data every 60 seconds, with 2s delay ("BME280/get_data_v2.py", 120, 0), # Get BME280 data every 120 seconds, no delay + ("MPPT/read.py", 120, 0), # Get MPPT data every 120 seconds, no delay ("sqlite/flush_old_data.py", 86400, 0) # flush old data inside db every day () ] diff --git a/sqlite/create_db.py b/sqlite/create_db.py index aa0b060..8024cae 100755 --- a/sqlite/create_db.py +++ b/sqlite/create_db.py @@ -30,8 +30,6 @@ cursor.execute(""" VALUES (1, CURRENT_TIMESTAMP); """) - - # Create a table NPM cursor.execute(""" CREATE TABLE IF NOT EXISTS data_NPM ( @@ -78,6 +76,26 @@ CREATE TABLE IF NOT EXISTS data_NPM_5channels ( ) """) +# Create a table WIND +cursor.execute(""" +CREATE TABLE IF NOT EXISTS data_WIND ( + timestamp TEXT, + wind_speed REAL, + wind_direction REAL +) +""") + +# Create a table MPPT +cursor.execute(""" +CREATE TABLE IF NOT EXISTS data_MPPT ( + timestamp TEXT, + battery_voltage REAL, + battery_current REAL, + solar_voltage REAL, + solar_power REAL, + charger_status INTEGER +) +""") # Commit and close the connection diff --git a/sqlite/read.py b/sqlite/read.py index 39732dd..f6109d9 100755 --- a/sqlite/read.py +++ b/sqlite/read.py @@ -14,6 +14,7 @@ data_NPM_5channels data_BME280 data_envea timestamp_table +data_MPPT '''