update
This commit is contained in:
215
MPPT/read.py
215
MPPT/read.py
@@ -1,12 +1,215 @@
|
||||
import serial
|
||||
'''
|
||||
__ __ ____ ____ _____
|
||||
| \/ | _ \| _ \_ _|
|
||||
| |\/| | |_) | |_) || |
|
||||
| | | | __/| __/ | |
|
||||
|_| |_|_| |_| |_|
|
||||
|
||||
def read_vedirect(port='/dev/serial0', baudrate=19200):
|
||||
Chargeur solaire Victron MPPT interface UART
|
||||
|
||||
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
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
print(f"Attempt {attempt+1} of {max_attempts}...")
|
||||
ser = serial.Serial(port, baudrate, timeout=1)
|
||||
|
||||
while True:
|
||||
# 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 line:
|
||||
print(line) # Raw data from Victron
|
||||
|
||||
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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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 ()
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,6 +14,7 @@ data_NPM_5channels
|
||||
data_BME280
|
||||
data_envea
|
||||
timestamp_table
|
||||
data_MPPT
|
||||
|
||||
'''
|
||||
|
||||
|
||||
Reference in New Issue
Block a user