From c3b68edba1ae1a875f5c69dbf542bd8e898191ea Mon Sep 17 00:00:00 2001 From: Wizznu Date: Mon, 12 Jan 2026 18:31:42 +0700 Subject: [PATCH] tersingkrong per tanggal 12 januari 2026 --- lib/firebase_options.dart | 3 +- lib/screens/dashboard_page.dart | 12 +- lib/screens/kontrol_page.dart | 173 +++++++--- lib/screens/sensor_config_page.dart | 149 ++++++++- lib/screens/waktu_config_page.dart | 174 +++++++++- lib/services/firebase_database_service.dart | 58 +++- lib/services/kontrol_automation_service.dart | 323 +++++++++++++++++++ 7 files changed, 801 insertions(+), 91 deletions(-) create mode 100644 lib/services/kontrol_automation_service.dart diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 10cdc7e..cf3d44e 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -96,5 +96,4 @@ class DefaultFirebaseOptions { storageBucket: 'project-ta-951b4.firebasestorage.app', measurementId: 'G-SB8J6ZZQHZ', ); - -} \ No newline at end of file +} diff --git a/lib/screens/dashboard_page.dart b/lib/screens/dashboard_page.dart index bbd664c..5a49eb0 100644 --- a/lib/screens/dashboard_page.dart +++ b/lib/screens/dashboard_page.dart @@ -356,9 +356,10 @@ class _DashboardPageState extends State { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: isConnected - ? Colors.green.withOpacity(0.1) - : Colors.red.withOpacity(0.1), + color: + isConnected + ? Colors.green.withOpacity(0.1) + : Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all( color: isConnected ? Colors.green : Colors.red, @@ -447,10 +448,7 @@ class _DashboardPageState extends State { padding: const EdgeInsets.only(left: 4, bottom: 2), child: Text( unit, - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), ), ], diff --git a/lib/screens/kontrol_page.dart b/lib/screens/kontrol_page.dart index 6f3a387..974db81 100644 --- a/lib/screens/kontrol_page.dart +++ b/lib/screens/kontrol_page.dart @@ -3,6 +3,7 @@ import '../theme/app_color.dart'; import '../widgets/kontrol_widgets.dart'; import 'pot_selection_page.dart'; import '../services/kontrol_storage.dart'; +import '../services/firebase_database_service.dart'; class KontrolPage extends StatefulWidget { const KontrolPage({super.key}); @@ -112,6 +113,8 @@ class ManualControlPage extends StatefulWidget { } class _ManualControlPageState extends State { + final FirebaseDatabaseService _dbService = FirebaseDatabaseService(); + bool _pompaAir = false; bool _pompaNutrisi = false; @@ -122,49 +125,100 @@ class _ManualControlPageState extends State { @override void initState() { super.initState(); - _loadSavedState(); + _loadFirebaseState(); + _listenToFirebaseChanges(); } - Future _loadSavedState() async { - final data = await KontrolStorage.loadManualControl(); - setState(() { - _pompaAir = data['pompaAir']; - _pompaNutrisi = data['pompaNutrisi']; - _potStatus = data['pots']; - _isLoading = false; + /// Load initial state from Firebase + Future _loadFirebaseState() async { + try { + final aktuatorData = await _dbService.getAktuatorStream().first; + if (mounted) { + setState(() { + _pompaAir = aktuatorData['mosvet_1'] ?? false; + _pompaNutrisi = aktuatorData['mosvet_2'] ?? false; + _potStatus = [ + aktuatorData['mosvet_3'] ?? false, + aktuatorData['mosvet_4'] ?? false, + aktuatorData['mosvet_5'] ?? false, + aktuatorData['mosvet_6'] ?? false, + aktuatorData['mosvet_7'] ?? false, + ]; + _isLoading = false; + }); + } + } catch (e) { + print('Error loading Firebase state: $e'); + setState(() => _isLoading = false); + } + } + + /// Listen to real-time Firebase changes + void _listenToFirebaseChanges() { + _dbService.getAktuatorStream().listen((aktuatorData) { + if (mounted) { + setState(() { + _pompaAir = aktuatorData['mosvet_1'] ?? false; + _pompaNutrisi = aktuatorData['mosvet_2'] ?? false; + _potStatus = [ + aktuatorData['mosvet_3'] ?? false, + aktuatorData['mosvet_4'] ?? false, + aktuatorData['mosvet_5'] ?? false, + aktuatorData['mosvet_6'] ?? false, + aktuatorData['mosvet_7'] ?? false, + ]; + }); + } }); } Future _jalankan() async { - // Save state - await KontrolStorage.saveManualControl( - pompaAir: _pompaAir, - pompaNutrisi: _pompaNutrisi, - pots: _potStatus, - ); + try { + // Update Firebase with current state + await _dbService.setMultipleAktuator({ + 'mosvet_1': _pompaAir, + 'mosvet_2': _pompaNutrisi, + 'mosvet_3': _potStatus[0], + 'mosvet_4': _potStatus[1], + 'mosvet_5': _potStatus[2], + 'mosvet_6': _potStatus[3], + 'mosvet_7': _potStatus[4], + }); - // Show which devices are running - List activeDevices = []; - if (_pompaAir) activeDevices.add('Pompa Air'); - if (_pompaNutrisi) activeDevices.add('Pompa Nutrisi'); - for (int i = 0; i < _potStatus.length; i++) { - if (_potStatus[i]) activeDevices.add('POT ${i + 1}'); - } - - if (activeDevices.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('Tidak ada perangkat yang diaktifkan'), - backgroundColor: Colors.orange, - ), + // Also save to local storage as backup + await KontrolStorage.saveManualControl( + pompaAir: _pompaAir, + pompaNutrisi: _pompaNutrisi, + pots: _potStatus, ); - } else { + + // Show which devices are running + List activeDevices = []; + if (_pompaAir) activeDevices.add('Pompa Air'); + if (_pompaNutrisi) activeDevices.add('Pompa Nutrisi'); + for (int i = 0; i < _potStatus.length; i++) { + if (_potStatus[i]) activeDevices.add('POT ${i + 1}'); + } + + if (activeDevices.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Tidak ada perangkat yang diaktifkan'), + backgroundColor: Colors.orange, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('✓ Berhasil: ${activeDevices.join(', ')}'), + backgroundColor: AppColor.primary, + duration: const Duration(seconds: 3), + ), + ); + } + } catch (e) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Menjalankan: ${activeDevices.join(', ')}'), - backgroundColor: AppColor.primary, - duration: const Duration(seconds: 3), - ), + SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red), ); } } @@ -182,19 +236,37 @@ class _ManualControlPageState extends State { ControlSwitchCard( title: 'Pompa Air', isActive: _pompaAir, - onPressed: () { - setState(() { - _pompaAir = !_pompaAir; - }); + onPressed: () async { + setState(() => _pompaAir = !_pompaAir); + try { + await _dbService.setPompaAir(_pompaAir); + } catch (e) { + setState(() => _pompaAir = !_pompaAir); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + backgroundColor: Colors.red, + ), + ); + } }, ), ControlSwitchCard( title: 'Pompa Nutrisi', isActive: _pompaNutrisi, - onPressed: () { - setState(() { - _pompaNutrisi = !_pompaNutrisi; - }); + onPressed: () async { + setState(() => _pompaNutrisi = !_pompaNutrisi); + try { + await _dbService.setPompaPupuk(_pompaNutrisi); + } catch (e) { + setState(() => _pompaNutrisi = !_pompaNutrisi); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + backgroundColor: Colors.red, + ), + ); + } }, ), @@ -219,10 +291,19 @@ class _ManualControlPageState extends State { return ControlSwitchCard( title: 'POT ${index + 1}', isActive: _potStatus[index], - onPressed: () { - setState(() { - _potStatus[index] = !_potStatus[index]; - }); + onPressed: () async { + setState(() => _potStatus[index] = !_potStatus[index]); + try { + await _dbService.setPot(index + 1, _potStatus[index]); + } catch (e) { + setState(() => _potStatus[index] = !_potStatus[index]); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + backgroundColor: Colors.red, + ), + ); + } }, ); }), diff --git a/lib/screens/sensor_config_page.dart b/lib/screens/sensor_config_page.dart index 1babfd3..1ba1230 100644 --- a/lib/screens/sensor_config_page.dart +++ b/lib/screens/sensor_config_page.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import '../theme/app_color.dart'; import '../widgets/kontrol_widgets.dart'; import '../services/kontrol_storage.dart'; +import '../services/firebase_database_service.dart'; +import '../services/kontrol_automation_service.dart'; class SensorConfigPage extends StatefulWidget { final String potName; @@ -18,8 +20,13 @@ class SensorConfigPage extends StatefulWidget { } class _SensorConfigPageState extends State { + final FirebaseDatabaseService _dbService = FirebaseDatabaseService(); + final KontrolAutomationService _automationService = + KontrolAutomationService(); + bool _isSaved = false; bool _isLoading = true; + bool _isSensorModeActive = false; // Konfigurasi sensor untuk pot ini Map _sensorConfig = { @@ -33,6 +40,7 @@ class _SensorConfigPageState extends State { void initState() { super.initState(); _loadSavedConfig(); + _loadSensorModeStatus(); } Future _loadSavedConfig() async { @@ -63,6 +71,17 @@ class _SensorConfigPageState extends State { }); } + Future _loadSensorModeStatus() async { + try { + final kontrolConfig = await _dbService.getKontrolConfig(); + setState(() { + _isSensorModeActive = kontrolConfig['otomatis'] ?? false; + }); + } catch (e) { + debugPrint('Error loading sensor mode status: $e'); + } + } + @override Widget build(BuildContext context) { return WillPopScope( @@ -134,6 +153,91 @@ class _SensorConfigPageState extends State { color: AppColor.textDark, ), ), + const SizedBox(height: 12), + + // Toggle Mode Sensor + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: + _isSensorModeActive + ? AppColor.primary.withOpacity(0.1) + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + _isSensorModeActive + ? AppColor.primary + : Colors.grey.shade300, + width: 2, + ), + ), + child: Row( + children: [ + Icon( + _isSensorModeActive + ? Icons.sensors + : Icons.sensors_outlined, + color: + _isSensorModeActive + ? AppColor.primary + : Colors.grey, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Mode Sensor', + style: TextStyle( + fontWeight: FontWeight.w600, + color: + _isSensorModeActive + ? AppColor.primary + : Colors.grey.shade700, + ), + ), + Text( + _isSensorModeActive + ? 'Aktif - Monitoring otomatis' + : 'Nonaktif', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Switch( + value: _isSensorModeActive, + onChanged: (value) async { + setState(() => _isSensorModeActive = value); + try { + await _dbService.setOtomatis(value); + if (value) { + _automationService.startSensorMode(); + } else { + _automationService.stopSensorMode(); + } + } catch (e) { + setState( + () => _isSensorModeActive = !value, + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + backgroundColor: Colors.red, + ), + ); + } + }, + activeColor: AppColor.primary, + ), + ], + ), + ), const SizedBox(height: 16), Expanded( @@ -458,23 +562,42 @@ class _SensorConfigPageState extends State { } Future _handleSaveOrUpdate() async { - // Save to storage - await KontrolStorage.saveSensorConfig(widget.potName, _sensorConfig); + try { + // Save to local storage + await KontrolStorage.saveSensorConfig(widget.potName, _sensorConfig); - setState(() { - _isSaved = true; - }); + // Update Firebase dengan konfigurasi sensor + await _dbService.setThreshold( + batasAtas: int.tryParse(_sensorConfig['batasMaksimal']) ?? 80, + batasBawah: int.tryParse(_sensorConfig['batasMinimal']) ?? 30, + ); - if (!mounted) return; + await _dbService.updateKontrolConfig({'otomatis': _isSensorModeActive}); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Konfigurasi ${widget.potName} berhasil ${_isSaved ? 'diupdate' : 'disimpan'}', + setState(() { + _isSaved = true; + }); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '✓ Konfigurasi ${widget.potName} berhasil ${_isSaved ? 'diupdate' : 'disimpan'}', + ), + backgroundColor: AppColor.primary, ), - backgroundColor: AppColor.primary, - ), - ); + ); + + // Start automation jika mode aktif + if (_isSensorModeActive) { + _automationService.startSensorMode(); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red), + ); + } } Future _copyToAllPots() async { diff --git a/lib/screens/waktu_config_page.dart b/lib/screens/waktu_config_page.dart index 892f0a6..612651e 100644 --- a/lib/screens/waktu_config_page.dart +++ b/lib/screens/waktu_config_page.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import '../theme/app_color.dart'; import '../widgets/kontrol_widgets.dart'; import '../services/kontrol_storage.dart'; +import '../services/firebase_database_service.dart'; +import '../services/kontrol_automation_service.dart'; class WaktuConfigPage extends StatefulWidget { final String potName; @@ -18,8 +20,13 @@ class WaktuConfigPage extends StatefulWidget { } class _WaktuConfigPageState extends State { + final FirebaseDatabaseService _dbService = FirebaseDatabaseService(); + final KontrolAutomationService _automationService = + KontrolAutomationService(); + bool _isSaved = false; bool _isLoading = true; + bool _isWaktuModeActive = false; // 2 Jadwal penyiraman untuk pot ini List> _jadwalPenyiraman = [ @@ -43,6 +50,7 @@ class _WaktuConfigPageState extends State { void initState() { super.initState(); _loadSavedConfig(); + _loadWaktuModeStatus(); } Future _loadSavedConfig() async { @@ -76,6 +84,17 @@ class _WaktuConfigPageState extends State { }); } + Future _loadWaktuModeStatus() async { + try { + final kontrolConfig = await _dbService.getKontrolConfig(); + setState(() { + _isWaktuModeActive = kontrolConfig['waktu'] ?? false; + }); + } catch (e) { + debugPrint('Error loading waktu mode status: $e'); + } + } + @override Widget build(BuildContext context) { return WillPopScope( @@ -147,6 +166,91 @@ class _WaktuConfigPageState extends State { color: AppColor.textDark, ), ), + const SizedBox(height: 12), + + // Toggle Mode Waktu + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: + _isWaktuModeActive + ? AppColor.primary.withOpacity(0.1) + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + _isWaktuModeActive + ? AppColor.primary + : Colors.grey.shade300, + width: 2, + ), + ), + child: Row( + children: [ + Icon( + _isWaktuModeActive + ? Icons.schedule + : Icons.schedule_outlined, + color: + _isWaktuModeActive + ? AppColor.primary + : Colors.grey, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Mode Waktu', + style: TextStyle( + fontWeight: FontWeight.w600, + color: + _isWaktuModeActive + ? AppColor.primary + : Colors.grey.shade700, + ), + ), + Text( + _isWaktuModeActive + ? 'Aktif - Penyiraman otomatis' + : 'Nonaktif', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Switch( + value: _isWaktuModeActive, + onChanged: (value) async { + setState(() => _isWaktuModeActive = value); + try { + await _dbService.updateKontrolConfig({ + 'waktu': value, + }); + if (value) { + _automationService.startWaktuMode(); + } else { + _automationService.stopWaktuMode(); + } + } catch (e) { + setState(() => _isWaktuModeActive = !value); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + backgroundColor: Colors.red, + ), + ); + } + }, + activeColor: AppColor.primary, + ), + ], + ), + ), const SizedBox(height: 16), Expanded( @@ -445,23 +549,67 @@ class _WaktuConfigPageState extends State { } Future _handleSaveOrUpdate() async { - // Save to storage - await KontrolStorage.saveWaktuConfig(widget.potName, _jadwalPenyiraman); + try { + // Save to local storage + await KontrolStorage.saveWaktuConfig(widget.potName, _jadwalPenyiraman); - setState(() { - _isSaved = true; - }); + // Convert durasi to seconds untuk Firebase + final durasi1Detik = _convertToSeconds( + _jadwalPenyiraman[0]['durasi'], + _jadwalPenyiraman[0]['durasiUnit'], + ); + final durasi2Detik = _convertToSeconds( + _jadwalPenyiraman[1]['durasi'], + _jadwalPenyiraman[1]['durasiUnit'], + ); - if (!mounted) return; + // Update Firebase dengan konfigurasi waktu + await _dbService.updateKontrolConfig({ + 'waktu_1': _jadwalPenyiraman[0]['jamMulai'], + 'waktu_2': _jadwalPenyiraman[1]['jamMulai'], + 'durasi_1': durasi1Detik, + 'durasi_2': durasi2Detik, + 'waktu': _isWaktuModeActive, + }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Konfigurasi ${widget.potName} berhasil ${_isSaved ? 'diupdate' : 'disimpan'}', + setState(() { + _isSaved = true; + }); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '✓ Konfigurasi ${widget.potName} berhasil ${_isSaved ? 'diupdate' : 'disimpan'}', + ), + backgroundColor: AppColor.primary, ), - backgroundColor: AppColor.primary, - ), - ); + ); + + // Start automation jika mode aktif + if (_isWaktuModeActive) { + _automationService.startWaktuMode(); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red), + ); + } + } + + int _convertToSeconds(String durasi, String unit) { + final value = int.tryParse(durasi) ?? 10; + switch (unit) { + case 'detik': + return value; + case 'menit': + return value * 60; + case 'jam': + return value * 3600; + default: + return value * 60; + } } Future _copyToAllPots() async { diff --git a/lib/services/firebase_database_service.dart b/lib/services/firebase_database_service.dart index 572eaad..e2d9bab 100644 --- a/lib/services/firebase_database_service.dart +++ b/lib/services/firebase_database_service.dart @@ -9,14 +9,12 @@ class FirebaseDatabaseService { 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, - ); + final data = Map.from(event.snapshot.value as Map); return { 'suhu': _parseValue(data['suhu']), 'kelembapan': _parseValue(data['kelembapan']), @@ -56,14 +54,12 @@ class FirebaseDatabaseService { } // ==================== 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, - ); + 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 @@ -88,8 +84,50 @@ class FirebaseDatabaseService { } } + /// 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 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, + }); + } + // ==================== KONTROL CONFIG ==================== - + /// Get real-time stream of kontrol configuration Stream> getKontrolStream() { return _database.child('kontrol').onValue.map((event) { @@ -178,7 +216,7 @@ class FirebaseDatabaseService { } // ==================== HELPER FUNCTIONS ==================== - + /// Parse value dari Firebase (handle empty string) String _parseValue(dynamic value) { if (value == null || value == '') { diff --git a/lib/services/kontrol_automation_service.dart b/lib/services/kontrol_automation_service.dart new file mode 100644 index 0000000..739a633 --- /dev/null +++ b/lib/services/kontrol_automation_service.dart @@ -0,0 +1,323 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'firebase_database_service.dart'; + +/// Service untuk menghandle logika otomatis kontrol waktu dan sensor +/// Berjalan di background untuk monitoring dan eksekusi otomatis +class KontrolAutomationService { + static final KontrolAutomationService _instance = + KontrolAutomationService._internal(); + factory KontrolAutomationService() => _instance; + KontrolAutomationService._internal(); + + final FirebaseDatabaseService _dbService = FirebaseDatabaseService(); + + Timer? _waktuCheckTimer; + Timer? _sensorCheckTimer; + StreamSubscription? _sensorSubscription; + StreamSubscription? _kontrolSubscription; + + bool _isWaktuModeActive = false; + bool _isSensorModeActive = false; + + // State untuk mencegah trigger berulang + Map _lastWateringTime = {}; + Map _isWateringActive = {}; + + // ==================== KONTROL WAKTU ==================== + + /// Start monitoring waktu mode + /// Cek setiap menit apakah ada jadwal yang harus dijalankan + void startWaktuMode() { + if (_isWaktuModeActive) return; + + _isWaktuModeActive = true; + debugPrint('🕐 Waktu Mode: Started'); + + // Cek setiap 30 detik + _waktuCheckTimer = Timer.periodic( + const Duration(seconds: 30), + (timer) => _checkScheduledWatering(), + ); + + // Jalankan check pertama kali + _checkScheduledWatering(); + } + + /// Stop monitoring waktu mode + void stopWaktuMode() { + _waktuCheckTimer?.cancel(); + _waktuCheckTimer = null; + _isWaktuModeActive = false; + debugPrint('🕐 Waktu Mode: Stopped'); + } + + /// Check apakah ada jadwal penyiraman yang harus dijalankan + Future _checkScheduledWatering() async { + try { + final kontrolConfig = await _dbService.getKontrolConfig(); + final waktuEnabled = kontrolConfig['waktu'] ?? false; + + if (!waktuEnabled || !_isWaktuModeActive) return; + + final now = DateTime.now(); + final currentTime = + '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}'; + + final waktu1 = kontrolConfig['waktu_1'] ?? ''; + final waktu2 = kontrolConfig['waktu_2'] ?? ''; + final durasi1 = kontrolConfig['durasi_1'] ?? 60; // detik + final durasi2 = kontrolConfig['durasi_2'] ?? 60; // detik + + // Check Jadwal 1 + if (waktu1.isNotEmpty && + _shouldStartWatering('jadwal_1', currentTime, waktu1)) { + debugPrint('🕐 Executing Jadwal 1 at $currentTime'); + await _executeWatering( + scheduleId: 'jadwal_1', + pompaAir: true, + pompaPupuk: true, + pots: [1, 2, 3, 4, 5], // Semua pot + durasiDetik: durasi1, + ); + } + + // Check Jadwal 2 + if (waktu2.isNotEmpty && + _shouldStartWatering('jadwal_2', currentTime, waktu2)) { + debugPrint('🕐 Executing Jadwal 2 at $currentTime'); + await _executeWatering( + scheduleId: 'jadwal_2', + pompaAir: true, + pompaPupuk: true, + pots: [1, 2, 3, 4, 5], // Semua pot + durasiDetik: durasi2, + ); + } + } catch (e) { + debugPrint('❌ Error checking scheduled watering: $e'); + } + } + + /// Check apakah jadwal harus dijalankan + bool _shouldStartWatering( + String scheduleId, + String currentTime, + String targetTime, + ) { + // Cek apakah waktu sekarang sama dengan target + if (currentTime != targetTime) return false; + + // Cek apakah sudah berjalan + if (_isWateringActive[scheduleId] == true) return false; + + // Cek apakah sudah pernah dijalankan dalam 1 menit terakhir (prevent double trigger) + final lastTime = _lastWateringTime[scheduleId]; + if (lastTime != null) { + final diff = DateTime.now().difference(lastTime); + if (diff.inSeconds < 60) return false; + } + + return true; + } + + /// Execute penyiraman dengan durasi tertentu + Future _executeWatering({ + required String scheduleId, + required bool pompaAir, + required bool pompaPupuk, + required List pots, + required int durasiDetik, + }) async { + try { + _isWateringActive[scheduleId] = true; + _lastWateringTime[scheduleId] = DateTime.now(); + + // Nyalakan pompa dan valve + final updates = {}; + if (pompaAir) updates['mosvet_1'] = true; + if (pompaPupuk) updates['mosvet_2'] = true; + + for (var pot in pots) { + if (pot >= 1 && pot <= 5) { + updates['mosvet_${pot + 2}'] = true; + } + } + + await _dbService.setMultipleAktuator(updates); + debugPrint('✅ Watering started: $updates'); + + // Tunggu sesuai durasi + await Future.delayed(Duration(seconds: durasiDetik)); + + // Matikan semua + final offUpdates = {}; + updates.forEach((key, _) => offUpdates[key] = false); + await _dbService.setMultipleAktuator(offUpdates); + + debugPrint('✅ Watering completed for $scheduleId'); + _isWateringActive[scheduleId] = false; + } catch (e) { + debugPrint('❌ Error executing watering: $e'); + _isWateringActive[scheduleId] = false; + } + } + + // ==================== KONTROL SENSOR ==================== + + /// Start monitoring sensor mode + void startSensorMode() { + if (_isSensorModeActive) return; + + _isSensorModeActive = true; + debugPrint('đŸŒĄī¸ Sensor Mode: Started'); + + // Monitor perubahan sensor setiap beberapa detik + _sensorCheckTimer = Timer.periodic( + const Duration(seconds: 10), + (timer) => _checkSensorThreshold(), + ); + + // Listen to sensor data real-time + _sensorSubscription = _dbService.getSensorDataStream().listen((sensorData) { + if (_isSensorModeActive) { + _processSensorData(sensorData); + } + }); + + // Jalankan check pertama kali + _checkSensorThreshold(); + } + + /// Stop monitoring sensor mode + void stopSensorMode() { + _sensorCheckTimer?.cancel(); + _sensorCheckTimer = null; + _sensorSubscription?.cancel(); + _sensorSubscription = null; + _isSensorModeActive = false; + debugPrint('đŸŒĄī¸ Sensor Mode: Stopped'); + } + + /// Check sensor threshold untuk semua pot + Future _checkSensorThreshold() async { + try { + final kontrolConfig = await _dbService.getKontrolConfig(); + final otomatisEnabled = kontrolConfig['otomatis'] ?? false; + + if (!otomatisEnabled || !_isSensorModeActive) return; + + final batasAtas = kontrolConfig['batas_atas'] ?? 100; + final batasBawah = kontrolConfig['batas_bawah'] ?? 40; + + final sensorData = await _dbService.getSensorData(); + + // Check setiap pot (soil_1 sampai soil_5) + for (int i = 1; i <= 5; i++) { + final soilKey = 'soil_$i'; + final soilValue = int.tryParse(sensorData[soilKey] ?? '0') ?? 0; + + // Jika kelembapan di bawah batas bawah, siram pot tersebut + if (soilValue < batasBawah) { + await _waterPotBySensor( + potNumber: i, + soilValue: soilValue, + batasBawah: batasBawah, + batasAtas: batasAtas, + ); + } + } + } catch (e) { + debugPrint('❌ Error checking sensor threshold: $e'); + } + } + + /// Process sensor data real-time + void _processSensorData(Map sensorData) { + // Could be used for real-time alerts or logging + // For now, the periodic check is sufficient + } + + /// Siram pot berdasarkan sensor + Future _waterPotBySensor({ + required int potNumber, + required int soilValue, + required int batasBawah, + required int batasAtas, + }) async { + final potKey = 'pot_$potNumber'; + + // Prevent multiple watering in short time + if (_isWateringActive[potKey] == true) return; + + final lastTime = _lastWateringTime[potKey]; + if (lastTime != null) { + final diff = DateTime.now().difference(lastTime); + if (diff.inMinutes < 5) return; // Minimum 5 menit antar penyiraman + } + + try { + _isWateringActive[potKey] = true; + _lastWateringTime[potKey] = DateTime.now(); + + debugPrint( + 'đŸŒĄī¸ Sensor Mode: Watering POT $potNumber (soil: $soilValue < $batasBawah)', + ); + + // Nyalakan pompa air dan valve pot tersebut + await _dbService.setPompaAir(true); + await _dbService.setPot(potNumber, true); + + // Siram sampai mencapai batas atas atau max 60 detik + int elapsedSeconds = 0; + const maxDuration = 60; + + while (elapsedSeconds < maxDuration && _isSensorModeActive) { + await Future.delayed(const Duration(seconds: 5)); + elapsedSeconds += 5; + + // Check sensor lagi + final currentData = await _dbService.getSensorData(); + final currentSoil = + int.tryParse(currentData['soil_$potNumber'] ?? '0') ?? 0; + + if (currentSoil >= batasAtas) { + debugPrint( + '✅ POT $potNumber reached target: $currentSoil >= $batasAtas', + ); + break; + } + } + + // Matikan pompa dan valve + await _dbService.setPompaAir(false); + await _dbService.setPot(potNumber, false); + + debugPrint('✅ Sensor Mode: Watering completed for POT $potNumber'); + _isWateringActive[potKey] = false; + } catch (e) { + debugPrint('❌ Error watering pot by sensor: $e'); + _isWateringActive[potKey] = false; + } + } + + // ==================== UTILITY ==================== + + /// Get status semua automation + Map getAutomationStatus() { + return {'waktuMode': _isWaktuModeActive, 'sensorMode': _isSensorModeActive}; + } + + /// Stop semua automation + void stopAll() { + stopWaktuMode(); + stopSensorMode(); + debugPrint('âšī¸ All automation stopped'); + } + + /// Cleanup saat app dispose + void dispose() { + stopAll(); + _kontrolSubscription?.cancel(); + } +}