import 'package:firebase_database/firebase_database.dart'; import 'package:flutter/foundation.dart'; class FirebaseDatabaseService { static final FirebaseDatabaseService _instance = FirebaseDatabaseService._internal(); factory FirebaseDatabaseService() => _instance; FirebaseDatabaseService._internal(); final DatabaseReference _database = FirebaseDatabase.instance.ref(); // ==================== SENSOR DATA ==================== /// Get real-time stream of sensor data Stream> getSensorDataStream() { return _database.child('data').onValue.map((event) { if (event.snapshot.value != null) { final data = Map.from(event.snapshot.value as Map); return { 'suhu': _parseValue(data['suhu']), 'kelembapan': _parseValue(data['kelembapan']), 'ldr': _parseValue(data['ldr']), 'water_flow': data['water_flow'] ?? 0, // TAMBAHAN: water flow sensor 'soil_1': _parseValue(data['soil_1']), 'soil_2': _parseValue(data['soil_2']), 'soil_3': _parseValue(data['soil_3']), 'soil_4': _parseValue(data['soil_4']), 'soil_5': _parseValue(data['soil_5']), }; } return {}; }); } /// Get sensor data once Future> getSensorData() async { try { final snapshot = await _database.child('data').get(); if (snapshot.exists) { final data = Map.from(snapshot.value as Map); return { 'suhu': _parseValue(data['suhu']), 'kelembapan': _parseValue(data['kelembapan']), 'ldr': _parseValue(data['ldr']), 'water_flow': data['water_flow'] ?? 0, // TAMBAHAN: water flow sensor 'soil_1': _parseValue(data['soil_1']), 'soil_2': _parseValue(data['soil_2']), 'soil_3': _parseValue(data['soil_3']), 'soil_4': _parseValue(data['soil_4']), 'soil_5': _parseValue(data['soil_5']), }; } } catch (e) { print('Error getting sensor data: $e'); } return {}; } // ==================== AKTUATOR CONTROL ==================== /// Get real-time stream of aktuator status Stream> getAktuatorStream() { return _database.child('aktuator').onValue.map((event) { if (event.snapshot.value != null) { final data = Map.from(event.snapshot.value as Map); return { 'mosvet_1': data['mosvet_1'] ?? false, // Pompa Air 'mosvet_2': data['mosvet_2'] ?? false, // Pompa Pupuk 'mosvet_3': data['mosvet_3'] ?? false, // Valve 1 'mosvet_4': data['mosvet_4'] ?? false, // Valve 2 'mosvet_5': data['mosvet_5'] ?? false, // Valve 3 'mosvet_6': data['mosvet_6'] ?? false, // Valve 4 'mosvet_7': data['mosvet_7'] ?? false, // Valve 5 'mosvet_8': data['mosvet_8'] ?? false, // Pengaduk }; } return {}; }); } /// Set aktuator status (untuk kontrol manual) Future setAktuator(String mosfetName, bool value) async { try { await _database.child('aktuator/$mosfetName').set(value); } catch (e) { print('Error setting aktuator: $e'); rethrow; } } /// Set pompa air (mosvet_1) Future setPompaAir(bool value) async { await setAktuator('mosvet_1', value); } /// Set pompa pupuk (mosvet_2) Future setPompaPupuk(bool value) async { await setAktuator('mosvet_2', value); } /// Set pengaduk/motor (mosvet_8) Future setPengaduk(bool value) async { await setAktuator('mosvet_8', value); } /// Set pot/valve (mosvet_3 to mosvet_7 untuk POT 1-5) /// potNumber: 1-5 Future setPot(int potNumber, bool value) async { if (potNumber < 1 || potNumber > 5) { throw ArgumentError('Pot number must be between 1 and 5'); } await setAktuator('mosvet_${potNumber + 2}', value); } /// Set multiple aktuator at once Future setMultipleAktuator(Map aktuatorStates) async { try { await _database.child('aktuator').update(aktuatorStates); } catch (e) { print('Error setting multiple aktuator: $e'); rethrow; } } /// Matikan semua aktuator Future turnOffAllAktuator() async { await setMultipleAktuator({ 'mosvet_1': false, 'mosvet_2': false, 'mosvet_3': false, 'mosvet_4': false, 'mosvet_5': false, 'mosvet_6': false, 'mosvet_7': false, 'mosvet_8': false, }); } // ==================== KONTROL CONFIG ==================== /// Get real-time stream of kontrol configuration Stream> getKontrolStream() { // Check kontrol_1 dulu (data terbaru) return _database.child('kontrol_1').onValue.map((event) { if (event.snapshot.value != null) { return Map.from(event.snapshot.value as Map); } return {}; }); } /// Get kontrol configuration once (checks kontrol_1 first, then kontrol for legacy) Future> getKontrolConfig() async { try { // Try kontrol_1 first (current data structure) final snapshot1 = await _database.child('kontrol_1').get(); if (snapshot1.exists && snapshot1.value != null) { print('[DEBUG] Got kontrol config from /kontrol_1'); return Map.from(snapshot1.value as Map); } // Fallback to kontrol (legacy) final snapshot = await _database.child('kontrol').get(); if (snapshot.exists && snapshot.value != null) { print('[DEBUG] Got kontrol config from /kontrol (legacy)'); return Map.from(snapshot.value as Map); } print('[WARN] Kontrol config not found in both /kontrol_1 and /kontrol'); } catch (e) { print('Error getting kontrol config: $e'); } return {}; } /// Update kontrol configuration (writes to kontrol_1) Future updateKontrolConfig(Map config) async { try { print('🔥 [DEBUG] Updating Firebase kontrol config (kontrol_1): $config'); await _database.child('kontrol_1').update(config); print('✅ [DEBUG] Firebase kontrol config updated successfully'); } catch (e) { print('❌ [DEBUG] Error updating kontrol config: $e'); rethrow; } } /// Set batas threshold (batas_atas, batas_bawah) - writes to kontrol_1 Future setThreshold({int? batasAtas, int? batasBawah}) async { try { final updates = {}; if (batasAtas != null) updates['batas_atas'] = batasAtas; if (batasBawah != null) updates['batas_bawah'] = batasBawah; await _database.child('kontrol_1').update(updates); } catch (e) { print('Error setting threshold: $e'); rethrow; } } /// Set waktu penyiraman - writes to kontrol_1 Future setWaktuPenyiraman({ String? waktu1, String? waktu2, bool? waktuEnabled, }) async { try { final updates = {}; if (waktu1 != null) updates['waktu_1'] = waktu1; if (waktu2 != null) updates['waktu_2'] = waktu2; if (waktuEnabled != null) updates['waktu'] = waktuEnabled; await _database.child('kontrol_1').update(updates); } catch (e) { print('Error setting waktu: $e'); rethrow; } } /// Set durasi penyiraman - writes to kontrol_1 Future setDurasi({int? durasi1, int? durasi2}) async { try { final updates = {}; if (durasi1 != null) updates['durasi_1'] = durasi1; if (durasi2 != null) updates['durasi_2'] = durasi2; await _database.child('kontrol_1').update(updates); } catch (e) { print('Error setting durasi: $e'); rethrow; } } /// Toggle mode otomatis - writes to kontrol_1 Future setOtomatis(bool value) async { try { await _database.child('kontrol_1/otomatis').set(value); } catch (e) { print('Error setting otomatis: $e'); rethrow; } } // ==================== HELPER FUNCTIONS ==================== /// Parse value dari Firebase (handle empty string) String _parseValue(dynamic value) { if (value == null || value == '') { return '0'; } return value.toString(); } /// Get database reference (untuk advanced usage) DatabaseReference get databaseRef => _database; /// Check connection status Stream getConnectionStatus() { return _database.child('.info/connected').onValue.map((event) { return event.snapshot.value as bool? ?? false; }); } // ==================== HISTORY LOGGING ==================== /// Save sensor data snapshot to history (monitoring) Future saveHistory(Map sensorData) async { try { final now = DateTime.now(); // Simpan per-pot menggunakan push() dengan jenis_histori: monitoring for (int pot = 1; pot <= 5; pot++) { final soilKey = 'soil_$pot'; final soilValue = sensorData[soilKey]; if (soilValue == null) continue; await _database.child('history').push().set({ 'timestamp': now.millisecondsSinceEpoch, 'tanggal': '${now.day}/${now.month}/${now.year}', 'waktu': '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}', 'jenis_histori': 'monitoring', 'sumber': 'app', 'mode': '-', 'pot': pot, 'suhu': sensorData['suhu'] ?? 0, 'kelembapan': sensorData['kelembapan'] ?? 0, 'soil': soilValue, 'durasi': 0, 'type': 'app_log', }); } } catch (e) { print('Error saving history: $e'); } } /// Save penyiraman manual ke histori Future saveHistoriPenyiramanManual({ required List potNumbers, required bool pompaAir, required bool pompaNutrisi, }) async { try { final now = DateTime.now(); final sensorData = await getSensorData(); for (final pot in potNumbers) { final soilKey = 'soil_$pot'; await _database.child('history').push().set({ 'timestamp': now.millisecondsSinceEpoch, 'tanggal': '${now.day}/${now.month}/${now.year}', 'waktu': '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}', 'jenis_histori': 'penyiraman', 'sumber': 'app', 'mode': 'manual', 'pot': pot, 'suhu': sensorData['suhu'] ?? 0, 'kelembapan': sensorData['kelembapan'] ?? 0, 'soil': sensorData[soilKey] ?? 0, 'durasi': 0, 'type': 'manual', 'pompa_air': pompaAir, 'pompa_nutrisi': pompaNutrisi, }); } print('✅ Histori penyiraman manual saved: ${potNumbers.length} pot'); } catch (e) { print('Error saving manual watering history: $e'); rethrow; } } /// Get history data for a specific date range. /// Mendukung dua format: /// 1. Push-key flat entries: /history/-key/ (ada field `timestamp`) /// 2. Date-keyed nested entries: /history/YYYY-MM-DD/HH:mm/ (format ESP32 lama) Future> getHistoryByDateRange( DateTime startDate, DateTime endDate, ) async { final result = {}; try { // ── Query 1: Push-key entries dengan field `timestamp` ────────────────── final startTs = startDate.millisecondsSinceEpoch; final endTs = endDate .copyWith(hour: 23, minute: 59, second: 59) .millisecondsSinceEpoch; final tsSnapshot = await _database .child('history') .orderByChild('timestamp') .startAt(startTs) .endAt(endTs) .get(); if (tsSnapshot.exists && tsSnapshot.value != null) { final raw = Map.from(tsSnapshot.value as Map); result.addAll(raw); debugPrint('[History] Push-key entries fetched: ${raw.length}'); } } catch (e) { debugPrint('Error fetching push-key history: $e'); } try { // ── Query 2: Date-keyed entries (format ESP32: YYYY-MM-DD/HH:mm) ─────── // Iterasi setiap tanggal dalam rentang DateTime cursor = DateTime(startDate.year, startDate.month, startDate.day); final endDay = DateTime(endDate.year, endDate.month, endDate.day); while (!cursor.isAfter(endDay)) { final dateStr = '${cursor.year}-' '${cursor.month.toString().padLeft(2, '0')}-' '${cursor.day.toString().padLeft(2, '0')}'; final daySnapshot = await _database.child('history/$dateStr').get(); if (daySnapshot.exists && daySnapshot.value != null) { // Simpan dengan prefix 'date|' agar bisa dikenali oleh parser result['date|$dateStr'] = daySnapshot.value; debugPrint('[History] Date-keyed entries for $dateStr fetched'); } cursor = cursor.add(const Duration(days: 1)); } } catch (e) { debugPrint('Error fetching date-keyed history: $e'); } debugPrint('[History] Total raw keys returned: ${result.length}'); return result; } /// Get latest history entries (limit) Future> getLatestHistory({int limit = 100}) async { try { final snapshot = await _database .child('history') .orderByKey() .limitToLast(limit) .get(); if (snapshot.exists) { return Map.from(snapshot.value as Map); } } catch (e) { print('Error getting latest history: $e'); } return {}; } /// Clear old history data (older than X days) Future clearOldHistory({int daysToKeep = 30}) async { try { final cutoffDate = DateTime.now().subtract(Duration(days: daysToKeep)); final snapshot = await _database.child('history').get(); if (snapshot.exists) { final data = Map.from(snapshot.value as Map); for (var dateKey in data.keys) { try { final parts = dateKey.split('-'); if (parts.length == 3) { final date = DateTime( int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2]), ); if (date.isBefore(cutoffDate)) { await _database.child('history/$dateKey').remove(); print('Deleted old history: $dateKey'); } } } catch (e) { print('Error parsing date key $dateKey: $e'); } } } } catch (e) { print('Error clearing old history: $e'); } } }