diff --git a/worker.js b/worker.js index 485b41a..4d5ef50 100644 --- a/worker.js +++ b/worker.js @@ -133,20 +133,24 @@ const wateringQueue = new Queue('watering', { connection: redis }); redis.on('connect', () => console.log('āœ… Redis connected')); redis.on('error', (err) => console.error('āŒ Redis error:', err.message)); -// Track last watering time untuk prevent spam -const lastWateringTime = {}; +// Track last watering time PER-THRESHOLD (not per-pot!) untuk prevent spam +const lastThresholdTime = {}; // ==================== WATERING WORKER ==================== const wateringWorker = new Worker( 'watering', async (job) => { - const { type, potNumbers, pompaAir, pompaPupuk, duration, scheduleId } = job.data; + const { type, potNumbers, pompaAir, pompaPupuk, duration, scheduleId, thresholdId, smartMode, sensorData } = job.data; console.log(`\nšŸ’§ Processing Job: ${job.id}`); console.log(` Type: ${type}`); console.log(` Pots: [${potNumbers.join(', ')}]`); - console.log(` Duration: ${duration}s`); + console.log(` Mode: ${smartMode ? 'SMART (auto-stop at target)' : 'FIXED'}`); + console.log(` Duration: ${duration}s ${smartMode ? '(max)' : ''}`); + if (sensorData) { + console.log(` Target: ${sensorData.batasBawah}% → ${sensorData.batasAtas}%`); + } try { // Prepare aktuator updates @@ -167,37 +171,120 @@ const wateringWorker = new Worker( console.log(' šŸ“ Updates:', JSON.stringify(updates, null, 2)); await updateFirebaseSmart('aktuator', updates); + console.log(` šŸš€ ALL VALVES STARTED SIMULTANEOUSLY: ${Object.keys(updates).filter(k => k.startsWith('mosvet_')).join(', ')}`); - // (Verification skipped to avoid SDK timeout - update success already logged above) - - // Wait for duration with progress logging - 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...`); + // 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; + } + + await updateFirebaseSmart('aktuator', stopUpdates); + console.log(` šŸ”“ STOPPED TOGETHER: ${Object.keys(stopUpdates).join(', ')} (Pots: [${potsToStop.join(', ')}])`); + + // 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}`); + } } - await sleep(1000); - } + + // 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; + } + await updateFirebaseSmart('aktuator', timeoutStops); + console.log(` šŸ”“ Force stopped TOGETHER: ${Object.keys(timeoutStops).join(', ')}`); + } + + // Finally, stop pumps + const pumpStop = {}; + if (pompaAir) pumpStop['mosvet_1'] = false; + if (pompaPupuk) pumpStop['mosvet_2'] = false; + if (Object.keys(pumpStop).length > 0) { + await updateFirebaseSmart('aktuator', pumpStop); + console.log(' šŸ”“ Pumps stopped:', Object.keys(pumpStop).join(', ')); + } + console.log(' āœ… Smart mode completed, now logging history...'); + + } else { + // FIXED MODE: Wait for fixed duration + const startTime = Date.now(); + const endTime = startTime + duration * 1000; - // Turn OFF - const offUpdates = {}; - for (const key in updates) { - offUpdates[key] = false; + while (Date.now() < endTime) { + const remaining = Math.ceil((endTime - Date.now()) / 1000); + if (remaining % 10 === 0 || remaining <= 5) { + console.log(` ā³ ${remaining}s remaining...`); + } + await sleep(1000); + } + + // Turn OFF all at once (FIXED mode only) + const offUpdates = {}; + for (const key in updates) { + offUpdates[key] = false; + } + console.log(' šŸ”“ Turning OFF:', Object.keys(offUpdates).join(', ')); + await updateFirebaseSmart('aktuator', offUpdates); + console.log(' āœ… Turn OFF completed, now logging history...'); } - console.log(' šŸ”“ Turning OFF:', Object.keys(offUpdates).join(', ')); - await updateFirebaseSmart('aktuator', offUpdates); - console.log(' āœ… Turn OFF completed, now logging history...'); // Log history await logHistory(type, potNumbers, duration); console.log(' āœ… History logged successfully'); - // Update last watering time - for (const pot of potNumbers) { - lastWateringTime[`pot_${pot}`] = Date.now(); + // Update last watering time PER-THRESHOLD (not per-pot!) + if (thresholdId) { + lastThresholdTime[thresholdId] = Date.now(); + console.log(` ā° Cooldown set for ${thresholdId} (2 minutes)`); } console.log(` āœ… Job completed successfully`); @@ -807,10 +894,10 @@ async function checkSensorThresholds() { return; } - // Check if sensor mode is enabled - if (!kontrolConfig.sensor) { + // Check if sensor mode is enabled (using 'otomatis' field, not deprecated 'sensor') + if (!kontrolConfig.otomatis) { if (sensorCheckCounter % 10 === 0) { - console.log(`āš ļø Sensor mode DISABLED (sensor=false). Skipping threshold check.`); + console.log(`āš ļø Sensor mode DISABLED (otomatis=false). Skipping threshold check.`); } return; } @@ -840,6 +927,14 @@ async function checkSensorThresholds() { continue; } + // Check THRESHOLD cooldown (not per-pot!) + const lastTime = lastThresholdTime[thresholdKey]; + if (lastTime && Date.now() - lastTime < config.worker.sensorDebounce) { + const remainingSeconds = Math.ceil((config.worker.sensorDebounce - (Date.now() - lastTime)) / 1000); + console.log(` ā³ ${thresholdKey}: Cooldown active (${remainingSeconds}s remaining) - skipping entire threshold`); + continue; + } + const batasBawah = threshold.batas_bawah || 30; const batasAtas = threshold.batas_atas || 70; const durasi = threshold.durasi || 600; @@ -848,6 +943,10 @@ async function checkSensorThresholds() { const pompaAir = threshold.pompa_air === true; const pompaPupuk = threshold.pompa_pupuk === true; + // Collect pots that need watering in this threshold + const potsNeedWatering = []; + const potDetails = []; + // Check each pot in this threshold for (const potNumber of potAktif) { if (potNumber < 1 || potNumber > 5) { @@ -859,57 +958,64 @@ async function checkSensorThresholds() { const soilValue = parseInt(sensorData[soilKey]) || 0; console.log(` 🌱 POT ${potNumber} (${soilKey}): ${soilValue}% | Threshold: ${batasBawah}-${batasAtas}%`); + console.log(` → Raw value: ${sensorData[soilKey]} | Parsed: ${soilValue} | Check: ${soilValue} < ${batasBawah} = ${soilValue < batasBawah}`); - // Check if below threshold + // NEW: Check if ABOVE upper threshold - skip if too wet! + if (soilValue >= batasAtas) { + console.log(` āœ… POT ${potNumber}: SKIP (${soilValue}% >= ${batasAtas}% - sudah basah!)`); + continue; + } + + // Check if below lower 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); - console.log(` ā³ POT ${potNumber}: Cooldown active (${remainingSeconds}s remaining)`); - continue; - } - - 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}`); + console.log(` 🚨 POT ${potNumber} KERING! ${soilValue}% < ${batasBawah}%`); - // Update last watering time to prevent rapid re-triggering - lastWateringTime[potKey] = Date.now(); + // Add to watering list (cooldown already checked at threshold level) + potsNeedWatering.push(potNumber); + potDetails.push({ pot: potNumber, value: soilValue }); } else { console.log(` āœ… POT ${potNumber}: OK (${soilValue}% >= ${batasBawah}%)`); } } + + // NEW: Create SINGLE job for ALL pots that need watering in this threshold + if (potsNeedWatering.length > 0) { + console.log(`\nšŸŒ”ļø THRESHOLD TRIGGERED: ${thresholdKey.toUpperCase()}`); + console.log(` Pots needing water: [${potsNeedWatering.join(', ')}]`); + potDetails.forEach(p => console.log(` - POT ${p.pot}: ${p.value}% < ${batasBawah}%`)); + console.log(` Mode: ${smartMode ? 'Smart (monitor until ' + batasAtas + '%)' : 'Fixed (' + durasi + 's)'}`); + console.log(` Pumps: Air=${pompaAir}, Pupuk=${pompaPupuk}`); + + const jobId = `${thresholdKey}-${Date.now()}`; + await wateringQueue.add( + thresholdKey, + { + type: 'sensor_threshold', + potNumbers: potsNeedWatering, // ALL pots in 1 job! + pompaAir: pompaAir, + pompaPupuk: pompaPupuk, + duration: durasi, + scheduleId: jobId, + thresholdId: thresholdKey, + smartMode: smartMode, + sensorData: { + batasBawah, + batasAtas, + mode: smartMode ? 'smart' : 'fixed', + potValues: potDetails + }, + }, + { + jobId, + removeOnComplete: true, + priority: 1, // Higher priority for sensor-triggered + } + ); + + console.log(` šŸ“Œ Added to queue: ${jobId}`); + console.log(` šŸ”„ ${thresholdKey} will execute simultaneously for ALL pots`); + console.log(` ā° After completion, ${thresholdKey} cooldown = 2 minutes (other thresholds can still run)`); + } } } catch (error) { console.error('āŒ Error in sensor threshold check:', error.message);