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 = `
+
+
+
+
+
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"),