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
This commit is contained in:
parent
6f4b7c25f1
commit
91cb3eaaea
|
|
@ -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();
|
||||||
89
worker.js
89
worker.js
|
|
@ -787,7 +787,7 @@ setTimeout(async () => {
|
||||||
let sensorCheckCounter = 0;
|
let sensorCheckCounter = 0;
|
||||||
|
|
||||||
async function setupSensorMonitoring() {
|
async function setupSensorMonitoring() {
|
||||||
console.log('✅ Sensor Mode monitoring started');
|
console.log('✅ Sensor Mode (Threshold System) monitoring started');
|
||||||
|
|
||||||
db.ref('data').on('value', async (snapshot) => {
|
db.ref('data').on('value', async (snapshot) => {
|
||||||
sensorCheckCounter++;
|
sensorCheckCounter++;
|
||||||
|
|
@ -802,52 +802,81 @@ async function setupSensorMonitoring() {
|
||||||
const configSnapshot = await fetchWithTimeout(db.ref(FIREBASE_PATHS.kontrol), 10000);
|
const configSnapshot = await fetchWithTimeout(db.ref(FIREBASE_PATHS.kontrol), 10000);
|
||||||
const kontrolConfig = configSnapshot.val();
|
const kontrolConfig = configSnapshot.val();
|
||||||
|
|
||||||
// Log sensor check (verbose hanya setiap 10 kali)
|
if (!kontrolConfig) {
|
||||||
if (sensorCheckCounter % 10 === 0) {
|
|
||||||
console.log(`\n🌡️ SENSOR CHECK #${sensorCheckCounter} | Mode Otomatis: ${kontrolConfig?.otomatis ? '✅' : '❌'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!kontrolConfig || !kontrolConfig.otomatis) {
|
|
||||||
// Sensor mode disabled
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const batasBawah = kontrolConfig.batas_bawah || 40;
|
// Detect all threshold_* nodes
|
||||||
const batasAtas = kontrolConfig.batas_atas || 100;
|
const allThresholds = Object.keys(kontrolConfig).filter(key => key.startsWith('threshold_'));
|
||||||
const durasiSensor = kontrolConfig.durasi_sensor || 60;
|
|
||||||
const modeSensor = kontrolConfig.mode_sensor || 'fixed'; // 'fixed' or 'smart'
|
|
||||||
|
|
||||||
// Check each pot
|
// Log sensor check (verbose hanya setiap 10 kali)
|
||||||
for (let i = 1; i <= 5; i++) {
|
if (sensorCheckCounter % 10 === 0) {
|
||||||
const soilKey = `soil_${i}`;
|
console.log(`\n🌡️ SENSOR CHECK #${sensorCheckCounter} | Total Thresholds: ${allThresholds.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allThresholds.length === 0) {
|
||||||
|
// No thresholds configured
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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;
|
const soilValue = parseInt(sensorData[soilKey]) || 0;
|
||||||
|
|
||||||
|
// Check if below threshold
|
||||||
if (soilValue < batasBawah) {
|
if (soilValue < batasBawah) {
|
||||||
const potKey = `pot_${i}`;
|
const potKey = `pot_${potNumber}`;
|
||||||
const lastTime = lastWateringTime[potKey];
|
const lastTime = lastWateringTime[potKey];
|
||||||
|
|
||||||
// Debounce: minimum 2 menit antar penyiraman
|
// Debounce: minimum 2 menit antar penyiraman
|
||||||
if (lastTime && Date.now() - lastTime < config.worker.sensorDebounce) {
|
if (lastTime && Date.now() - lastTime < config.worker.sensorDebounce) {
|
||||||
const remainingSeconds = Math.ceil((config.worker.sensorDebounce - (Date.now() - lastTime)) / 1000);
|
const remainingSeconds = Math.ceil((config.worker.sensorDebounce - (Date.now() - lastTime)) / 1000);
|
||||||
console.log(`⏳ POT ${i}: Cooldown active (${remainingSeconds}s remaining)`);
|
if (sensorCheckCounter % 10 === 0) {
|
||||||
|
console.log(`⏳ ${thresholdKey.toUpperCase()} - POT ${potNumber}: Cooldown (${remainingSeconds}s)`);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n🌡️ SENSOR TRIGGERED: POT ${i}`);
|
console.log(`\n🌡️ THRESHOLD TRIGGERED: ${thresholdKey.toUpperCase()}`);
|
||||||
console.log(` Soil moisture: ${soilValue}% < ${batasBawah}%`);
|
console.log(` POT ${potNumber}: ${soilValue}% < ${batasBawah}%`);
|
||||||
console.log(` Mode: ${modeSensor}, Duration: ${durasiSensor}s`);
|
console.log(` Mode: ${smartMode ? 'Smart' : 'Fixed'}, Target: ${smartMode ? batasAtas + '%' : durasi + 's'}`);
|
||||||
|
console.log(` Pumps: Air=${pompaAir}, Pupuk=${pompaPupuk}`);
|
||||||
|
|
||||||
const jobId = `sensor-pot-${i}-${Date.now()}`;
|
const jobId = `${thresholdKey}-pot-${potNumber}-${Date.now()}`;
|
||||||
await wateringQueue.add(
|
await wateringQueue.add(
|
||||||
`sensor-pot-${i}`,
|
`threshold-pot-${potNumber}`,
|
||||||
{
|
{
|
||||||
type: 'sensor_threshold',
|
type: 'sensor_threshold',
|
||||||
potNumbers: [i],
|
potNumbers: [potNumber],
|
||||||
pompaAir: true,
|
pompaAir: pompaAir,
|
||||||
pompaPupuk: false, // No pupuk for sensor mode
|
pompaPupuk: pompaPupuk,
|
||||||
duration: durasiSensor,
|
duration: durasi,
|
||||||
scheduleId: jobId,
|
scheduleId: jobId,
|
||||||
sensorData: { soilValue, batasBawah, batasAtas, mode: modeSensor },
|
thresholdId: thresholdKey,
|
||||||
|
sensorData: {
|
||||||
|
soilValue,
|
||||||
|
batasBawah,
|
||||||
|
batasAtas,
|
||||||
|
mode: smartMode ? 'smart' : 'fixed'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
jobId,
|
jobId,
|
||||||
|
|
@ -857,10 +886,14 @@ async function setupSensorMonitoring() {
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(` 📌 Added to queue: ${jobId}`);
|
console.log(` 📌 Added to queue: ${jobId}`);
|
||||||
|
|
||||||
|
// Update last watering time to prevent rapid re-triggering
|
||||||
|
lastWateringTime[potKey] = Date.now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error in sensor monitoring:', error.message);
|
console.error('❌ Error in threshold monitoring:', error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue