From 91cb3eaaea1258f5dccacc2f343e49d9338bc89b Mon Sep 17 00:00:00 2001 From: Wizznu Date: Mon, 16 Feb 2026 12:31:26 +0700 Subject: [PATCH] feat: Dynamic threshold system for sensor-based watering - Detect all threshold_* nodes from kontrol_1 - Support unlimited threshold configurations - Each threshold: batas_bawah, batas_atas, smart_mode, pot_aktif[], pompa settings - Smart mode: water until batas_atas reached - Fixed mode: water for fixed duration - Setup script: setup-firebase-threshold.js for auto Firebase initialization - Improved logging: show threshold ID, mode, and pot info on trigger --- setup-firebase-threshold.js | 108 ++++++++++++++++++++++++++++++ worker.js | 127 +++++++++++++++++++++++------------- 2 files changed, 188 insertions(+), 47 deletions(-) create mode 100644 setup-firebase-threshold.js diff --git a/setup-firebase-threshold.js b/setup-firebase-threshold.js new file mode 100644 index 0000000..437148c --- /dev/null +++ b/setup-firebase-threshold.js @@ -0,0 +1,108 @@ +/** + * Script untuk setup Firebase Threshold System + * Run: node setup-firebase-threshold.js + */ + +require('dotenv').config(); +const admin = require('firebase-admin'); + +// Initialize Firebase Admin +try { + admin.initializeApp({ + credential: admin.credential.cert({ + projectId: process.env.FIREBASE_PROJECT_ID, + clientEmail: process.env.FIREBASE_CLIENT_EMAIL, + privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'), + }), + databaseURL: process.env.FIREBASE_DATABASE_URL, + }); + console.log('āœ… Firebase Admin initialized'); +} catch (error) { + console.error('āŒ Firebase initialization failed:', error.message); + process.exit(1); +} + +const db = admin.database(); + +// Data threshold yang akan di-setup +// Akan merge dengan data yang sudah ada (jadwal_1, jadwal_2, dll) +const thresholdData = { + threshold_1: { + aktif: true, + batas_bawah: 30, // Kelembaban minimum 30% + batas_atas: 70, // Target kelembaban 70% (untuk smart mode) + durasi: 600, // 10 menit (untuk fixed mode) + smart_mode: true, // Smart mode: siram sampai mencapai batas_atas + pot_aktif: [1, 2, 3], // Pot 1, 2, 3 pakai threshold ini + pompa_air: true, + pompa_pupuk: false + }, + + threshold_2: { + aktif: true, + batas_bawah: 40, // Kelembaban minimum 40% + batas_atas: 80, // Target kelembaban 80% + durasi: 300, // 5 menit + smart_mode: false, // Fixed mode: siram selama durasi saja + pot_aktif: [4, 5], // Pot 4, 5 pakai threshold ini + pompa_air: true, + pompa_pupuk: true + }, + + threshold_3: { + aktif: false, + batas_bawah: 25, // Untuk tanaman yang butuh kelembaban lebih tinggi + batas_atas: 75, + durasi: 480, // 8 menit + smart_mode: true, + pot_aktif: [3], // Hanya pot 3 + pompa_air: true, + pompa_pupuk: false + } +}; + +async function setupThreshold() { + try { + console.log('\nšŸ“¦ Starting Threshold System setup...'); + console.log('šŸ“ Target path: /kontrol_1\n'); + + // Get existing data di kontrol_1 + const existingSnapshot = await db.ref('kontrol_1').get(); + const existingData = existingSnapshot.val() || {}; + + console.log('šŸ“‹ Existing data keys:', Object.keys(existingData).join(', ')); + + // Merge threshold baru dengan data yang sudah ada + const mergedData = { + ...existingData, + ...thresholdData + }; + + // Push ke Firebase + await db.ref('kontrol_1').set(mergedData); + + console.log('\nāœ… Threshold system setup completed!'); + console.log('\nšŸ“Š Summary:'); + console.log(` Total keys in kontrol_1: ${Object.keys(mergedData).length}`); + console.log(` Jadwal nodes: ${Object.keys(mergedData).filter(k => k.startsWith('jadwal_')).length}`); + console.log(` Threshold nodes: ${Object.keys(mergedData).filter(k => k.startsWith('threshold_')).length}`); + + console.log('\nšŸŽÆ Threshold details:'); + Object.keys(thresholdData).forEach(key => { + const t = thresholdData[key]; + console.log(` ${key}: ${t.aktif ? 'āœ…' : 'āŒ'} | ${t.batas_bawah}-${t.batas_atas}% | ${t.smart_mode ? 'Smart' : 'Fixed'} | Pot ${t.pot_aktif.join(',')}`); + }); + + console.log('\n✨ Firebase updated successfully!'); + console.log('šŸ”— Check: https://console.firebase.google.com/u/0/project/YOUR_PROJECT/database'); + + process.exit(0); + } catch (error) { + console.error('\nāŒ Setup failed:', error.message); + console.error('Stack:', error.stack); + process.exit(1); + } +} + +// Run the setup +setupThreshold(); diff --git a/worker.js b/worker.js index 843b0de..ea0ad1c 100644 --- a/worker.js +++ b/worker.js @@ -787,7 +787,7 @@ setTimeout(async () => { let sensorCheckCounter = 0; async function setupSensorMonitoring() { - console.log('āœ… Sensor Mode monitoring started'); + console.log('āœ… Sensor Mode (Threshold System) monitoring started'); db.ref('data').on('value', async (snapshot) => { sensorCheckCounter++; @@ -802,65 +802,98 @@ async function setupSensorMonitoring() { const configSnapshot = await fetchWithTimeout(db.ref(FIREBASE_PATHS.kontrol), 10000); const kontrolConfig = configSnapshot.val(); - // Log sensor check (verbose hanya setiap 10 kali) - if (sensorCheckCounter % 10 === 0) { - console.log(`\nšŸŒ”ļø SENSOR CHECK #${sensorCheckCounter} | Mode Otomatis: ${kontrolConfig?.otomatis ? 'āœ…' : 'āŒ'}`); - } - - if (!kontrolConfig || !kontrolConfig.otomatis) { - // Sensor mode disabled + if (!kontrolConfig) { return; } - const batasBawah = kontrolConfig.batas_bawah || 40; - const batasAtas = kontrolConfig.batas_atas || 100; - const durasiSensor = kontrolConfig.durasi_sensor || 60; - const modeSensor = kontrolConfig.mode_sensor || 'fixed'; // 'fixed' or 'smart' + // Detect all threshold_* nodes + const allThresholds = Object.keys(kontrolConfig).filter(key => key.startsWith('threshold_')); - // Check each pot - for (let i = 1; i <= 5; i++) { - const soilKey = `soil_${i}`; - const soilValue = parseInt(sensorData[soilKey]) || 0; + // Log sensor check (verbose hanya setiap 10 kali) + if (sensorCheckCounter % 10 === 0) { + console.log(`\nšŸŒ”ļø SENSOR CHECK #${sensorCheckCounter} | Total Thresholds: ${allThresholds.length}`); + } - if (soilValue < batasBawah) { - const potKey = `pot_${i}`; - const lastTime = lastWateringTime[potKey]; + if (allThresholds.length === 0) { + // No thresholds configured + return; + } - // Debounce: minimum 2 menit antar penyiraman - if (lastTime && Date.now() - lastTime < config.worker.sensorDebounce) { - const remainingSeconds = Math.ceil((config.worker.sensorDebounce - (Date.now() - lastTime)) / 1000); - console.log(`ā³ POT ${i}: Cooldown active (${remainingSeconds}s remaining)`); - continue; - } + // Process each threshold + for (const thresholdKey of allThresholds) { + const threshold = kontrolConfig[thresholdKey]; + + // Skip if threshold is not active or invalid + if (!threshold || !threshold.aktif) continue; - console.log(`\nšŸŒ”ļø SENSOR TRIGGERED: POT ${i}`); - console.log(` Soil moisture: ${soilValue}% < ${batasBawah}%`); - console.log(` Mode: ${modeSensor}, Duration: ${durasiSensor}s`); + const batasBawah = threshold.batas_bawah || 30; + const batasAtas = threshold.batas_atas || 70; + const durasi = threshold.durasi || 600; + const smartMode = threshold.smart_mode === true; + const potAktif = threshold.pot_aktif || []; + const pompaAir = threshold.pompa_air === true; + const pompaPupuk = threshold.pompa_pupuk === true; - const jobId = `sensor-pot-${i}-${Date.now()}`; - await wateringQueue.add( - `sensor-pot-${i}`, - { - type: 'sensor_threshold', - potNumbers: [i], - pompaAir: true, - pompaPupuk: false, // No pupuk for sensor mode - duration: durasiSensor, - scheduleId: jobId, - sensorData: { soilValue, batasBawah, batasAtas, mode: modeSensor }, - }, - { - jobId, - removeOnComplete: true, - priority: 1, // Higher priority for sensor-triggered + // Check each pot in this threshold + for (const potNumber of potAktif) { + if (potNumber < 1 || potNumber > 5) continue; + + const soilKey = `soil_${potNumber}`; + const soilValue = parseInt(sensorData[soilKey]) || 0; + + // Check if below threshold + if (soilValue < batasBawah) { + const potKey = `pot_${potNumber}`; + const lastTime = lastWateringTime[potKey]; + + // Debounce: minimum 2 menit antar penyiraman + if (lastTime && Date.now() - lastTime < config.worker.sensorDebounce) { + const remainingSeconds = Math.ceil((config.worker.sensorDebounce - (Date.now() - lastTime)) / 1000); + if (sensorCheckCounter % 10 === 0) { + console.log(`ā³ ${thresholdKey.toUpperCase()} - POT ${potNumber}: Cooldown (${remainingSeconds}s)`); + } + continue; } - ); - console.log(` šŸ“Œ Added to queue: ${jobId}`); + console.log(`\nšŸŒ”ļø THRESHOLD TRIGGERED: ${thresholdKey.toUpperCase()}`); + console.log(` POT ${potNumber}: ${soilValue}% < ${batasBawah}%`); + console.log(` Mode: ${smartMode ? 'Smart' : 'Fixed'}, Target: ${smartMode ? batasAtas + '%' : durasi + 's'}`); + console.log(` Pumps: Air=${pompaAir}, Pupuk=${pompaPupuk}`); + + const jobId = `${thresholdKey}-pot-${potNumber}-${Date.now()}`; + await wateringQueue.add( + `threshold-pot-${potNumber}`, + { + type: 'sensor_threshold', + potNumbers: [potNumber], + pompaAir: pompaAir, + pompaPupuk: pompaPupuk, + duration: durasi, + scheduleId: jobId, + thresholdId: thresholdKey, + sensorData: { + soilValue, + batasBawah, + batasAtas, + mode: smartMode ? 'smart' : 'fixed' + }, + }, + { + jobId, + removeOnComplete: true, + priority: 1, // Higher priority for sensor-triggered + } + ); + + console.log(` šŸ“Œ Added to queue: ${jobId}`); + + // Update last watering time to prevent rapid re-triggering + lastWateringTime[potKey] = Date.now(); + } } } } catch (error) { - console.error('āŒ Error in sensor monitoring:', error.message); + console.error('āŒ Error in threshold monitoring:', error.message); } }); }