update
This commit is contained in:
217
MPPT/read.py
217
MPPT/read.py
@@ -1,12 +1,215 @@
|
|||||||
|
'''
|
||||||
|
__ __ ____ ____ _____
|
||||||
|
| \/ | _ \| _ \_ _|
|
||||||
|
| |\/| | |_) | |_) || |
|
||||||
|
| | | | __/| __/ | |
|
||||||
|
|_| |_|_| |_| |_|
|
||||||
|
|
||||||
|
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 serial
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
def read_vedirect(port='/dev/serial0', baudrate=19200):
|
# Connect to the SQLite database
|
||||||
ser = serial.Serial(port, baudrate, timeout=1)
|
conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
while True:
|
|
||||||
line = ser.readline().decode('utf-8', errors='ignore').strip()
|
def read_vedirect(port='/dev/ttyAMA4', baudrate=19200, timeout=20, max_attempts=3):
|
||||||
if line:
|
"""
|
||||||
print(line) # Raw data from Victron
|
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)
|
||||||
|
|
||||||
|
# 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__":
|
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,
|
"RTC/save_to_db.py": true,
|
||||||
"BME280/get_data_v2.py": true,
|
"BME280/get_data_v2.py": true,
|
||||||
"envea/read_value_v2.py": false,
|
"envea/read_value_v2.py": false,
|
||||||
|
"MPPT/read.py": false,
|
||||||
|
"windMeter/read.py": false,
|
||||||
"sqlite/flush_old_data.py": true,
|
"sqlite/flush_old_data.py": true,
|
||||||
"deviceID": "XXXX",
|
"deviceID": "XXXX",
|
||||||
"npm_5channel": false,
|
"npm_5channel": false,
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ CSV PAYLOAD (AirCarto Servers)
|
|||||||
17 -> PM 5.0μm to 10μm quantity (Nb/L)
|
17 -> PM 5.0μm to 10μm quantity (Nb/L)
|
||||||
18 -> NPM temp inside
|
18 -> NPM temp inside
|
||||||
19 -> NPM hum 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)
|
JSON PAYLOAD (Micro-Spot Servers)
|
||||||
Same as NebuleAir wifi
|
Same as NebuleAir wifi
|
||||||
@@ -115,7 +120,7 @@ if uptime_seconds < 120:
|
|||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
#Payload CSV to be sent to data.nebuleair.fr
|
#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 to be sent to uSpot
|
||||||
payload_json = {
|
payload_json = {
|
||||||
"nebuleairid": "XXX",
|
"nebuleairid": "XXX",
|
||||||
@@ -210,6 +215,9 @@ baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du
|
|||||||
device_id = config.get('deviceID', '').upper() #device ID en maj
|
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
|
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)
|
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_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr)
|
||||||
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
|
send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot ()
|
||||||
selected_networkID = int(config.get('SARA_R4_neworkID', 0))
|
selected_networkID = int(config.get('SARA_R4_neworkID', 0))
|
||||||
@@ -355,7 +363,7 @@ try:
|
|||||||
#NextPM 5 channels
|
#NextPM 5 channels
|
||||||
if npm_5channel:
|
if npm_5channel:
|
||||||
print("➡️Getting NextPM 5 channels values (last 6 measures)")
|
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()
|
rows = cursor.fetchall()
|
||||||
# Exclude the timestamp column (assuming first column is timestamp)
|
# Exclude the timestamp column (assuming first column is timestamp)
|
||||||
data_values = [row[1:] for row in rows] # Exclude timestamp
|
data_values = [row[1:] for row in rows] # Exclude timestamp
|
||||||
@@ -373,7 +381,7 @@ try:
|
|||||||
#BME280
|
#BME280
|
||||||
if bme_280_config:
|
if bme_280_config:
|
||||||
print("➡️Getting BME280 values")
|
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()
|
last_row = cursor.fetchone()
|
||||||
if last_row:
|
if last_row:
|
||||||
print("SQLite DB last available row:", last_row)
|
print("SQLite DB last available row:", last_row)
|
||||||
@@ -396,7 +404,7 @@ try:
|
|||||||
#envea
|
#envea
|
||||||
if envea_cairsens:
|
if envea_cairsens:
|
||||||
print("➡️Getting envea cairsens values")
|
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()
|
rows = cursor.fetchall()
|
||||||
# Exclude the timestamp column (assuming first column is timestamp)
|
# Exclude the timestamp column (assuming first column is timestamp)
|
||||||
data_values = [row[1:] for row in rows] # Exclude 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_NO2", "value": str(averages[1])})
|
||||||
payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NH3", "value": str(averages[2])})
|
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")
|
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
|
("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
|
("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
|
("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 ()
|
("sqlite/flush_old_data.py", 86400, 0) # flush old data inside db every day ()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ cursor.execute("""
|
|||||||
VALUES (1, CURRENT_TIMESTAMP);
|
VALUES (1, CURRENT_TIMESTAMP);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Create a table NPM
|
# Create a table NPM
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS data_NPM (
|
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
|
# Commit and close the connection
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ data_NPM_5channels
|
|||||||
data_BME280
|
data_BME280
|
||||||
data_envea
|
data_envea
|
||||||
timestamp_table
|
timestamp_table
|
||||||
|
data_MPPT
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user