feat: Change watering mode from parallel to sequential (bergiliran) with 30s break between pots

This commit is contained in:
Wizznu 2026-06-02 07:50:05 +07:00
parent fb7e3216f1
commit e666c1ddea
2 changed files with 450 additions and 178 deletions

View File

@ -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

View File

@ -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);