From 198836fa13d885c767e073f646f7bd1c976538d8 Mon Sep 17 00:00:00 2001 From: PaulVua Date: Tue, 17 Feb 2026 11:04:45 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20int=C3=A9gration=20capteur=20CO2=20MH-Z?= =?UTF-8?q?19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Scripts MH-Z19/get_data.py (lecture standalone) et write_data.py (écriture SQLite) - Table data_MHZ19, config MHZ19, cleanup et service systemd (120s) - Web UI : carte test sensors, checkbox admin, boutons database + CSV download - SARA_send_data_v2.py non modifié (sera fait dans un second temps) Co-Authored-By: Claude Opus 4.6 --- MH-Z19/get_data.py | 58 +++++++++++++++++++++++++++++++++ MH-Z19/write_data.py | 66 ++++++++++++++++++++++++++++++++++++++ html/admin.html | 9 ++++++ html/database.html | 19 +++++++++-- html/launcher.php | 19 +++++++++-- html/sensors.html | 53 ++++++++++++++++++++++++++++++ services/setup_services.sh | 34 +++++++++++++++++++- sqlite/create_db.py | 8 +++++ sqlite/flush_old_data.py | 4 ++- sqlite/set_config.py | 1 + 10 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 MH-Z19/get_data.py create mode 100644 MH-Z19/write_data.py diff --git a/MH-Z19/get_data.py b/MH-Z19/get_data.py new file mode 100644 index 0000000..b43fb86 --- /dev/null +++ b/MH-Z19/get_data.py @@ -0,0 +1,58 @@ +''' +Script to get CO2 values from MH-Z19 sensor +need parameter: CO2_port +/usr/bin/python3 /var/www/nebuleair_pro_4g/MH-Z19/get_data.py ttyAMA4 +''' + +import serial +import json +import sys +import time + +parameter = sys.argv[1:] +port = '/dev/' + parameter[0] + +ser = serial.Serial( + port=port, + baudrate=9600, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS, + timeout=1 +) + +READ_CO2_COMMAND = b'\xFF\x01\x86\x00\x00\x00\x00\x00\x79' + + +def read_co2(): + ser.write(READ_CO2_COMMAND) + time.sleep(2) + response = ser.read(9) + if len(response) < 9: + print("Error: No data or incomplete data received.") + return None + if response[0] == 0xFF: + co2_concentration = response[2] * 256 + response[3] + return co2_concentration + else: + print("Error reading data from sensor.") + return None + + +def main(): + try: + co2 = read_co2() + if co2 is not None: + data = {"CO2": co2} + json_data = json.dumps(data) + print(json_data) + else: + print("Failed to get CO2 data.") + except KeyboardInterrupt: + print("Program terminated.") + finally: + ser.close() + + +if __name__ == '__main__': + main() diff --git a/MH-Z19/write_data.py b/MH-Z19/write_data.py new file mode 100644 index 0000000..6cd2bbe --- /dev/null +++ b/MH-Z19/write_data.py @@ -0,0 +1,66 @@ +''' +Script to get CO2 values from MH-Z19 sensor and write to database +/usr/bin/python3 /var/www/nebuleair_pro_4g/MH-Z19/write_data.py +''' + +import serial +import json +import sys +import time +import sqlite3 + +conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db") +cursor = conn.cursor() + +mh_z19_port = "/dev/ttyAMA4" + +ser = serial.Serial( + port=mh_z19_port, + baudrate=9600, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS, + timeout=1 +) + +READ_CO2_COMMAND = b'\xFF\x01\x86\x00\x00\x00\x00\x00\x79' + + +def read_co2(): + ser.write(READ_CO2_COMMAND) + time.sleep(2) + response = ser.read(9) + if len(response) < 9: + print("Error: No data or incomplete data received from CO2 sensor.") + return None + if response[0] == 0xFF: + co2_concentration = response[2] * 256 + response[3] + return co2_concentration + else: + print("Error reading data from CO2 sensor.") + return None + + +def main(): + try: + co2 = read_co2() + if co2 is not None: + # Get RTC time from SQLite + cursor.execute("SELECT * FROM timestamp_table LIMIT 1") + row = cursor.fetchone() + rtc_time_str = row[1] + # Save to SQLite + cursor.execute('INSERT INTO data_MHZ19 (timestamp, CO2) VALUES (?, ?)', (rtc_time_str, co2)) + conn.commit() + print(f"CO2: {co2} ppm (saved at {rtc_time_str})") + else: + print("Failed to get CO2 data.") + except KeyboardInterrupt: + print("Program terminated.") + finally: + ser.close() + conn.close() + + +if __name__ == '__main__': + main() diff --git a/html/admin.html b/html/admin.html index 2874a4b..7a2d38d 100755 --- a/html/admin.html +++ b/html/admin.html @@ -118,6 +118,13 @@ +
+ + +
+
@@ -104,6 +105,7 @@ + @@ -358,7 +360,12 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "") Timestamp Curent LEQ DB_A_value - + + `; + }else if (table === "data_MHZ19") { + tableHTML += ` + Timestamp + CO2 (ppm) `; } @@ -433,7 +440,12 @@ function get_data_sqlite(table, limit, download , startDate = "", endDate = "") ${columns[0]} ${columns[1]} ${columns[2]} - + + `; + }else if (table === "data_MHZ19") { + tableHTML += ` + ${columns[0]} + ${columns[1]} `; } @@ -480,6 +492,9 @@ function downloadCSV(response, table) { else if (table === "data_NPM_5channels") { csvContent += "TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5\n"; } + else if (table === "data_MHZ19") { + csvContent += "TimestampUTC,CO2_ppm\n"; + } // Format rows as CSV rows.forEach(row => { diff --git a/html/launcher.php b/html/launcher.php index df83e7c..39945a8 100755 --- a/html/launcher.php +++ b/html/launcher.php @@ -542,7 +542,7 @@ if ($type == "db_table_stats") { $fileSizeMB = round($fileSizeBytes / (1024 * 1024), 2); // Sensor data tables to inspect - $tables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE']; + $tables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE', 'data_MHZ19']; $tableStats = []; foreach ($tables as $tableName) { @@ -589,7 +589,7 @@ if ($type == "download_full_table") { $table = $_GET['table'] ?? ''; // Whitelist of allowed tables - $allowedTables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE']; + $allowedTables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE', 'data_MHZ19']; if (!in_array($table, $allowedTables)) { header('Content-Type: application/json'); @@ -605,7 +605,8 @@ if ($type == "download_full_table") { 'data_envea' => 'TimestampUTC,NO2,H2S,NH3,CO,O3,SO2', 'data_WIND' => 'TimestampUTC,Wind_speed_kmh,Wind_direction_V', 'data_MPPT' => 'TimestampUTC,Battery_voltage,Battery_current,Solar_voltage,Solar_power,Charger_status', - 'data_NOISE' => 'TimestampUTC,Current_LEQ,DB_A_value' + 'data_NOISE' => 'TimestampUTC,Current_LEQ,DB_A_value', + 'data_MHZ19' => 'TimestampUTC,CO2_ppm' ]; try { @@ -741,6 +742,12 @@ if ($type == "BME280") { echo $output; } +if ($type == "mhz19") { + $command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/MH-Z19/get_data.py ttyAMA4'; + $output = shell_exec($command); + echo $output; +} + if ($type == "table_mesure") { $table=$_GET['table']; @@ -1276,6 +1283,10 @@ if ($type == "get_systemd_services") { 'description' => 'Get Data from noise sensor', 'frequency' => 'Every minute' ], + 'nebuleair-mhz19-data.timer' => [ + 'description' => 'Reads CO2 concentration from MH-Z19 sensor', + 'frequency' => 'Every 2 minutes' + ], 'nebuleair-db-cleanup-data.timer' => [ 'description' => 'Cleans up old data from database', 'frequency' => 'Daily' @@ -1341,6 +1352,7 @@ if ($type == "restart_systemd_service") { 'nebuleair-sara-data.timer', 'nebuleair-bme280-data.timer', 'nebuleair-mppt-data.timer', + 'nebuleair-mhz19-data.timer', 'nebuleair-db-cleanup-data.timer' ]; @@ -1401,6 +1413,7 @@ if ($type == "toggle_systemd_service") { 'nebuleair-sara-data.timer', 'nebuleair-bme280-data.timer', 'nebuleair-mppt-data.timer', + 'nebuleair-mhz19-data.timer', 'nebuleair-db-cleanup-data.timer' ]; diff --git a/html/sensors.html b/html/sensors.html index 7e39e83..dc17c64 100755 --- a/html/sensors.html +++ b/html/sensors.html @@ -285,6 +285,36 @@ function getNoise_values(){ }); } +function getMHZ19_values(){ + console.log("Data from MH-Z19 CO2 sensor:"); + $("#loading_mhz19").show(); + + $.ajax({ + url: 'launcher.php?type=mhz19', + dataType: 'json', + method: 'GET', + success: function(response) { + console.log(response); + const tableBody = document.getElementById("data-table-body_mhz19"); + tableBody.innerHTML = ""; + $("#loading_mhz19").hide(); + + if (response.CO2 !== undefined) { + $("#data-table-body_mhz19").append(` + + CO2 + ${response.CO2} ppm + + `); + } + }, + error: function(xhr, status, error) { + console.error('AJAX request failed:', status, error); + $("#loading_mhz19").hide(); + } + }); + } + function getBME280_values(){ console.log("Data from I2C BME280:"); $("#loading_BME280").show(); @@ -462,6 +492,29 @@ error: function(xhr, status, error) { container.innerHTML += i2C_HTML; // Add the I2C card if condition is met } + //creates MH-Z19 CO2 card + if (config.MHZ19) { + const MHZ19_HTML = ` +
+
+
+ Port UART 4 +
+
+
MH-Z19 CO2
+

Capteur de dioxyde de carbone.

+ + + + +
+
+
+
`; + + container.innerHTML += MHZ19_HTML; + } + //Si on a des SONDES ENVEA connectée il faut faire un deuxième call dans la table envea_sondes_table //creates ENVEA debug card if (config.envea) { diff --git a/services/setup_services.sh b/services/setup_services.sh index 97f5e84..6eca439 100644 --- a/services/setup_services.sh +++ b/services/setup_services.sh @@ -205,6 +205,38 @@ AccuracySec=1s WantedBy=timers.target EOL +# Create service and timer files for MH-Z19 CO2 Data +cat > /etc/systemd/system/nebuleair-mhz19-data.service << 'EOL' +[Unit] +Description=NebuleAir MH-Z19 CO2 Data Collection Service +After=network.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/python3 /var/www/nebuleair_pro_4g/MH-Z19/write_data.py +User=root +WorkingDirectory=/var/www/nebuleair_pro_4g +StandardOutput=append:/var/www/nebuleair_pro_4g/logs/mhz19_service.log +StandardError=append:/var/www/nebuleair_pro_4g/logs/mhz19_service_errors.log + +[Install] +WantedBy=multi-user.target +EOL + +cat > /etc/systemd/system/nebuleair-mhz19-data.timer << 'EOL' +[Unit] +Description=Run NebuleAir MH-Z19 CO2 Data Collection every 120 seconds +Requires=nebuleair-mhz19-data.service + +[Timer] +OnBootSec=120s +OnUnitActiveSec=120s +AccuracySec=1s + +[Install] +WantedBy=timers.target +EOL + # Create service and timer files for Database Cleanup cat > /etc/systemd/system/nebuleair-db-cleanup-data.service << 'EOL' [Unit] @@ -294,7 +326,7 @@ systemctl daemon-reload # Enable and start all timers echo "Enabling and starting all services..." -for service in npm envea sara bme280 mppt db-cleanup noise; do +for service in npm envea sara bme280 mppt mhz19 db-cleanup noise; do systemctl enable nebuleair-$service-data.timer systemctl start nebuleair-$service-data.timer echo "Started nebuleair-$service-data timer" diff --git a/sqlite/create_db.py b/sqlite/create_db.py index 16f9fbf..ecb3e74 100755 --- a/sqlite/create_db.py +++ b/sqlite/create_db.py @@ -136,6 +136,14 @@ CREATE TABLE IF NOT EXISTS data_NOISE ( ) """) +# Create a table MHZ19 (CO2 sensor) +cursor.execute(""" +CREATE TABLE IF NOT EXISTS data_MHZ19 ( + timestamp TEXT, + CO2 REAL +) +""") + # Commit and close the connection conn.commit() conn.close() diff --git a/sqlite/flush_old_data.py b/sqlite/flush_old_data.py index 3bebec1..fb69617 100755 --- a/sqlite/flush_old_data.py +++ b/sqlite/flush_old_data.py @@ -22,6 +22,7 @@ timestamp_table data_MPPT data_NOISE data_WIND +data_MHZ19 ''' @@ -124,7 +125,8 @@ def main(): "data_envea", "data_WIND", "data_MPPT", - "data_NOISE" + "data_NOISE", + "data_MHZ19" ] # Check which tables actually exist diff --git a/sqlite/set_config.py b/sqlite/set_config.py index 863a588..35e04cc 100644 --- a/sqlite/set_config.py +++ b/sqlite/set_config.py @@ -50,6 +50,7 @@ config_entries = [ ("BME280", "1", "bool"), ("MPPT", "0", "bool"), ("NOISE", "0", "bool"), + ("MHZ19", "0", "bool"), ("modem_version", "XXX", "str"), ("device_type", "nebuleair_pro", "str"), ("language", "fr", "str"),