feat: Change watering mode from parallel to sequential (bergiliran) with 30s break between pots
This commit is contained in:
parent
fb7e3216f1
commit
e666c1ddea
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue