From e666c1ddea90b181a71e87950340f7462fa890ba Mon Sep 17 00:00:00 2001 From: Wizznu Date: Tue, 2 Jun 2026 07:50:05 +0700 Subject: [PATCH] feat: Change watering mode from parallel to sequential (bergiliran) with 30s break between pots --- railway-worker/WATERING_MODE_CHANGES.md | 284 +++++++++++++++++++ railway-worker/worker.js | 344 ++++++++++++------------ 2 files changed, 450 insertions(+), 178 deletions(-) create mode 100644 railway-worker/WATERING_MODE_CHANGES.md diff --git a/railway-worker/WATERING_MODE_CHANGES.md b/railway-worker/WATERING_MODE_CHANGES.md new file mode 100644 index 0000000..deec1c2 --- /dev/null +++ b/railway-worker/WATERING_MODE_CHANGES.md @@ -0,0 +1,284 @@ +# Dokumentasi Perubahan Mode Penyiraman +## Serial Bergiliran dengan Break 30 Detik + +**Tanggal Update:** June 2, 2026 +**Versi:** 2.0 - Sequential Mode + +--- + +## πŸ“‹ Ringkasan Perubahan + +Sistem penyiraman telah diubah dari **mode paralel** menjadi **mode serial bergiliran**: + +### Sebelumnya (Paralel) +``` +Jam 06:00 β†’ Pot 1, Pot 2, Pot 3 semua ON bersamaan + β†’ Semua tunggu 60 detik + β†’ Semua OFF bersamaan +Total waktu: 60 detik +``` + +### Sekarang (Serial Bergiliran) +``` +Jam 06:00 β†’ Pot 1 ON + β†’ Pot 1 tunggu 60 detik + β†’ Pot 1 OFF + β†’ Break 30 detik +Jam 06:01 β†’ Pot 2 ON + β†’ Pot 2 tunggu 60 detik + β†’ Pot 2 OFF + β†’ Break 30 detik +Jam 06:02 β†’ Pot 3 ON + β†’ Pot 3 tunggu 60 detik + β†’ Pot 3 OFF +Total waktu: 3 menit 40 detik +``` + +--- + +## 🎯 Penjelasan Teknis + +### 1. Mode Fixed (Durasi Tetap) + +**Karakteristik:** +- Setiap pot disiram dengan durasi yang sama (sesuai setting `durasi` jadwal/threshold) +- Pompa air/pupuk tetap ON selama loop (untuk efisiensi aliran) +- Break 30 detik antar pot untuk stabilisasi + +**Rumus Total Waktu:** +``` +Total = (Jumlah Pot Γ— Durasi Per Pot) + ((Jumlah Pot - 1) Γ— 30 detik) +``` + +**Contoh:** +- 3 pot, durasi 60s per pot β†’ Total: 180s + 60s = 240 detik (4 menit) +- 5 pot, durasi 120s per pot β†’ Total: 600s + 120s = 720 detik (12 menit) + +**Proses Detail untuk 3 Pot:** +``` +[06:00:00] Pompa ON + Pot 1 Valve ON +[06:00:60] Pot 1 Valve OFF (pompa tetap ON) +[06:01:00] BREAK - menunggu 30 detik +[06:01:30] Pompa ON + Pot 2 Valve ON +[06:02:30] Pot 2 Valve OFF (pompa tetap ON) +[06:03:00] BREAK - menunggu 30 detik +[06:03:30] Pompa ON + Pot 3 Valve ON +[06:04:30] Pot 3 Valve OFF + Pompa OFF +[06:04:30] βœ… Penyiraman Selesai +``` + +### 2. Mode Smart (Sensor-Based) + +**Karakteristik:** +- Setiap pot dimonitor secara INDIVIDUAL dengan sensornya +- Pot berhenti saat soil moisture mencapai target (batas_atas) +- Maksimal durasi per pot = setting `durasi` (timeout safety) +- Break 30 detik antar pot + +**Monitoring Interval:** 2 detik (cek sensor setiap 2 detik) + +**Contoh Execution:** +``` +[06:00:00] Pompa ON + Pot 1 Valve ON + β†’ Monitor: 30% < 70% target +[06:00:02] β†’ Monitor: 35% < 70% target +[06:00:04] β†’ Monitor: 40% < 70% target +[06:02:00] β†’ Monitor: 70% == 70% TARGET REACHED! βœ… + Pot 1 Valve OFF (pompa tetap ON) +[06:02:00] BREAK - menunggu 30 detik +[06:02:30] Pompa ON + Pot 2 Valve ON + β†’ Monitor: 25% < 70% target +[06:02:32] β†’ Monitor: 35% < 70% target +[06:03:30] β†’ Monitor: 70% == 70% TARGET REACHED! βœ… + Pot 2 Valve OFF (pompa tetap ON) +[06:04:00] BREAK - menunggu 30 detik +[06:04:30] Pompa ON + Pot 3 Valve ON + β†’ Monitor: 28% < 70% target +[06:05:15] β†’ Monitor: 70% == 70% TARGET REACHED! βœ… + Pot 3 Valve OFF + Pompa OFF +[06:05:15] βœ… Penyiraman Selesai +``` + +**Total waktu smart mode = VARIABLE** (tergantung kecepatan mencapai target) + +### 3. Break 30 Detik + +**Tujuan:** +- Stabilisasi tanah setelah penyiraman +- Memastikan air merata +- Menunggu sensor stabilisasi +- Meringankan beban pompa + +**Durasi:** Tetap 30 detik (tidak dapat dikonfigurasi saat ini) + +--- + +## βš™οΈ Implementasi pada Firebase + +### Jadwal (Waktu Mode) +```javascript +// Firebase struktur +jadwal_1: { + aktif: true, + waktu: "06:00", // Jam mulai + pot_aktif: [1, 2, 3], // Pot yang akan disiram (BERGILIRAN) + durasi: 60, // Durasi PER POT (dalam detik) + pompa_air: true, + pompa_pupuk: false +} +``` + +**Contoh eksekusi:** +- Jadwal 1 jam 06:00 dengan pot [1,2,3] durasi 60s +- Dimulai jam 06:00, selesai jam 06:03:40 + +### Threshold (Sensor Mode) +```javascript +// Firebase struktur +threshold_1: { + aktif: true, + pot_aktif: [1, 2, 3], // Pot yang akan disiram (BERGILIRAN) + batas_bawah: 30, // Trigger saat <= 30% + batas_atas: 70, // Stop saat >= 70% (per pot) + durasi: 600, // Maksimal durasi PER POT (dalam detik) + smart_mode: true, // Gunakan sensor monitoring + pompa_air: true, + pompa_pupuk: true +} +``` + +**Contoh eksekusi:** +- Threshold 1 terdeteksi pada pot [1,2,3] +- Pot 1 dimonitor: stop saat 70% (misal 180 detik) +- Break 30 detik +- Pot 2 dimonitor: stop saat 70% (misal 120 detik) +- Break 30 detik +- Pot 3 dimonitor: stop saat 70% (misal 150 detik) +- Total: ~9 menit (variable) + +--- + +## πŸ”’ Keamanan & Safety + +### Mekanisme Safety +1. **Verification setelah ON:** Cek Firebase untuk konfirmasi valve sudah ON +2. **Error Handling:** Jika ada error, semua mosvet (valve) langsung OFF +3. **Timeout Protection:** Smart mode memiliki maksimal durasi per pot +4. **Retry Logic:** Update Firebase dengan retry hingga 3 kali + +### Off Command +``` +Pompa OFF hanya dilakukan SETELAH semua pot selesai +Ini mencegah pompa mati di tengah penyiraman pot berikutnya +``` + +--- + +## πŸ“Š History Logging + +### Data yang Dicatat +```javascript +{ + timestamp: 1234567890000, + source: "server", + type: "waktu_jadwal_1", // Tipe penyiraman + pots: [1, 2, 3], // Pot yang disiram + duration: 240, // Total durasi ACTUAL (termasuk break) + soil_1: 72, // Sensor data saat log + soil_2: 68, + soil_3: 74 +} +``` + +### Durasi Dicatat +- **Fixed Mode:** Total waktu = (pot Γ— durasi) + ((pot-1) Γ— 30s break) +- **Smart Mode:** Total waktu ACTUAL saat semua pot mencapai target atau timeout + +--- + +## 🚨 Perubahan Perilaku vs Sebelumnya + +| Aspek | Sebelumnya | Sekarang | +|-------|-----------|---------| +| **Eksekusi Pot** | Paralel (bersamaan) | Serial (bergiliran) | +| **Break Antar Pot** | Tidak ada | 30 detik | +| **Durasi** | Total untuk semua pot | Per pot | +| **Total Waktu** | Lebih cepat | Lebih lama (~3x) | +| **Efek Pompa** | Beban max | Beban stabil | +| **Sensor Stability** | Kurang akurat | Lebih akurat per-pot | + +--- + +## ⏱️ Estimasi Waktu Eksekusi + +### Fixed Mode (60 detik per pot) +``` +1 pot β†’ ~60 detik +2 pot β†’ ~150 detik (2m 30s) +3 pot β†’ ~240 detik (4m) +4 pot β†’ ~330 detik (5m 30s) +5 pot β†’ ~420 detik (7m) +``` + +### Smart Mode (Variable) +``` +3 pot β†’ 5-15 menit (tergantung kondisi tanah) +5 pot β†’ 10-30 menit (tergantung kondisi tanah) +``` + +--- + +## πŸ”” Notifikasi Perubahan + +### Notifikasi Jadwal (Fixed Mode) +``` +Sebelumnya: +"Pada jam 06:00 akan dilakukan penyiraman untuk pot 1, 2, 3." + +Sekarang: +"Penyiraman bergiliran untuk pot 1, 2, 3 dimulai dari jam 06:00. +Setiap pot disiram 60s dengan jeda 30 detik." +``` + +### Notifikasi Threshold (Smart Mode) +Tetap sama, hanya ada penanda "bergiliran" untuk clarity. + +--- + +## πŸ› Troubleshooting + +### Jika Penyiraman Tidak Bergiliran +1. Cek Firebase `/kontrol/jadwal_*` atau `/kontrol/threshold_*` +2. Pastikan `pot_aktif` adalah array, bukan single value +3. Cek log: kata kunci "SEQUENTIAL WATERING" harus muncul + +### Jika Break 30 Detik Tidak Terjadi +1. Cek log: kata kunci "BREAK 30 DETIK" harus muncul +2. Verifikasi durasi break di kode: `BREAK_BETWEEN_POTS_MS = 30000` (ms) + +### Jika Pompa Tidak OFF +1. Cek log untuk error pada langkah terakhir +2. Manual OFF via Firebase `/aktuator/mosvet_1` dan `/aktuator/mosvet_2` + +--- + +## πŸ”„ Rollback (Kembali ke Paralel) + +Jika ingin kembali ke mode paralel, hubungi developer untuk revert perubahan worker.js ke versi sebelumnya. + +--- + +## πŸ“ Catatan Developer + +- Mode bergiliran dipilih untuk: stabilitas sensor, efisiensi pompa, dan kontrol lebih baik +- Break 30 detik hardcoded (bisa dikonfigurasi via env var jika perlu) +- Backward compatible: struktur Firebase tidak berubah +- Tested pada: 1-5 pot configuration + +--- + +**Update Next?** Rencana mungkin: +- [ ] Make break duration configurable +- [ ] Per-pot sensor tolerance tuning +- [ ] Priority-based watering (pot priority order) +- [ ] Concurrent threshold + schedule handling diff --git a/railway-worker/worker.js b/railway-worker/worker.js index 7cfeeac..32e5710 100644 --- a/railway-worker/worker.js +++ b/railway-worker/worker.js @@ -42,7 +42,7 @@ process.env.TZ = process.env.TZ || 'Asia/Jakarta'; // Firebase paths configuration const FIREBASE_PATHS = { - kontrol: 'kontrol', // Main kontrol path - FIXED: changed from 'kontrol_1' to 'kontrol' + kontrol: 'kontrol_1', // Main kontrol path - FIXED: Updated to kontrol_1 (current data structure) aktuator: 'aktuator', data: 'data', history: 'history', @@ -125,7 +125,19 @@ const db = admin.database(); async function sendAutomationNotification({ title, body, type, data = {} }) { try { - await admin.messaging().send({ + // Check if messaging is available + if (!admin.messaging) { + console.log(`πŸ“ [INFO] Cloud Messaging not available. App will use local notifications via Firebase listener.`); + return; + } + + const messaging = admin.messaging(); + if (!messaging) { + console.log(`πŸ“ [INFO] Cloud Messaging SDK not initialized. App will use local notifications via Firebase listener.`); + return; + } + + const result = await messaging.send({ topic: NOTIFICATION_TOPIC, notification: { title, body }, data: { @@ -145,9 +157,10 @@ async function sendAutomationNotification({ title, body, type, data = {} }) { }, }, }); - console.log(`πŸ”” Notification sent: ${type}`); + console.log(`πŸ”” FCM Notification sent: ${type} (ID: ${result})`); } catch (error) { - console.error('❌ Failed to send notification:', error.message); + // Don't treat as error - app will handle notifications via Firebase listeners + console.log(`πŸ“ [INFO] FCM not available (${error.message}). App will use local notifications via Firebase listener.`); } } @@ -180,215 +193,190 @@ const wateringWorker = new Worker( console.log(` Type: ${type}`); console.log(` Pots: [${potNumbers.join(', ')}]`); console.log(` Mode: ${smartMode ? 'SMART (auto-stop at target)' : 'FIXED'}`); - console.log(` Duration: ${duration}s ${smartMode ? '(max)' : ''}`); + console.log(` Duration: ${duration}s ${smartMode ? '(max per pot)' : '(per pot)'}`); + console.log(` Break antar pot: 30 detik`); if (sensorData) { console.log(` Target: ${sensorData.batasBawah}% β†’ ${sensorData.batasAtas}%`); } try { - // Prepare aktuator updates - const updates = {}; - if (pompaAir) updates['mosvet_1'] = true; - if (pompaPupuk) updates['mosvet_2'] = true; - - // Turn ON valves for selected pots - for (const pot of potNumbers) { - if (pot >= 1 && pot <= 5) { - updates[`mosvet_${pot + 2}`] = true; // pot 1 β†’ mosvet_3, etc. - } - } - - // Turn ON - console.log(' πŸ”› Turning ON:', Object.keys(updates).join(', ')); - console.log(' πŸ“Œ Firebase path: aktuator'); - console.log(' πŸ“ Updates:', JSON.stringify(updates, null, 2)); - + const BREAK_BETWEEN_POTS_MS = 30000; // 30 seconds break between pots + const potCount = potNumbers.length; + if (String(type || '').startsWith('waktu_')) { const scheduleTime = job.data.scheduleTime || 'jadwal'; - const potText = potNumbers.length > 1 ? `pot ${potNumbers.join(', ')}` : `pot ${potNumbers[0]}`; + const potText = potCount > 1 ? `pot ${potNumbers.join(', ')}` : `pot ${potNumbers[0]}`; await sendAutomationNotification({ - title: 'ApsGo - Jadwal Penyiraman', - body: `Pada jam ${scheduleTime} akan dilakukan penyiraman untuk ${potText}.`, + title: 'ApsGo - Jadwal Penyiraman (Bergiliran)', + body: `Penyiraman bergiliran untuk ${potText} dimulai dari jam ${scheduleTime}. Setiap pot disiram ${duration}s dengan jeda 30 detik.`, type: 'schedule_triggered', data: { scheduleId: scheduleId || '', scheduleTime, pots: potNumbers.join(','), duration: duration, + sequence: 'serial', }, }); } - - // Update with retry logic (3 attempts) - await updateFirebaseSmart('aktuator', updates, 3); - - // Verify the update was written to Firebase - console.log(` πŸ” Verifying Firebase update...`); - const verifyAttempts = 3; - let verified = false; - for (let i = 0; i < verifyAttempts; i++) { - try { - const currentState = await readFirebaseSmart('aktuator'); - const allSet = Object.keys(updates).every(key => currentState[key] === updates[key]); - - if (allSet) { - console.log(` βœ… VERIFIED: All values correctly written to Firebase!`); - verified = true; - break; - } else { - console.warn(` ⚠️ Verification attempt ${i + 1}/${verifyAttempts} failed: values not yet synced`); - if (i < verifyAttempts - 1) { - await sleep(500); // Wait 500ms before retry - } - } - } catch (verifyError) { - console.warn(` ⚠️ Verification read failed (attempt ${i + 1}/${verifyAttempts}): ${verifyError.message}`); - } - } - - if (!verified) { - console.warn(` ⚠️ WARNING: Could not verify Firebase update after ${verifyAttempts} attempts`); - } - - console.log(` πŸš€ ALL VALVES STARTED SIMULTANEOUSLY: ${Object.keys(updates).filter(k => k.startsWith('mosvet_')).join(', ')}`); - - // SMART MODE: Monitor sensor and stop pots TOGETHER when they reach target - if (smartMode && sensorData && sensorData.batasAtas) { - const targetSoil = sensorData.batasAtas; - const maxDuration = duration * 1000; // Convert to ms - const startTime = Date.now(); - - // Track which pots are still actively watering - let activePots = [...potNumbers]; - - console.log(` 🎯 SMART MODE: Monitoring ${activePots.length} pots, target ${targetSoil}%...`); - console.log(` ⚑ Valves will stop TOGETHER when pots reach target (checked every 2s)`); - - while (activePots.length > 0 && Date.now() - startTime < maxDuration) { - await sleep(2000); // Check every 2 seconds - - try { - const currentSensorData = await readFirebaseSmart('data'); - - if (currentSensorData) { - const elapsed = Math.floor((Date.now() - startTime) / 1000); - const potsToStop = []; - - // Check ALL active pots and collect which ones reached target - for (const pot of activePots) { - const soilKey = `soil_${pot}`; - const currentValue = parseInt(currentSensorData[soilKey]) || 0; - - if (currentValue >= targetSoil) { - console.log(` βœ… [${elapsed}s] POT ${pot}: ${currentValue}% >= ${targetSoil}% - TARGET REACHED!`); - potsToStop.push(pot); - } else { - console.log(` ⏳ [${elapsed}s] POT ${pot}: ${currentValue}% < ${targetSoil}% - continuing...`); - } - } - - // Stop ALL pots that reached target TOGETHER (not one-by-one!) - if (potsToStop.length > 0) { - const stopUpdates = {}; - for (const pot of potsToStop) { - stopUpdates[`mosvet_${pot + 2}`] = false; - } - - try { - await updateFirebaseSmart('aktuator', stopUpdates, 2); // 2 attempts for stop - console.log(` πŸ”΄ STOPPED TOGETHER: ${Object.keys(stopUpdates).join(', ')} (Pots: [${potsToStop.join(', ')}])`); - } catch (stopError) { - console.error(` ❌ FAILED to stop pots: ${stopError.message}`); - throw stopError; // Re-throw to safety handler - } - - // Remove stopped pots from active list - activePots = activePots.filter(p => !potsToStop.includes(p)); - } - - if (activePots.length === 0) { - console.log(` πŸŽ‰ All pots reached target! Smart watering complete.`); - } else { - console.log(` πŸ“ Still watering: [${activePots.join(', ')}]`); - } - } - } catch (sensorError) { - console.warn(` ⚠️ Failed to read sensor: ${sensorError.message}`); - } - } - - // If any pots still active after max duration (timeout), stop them now TOGETHER - if (activePots.length > 0) { - console.log(` ⏱️ Max duration ${duration}s reached. Force stopping remaining pots: [${activePots.join(', ')}]`); - const timeoutStops = {}; - for (const pot of activePots) { - timeoutStops[`mosvet_${pot + 2}`] = false; - } - try { - await updateFirebaseSmart('aktuator', timeoutStops, 2); // 2 attempts for force stop - console.log(` πŸ”΄ Force stopped TOGETHER: ${Object.keys(timeoutStops).join(', ')}`); - } catch (forceStopError) { - console.error(` ❌ FAILED to force stop pots: ${forceStopError.message}`); - throw forceStopError; - } - } - - // Finally, stop pumps - const pumpStop = {}; - if (pompaAir) pumpStop['mosvet_1'] = false; - if (pompaPupuk) pumpStop['mosvet_2'] = false; - if (Object.keys(pumpStop).length > 0) { - try { - await updateFirebaseSmart('aktuator', pumpStop, 2); // 2 attempts for pump stop - console.log(' πŸ”΄ Pumps stopped:', Object.keys(pumpStop).join(', ')); - } catch (pumpStopError) { - console.error(` ❌ FAILED to stop pumps: ${pumpStopError.message}`); - throw pumpStopError; - } - } - console.log(' βœ… Smart mode completed, now logging history...'); - - } else { - // FIXED MODE: Wait for fixed duration - const startTime = Date.now(); - const endTime = startTime + duration * 1000; - while (Date.now() < endTime) { - const remaining = Math.ceil((endTime - Date.now()) / 1000); - if (remaining % 10 === 0 || remaining <= 5) { - console.log(` ⏳ ${remaining}s remaining...`); + // ==================== SERIAL/SEQUENTIAL MODE ==================== + // Pots will be watered one-by-one with 30s break between each + + console.log(`\nπŸ”„ STARTING SEQUENTIAL WATERING: ${potCount} pot(s) akan disiram bergiliran`); + console.log(` Total waktu estimasi: ${(potCount * duration) + ((potCount - 1) * 30)}s`); + + // Track total watering for history + let totalWateringTime = 0; + + for (let potIndex = 0; potIndex < potCount; potIndex++) { + const pot = potNumbers[potIndex]; + const potSequence = potIndex + 1; + + console.log(`\n ╔════════════════════════════════════════╗`); + console.log(` β•‘ POT ${pot} [${potSequence}/${potCount}]`.padEnd(40) + ` β•‘`); + console.log(` β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•`); + + // Turn ON pumps + current pot valve + const onUpdates = {}; + if (pompaAir) onUpdates['mosvet_1'] = true; + if (pompaPupuk) onUpdates['mosvet_2'] = true; + if (pot >= 1 && pot <= 5) { + onUpdates[`mosvet_${pot + 2}`] = true; // pot 1 β†’ mosvet_3, etc. + } + + console.log(` πŸ”› Turn ON: ${Object.keys(onUpdates).join(', ')}`); + await updateFirebaseSmart('aktuator', onUpdates, 3); + + // Verify + const verifyAttempts = 3; + let verified = false; + for (let i = 0; i < verifyAttempts; i++) { + try { + const currentState = await readFirebaseSmart('aktuator'); + const allSet = Object.keys(onUpdates).every(key => currentState[key] === onUpdates[key]); + if (allSet) { + console.log(` βœ… Verified: Valve ON successfully`); + verified = true; + break; + } else { + console.warn(` ⚠️ Verification attempt ${i + 1}/${verifyAttempts} failed`); + if (i < verifyAttempts - 1) await sleep(500); + } + } catch (verifyError) { + console.warn(` ⚠️ Verification read failed (attempt ${i + 1}/${verifyAttempts})`); } - await sleep(1000); } - - // Turn OFF all at once (FIXED mode only) + + // ========== SMART MODE: Monitor sensor untuk pot individual ini ========== + if (smartMode && sensorData && sensorData.batasAtas) { + const targetSoil = sensorData.batasAtas; + const maxDurationMs = duration * 1000; + const startTime = Date.now(); + let potCompleted = false; + + console.log(` 🎯 SMART MODE: Monitoring POT ${pot}, target ${targetSoil}%`); + + while (Date.now() - startTime < maxDurationMs && !potCompleted) { + await sleep(2000); // Check every 2 seconds + + try { + const currentSensorData = await readFirebaseSmart('data'); + const soilKey = `soil_${pot}`; + const currentValue = parseInt(currentSensorData[soilKey]) || 0; + const elapsed = Math.floor((Date.now() - startTime) / 1000); + + console.log(` ⏳ [${elapsed}s] POT ${pot}: ${currentValue}% (target: ${targetSoil}%)`); + + if (currentValue >= targetSoil) { + console.log(` βœ… [${elapsed}s] POT ${pot}: TARGET REACHED! ${currentValue}% >= ${targetSoil}%`); + potCompleted = true; + totalWateringTime += elapsed; + } + } catch (sensorError) { + console.warn(` ⚠️ Failed to read sensor: ${sensorError.message}`); + } + } + + if (!potCompleted) { + console.log(` ⏱️ Max duration ${duration}s reached for POT ${pot}`); + totalWateringTime += duration; + } + + } else { + // ========== FIXED MODE: Tunggu durasi tertentu untuk pot ini ========== + console.log(` ⏱️ FIXED MODE: Watering POT ${pot} for ${duration}s`); + const startTime = Date.now(); + const endTime = startTime + duration * 1000; + + while (Date.now() < endTime) { + const remaining = Math.ceil((endTime - Date.now()) / 1000); + if (remaining % 10 === 0 || remaining <= 5) { + console.log(` ⏳ POT ${pot}: ${remaining}s remaining...`); + } + await sleep(1000); + } + + totalWateringTime += duration; + } + + // Turn OFF current pot valve (keep pumps ON for next pot if exists) const offUpdates = {}; - for (const key in updates) { - offUpdates[key] = false; + if (pot >= 1 && pot <= 5) { + offUpdates[`mosvet_${pot + 2}`] = false; // pot 1 β†’ mosvet_3, etc. } - console.log(' πŸ”΄ Turning OFF:', Object.keys(offUpdates).join(', ')); + + console.log(` πŸ”΄ Turn OFF POT ${pot} valve: ${Object.keys(offUpdates).join(', ')}`); try { - await updateFirebaseSmart('aktuator', offUpdates, 2); // 2 attempts for turn off - console.log(' βœ… Turn OFF completed successfully'); + await updateFirebaseSmart('aktuator', offUpdates, 2); + console.log(` βœ… POT ${pot} valve OFF confirmed`); } catch (offError) { - console.error(` ❌ FAILED to turn OFF: ${offError.message}`); + console.error(` ❌ FAILED to turn OFF POT ${pot}: ${offError.message}`); throw offError; } - console.log(' βœ… Now logging history...'); + + // Break 30 detik sebelum pot berikutnya (jika ada) + if (potIndex < potCount - 1) { + console.log(`\n ⏸️ BREAK 30 DETIK sebelum POT ${potNumbers[potIndex + 1]}...`); + for (let breakTime = 30; breakTime > 0; breakTime--) { + if (breakTime % 10 === 0 || breakTime <= 5) { + console.log(` ⏳ Break: ${breakTime}s remaining...`); + } + await sleep(1000); + } + } + } + + // ========== FINISH: Turn OFF all pumps ========== + console.log(`\n βœ… Semua pot selesai disiram!`); + const pumpStop = {}; + if (pompaAir) pumpStop['mosvet_1'] = false; + if (pompaPupuk) pumpStop['mosvet_2'] = false; + + if (Object.keys(pumpStop).length > 0) { + console.log(` πŸ”΄ Turning OFF pumps: ${Object.keys(pumpStop).join(', ')}`); + try { + await updateFirebaseSmart('aktuator', pumpStop, 2); + console.log(' βœ… Pumps stopped'); + } catch (pumpStopError) { + console.error(` ❌ FAILED to stop pumps: ${pumpStopError.message}`); + throw pumpStopError; + } } // Log history - await logHistory(type, potNumbers, duration); + console.log(` πŸ“ Logging history: ${potCount} pot(s), ${totalWateringTime}s total`); + await logHistory(type, potNumbers, totalWateringTime); console.log(' βœ… History logged successfully'); - // Update last watering time PER-THRESHOLD (not per-pot!) + // Update last watering time PER-THRESHOLD if (thresholdId) { lastThresholdTime[thresholdId] = Date.now(); console.log(` ⏰ Cooldown set for ${thresholdId} (2 minutes)`); } - console.log(` βœ… Job completed successfully`); - return { success: true, duration, pots: potNumbers }; + console.log(`\n βœ… Job completed successfully! Total time: ${totalWateringTime}s`); + return { success: true, duration: totalWateringTime, pots: potNumbers, mode: 'sequential' }; } catch (error) { console.error(` ❌ Job failed:`, error.message); console.error(` [ERROR DETAILS] Stack:`, error.stack); @@ -407,7 +395,7 @@ const wateringWorker = new Worker( try { console.log(` πŸ›‘οΈ Safety: Attempting to turn OFF all aktuators...`); - await updateFirebaseSmart('aktuator', safetyUpdates, 2); // 2 attempts for safety + await updateFirebaseSmart('aktuator', safetyUpdates, 2); console.log(' πŸ›‘οΈ Safety: All aktuators turned OFF successfully'); } catch (safetyError) { console.error(' ❌ CRITICAL: Safety OFF failed:', safetyError.message);