tersingkrong per tanggal 12 januari 2026

This commit is contained in:
Wizznu 2026-01-12 18:31:42 +07:00
parent 2586f744a9
commit c3b68edba1
7 changed files with 801 additions and 91 deletions

View File

@ -96,5 +96,4 @@ class DefaultFirebaseOptions {
storageBucket: 'project-ta-951b4.firebasestorage.app',
measurementId: 'G-SB8J6ZZQHZ',
);
}
}

View File

@ -356,9 +356,10 @@ class _DashboardPageState extends State<DashboardPage> {
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<DashboardPage> {
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]),
),
),
],

View File

@ -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<ManualControlPage> {
final FirebaseDatabaseService _dbService = FirebaseDatabaseService();
bool _pompaAir = false;
bool _pompaNutrisi = false;
@ -122,49 +125,100 @@ class _ManualControlPageState extends State<ManualControlPage> {
@override
void initState() {
super.initState();
_loadSavedState();
_loadFirebaseState();
_listenToFirebaseChanges();
}
Future<void> _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<void> _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<void> _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<String> 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<String> 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<ManualControlPage> {
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<ManualControlPage> {
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,
),
);
}
},
);
}),

View File

@ -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<SensorConfigPage> {
final FirebaseDatabaseService _dbService = FirebaseDatabaseService();
final KontrolAutomationService _automationService =
KontrolAutomationService();
bool _isSaved = false;
bool _isLoading = true;
bool _isSensorModeActive = false;
// Konfigurasi sensor untuk pot ini
Map<String, dynamic> _sensorConfig = {
@ -33,6 +40,7 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
void initState() {
super.initState();
_loadSavedConfig();
_loadSensorModeStatus();
}
Future<void> _loadSavedConfig() async {
@ -63,6 +71,17 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
});
}
Future<void> _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<SensorConfigPage> {
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<SensorConfigPage> {
}
Future<void> _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<void> _copyToAllPots() async {

View File

@ -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<WaktuConfigPage> {
final FirebaseDatabaseService _dbService = FirebaseDatabaseService();
final KontrolAutomationService _automationService =
KontrolAutomationService();
bool _isSaved = false;
bool _isLoading = true;
bool _isWaktuModeActive = false;
// 2 Jadwal penyiraman untuk pot ini
List<Map<String, dynamic>> _jadwalPenyiraman = [
@ -43,6 +50,7 @@ class _WaktuConfigPageState extends State<WaktuConfigPage> {
void initState() {
super.initState();
_loadSavedConfig();
_loadWaktuModeStatus();
}
Future<void> _loadSavedConfig() async {
@ -76,6 +84,17 @@ class _WaktuConfigPageState extends State<WaktuConfigPage> {
});
}
Future<void> _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<WaktuConfigPage> {
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<WaktuConfigPage> {
}
Future<void> _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<void> _copyToAllPots() async {

View File

@ -9,14 +9,12 @@ class FirebaseDatabaseService {
final DatabaseReference _database = FirebaseDatabase.instance.ref();
// ==================== SENSOR DATA ====================
/// Get real-time stream of sensor data
Stream<Map<String, dynamic>> getSensorDataStream() {
return _database.child('data').onValue.map((event) {
if (event.snapshot.value != null) {
final data = Map<String, dynamic>.from(
event.snapshot.value as Map,
);
final data = Map<String, dynamic>.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<Map<String, bool>> getAktuatorStream() {
return _database.child('aktuator').onValue.map((event) {
if (event.snapshot.value != null) {
final data = Map<String, dynamic>.from(
event.snapshot.value as Map,
);
final data = Map<String, dynamic>.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<void> setPompaAir(bool value) async {
await setAktuator('mosvet_1', value);
}
/// Set pompa pupuk (mosvet_2)
Future<void> 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<void> 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<void> setMultipleAktuator(Map<String, bool> aktuatorStates) async {
try {
await _database.child('aktuator').update(aktuatorStates);
} catch (e) {
print('Error setting multiple aktuator: $e');
rethrow;
}
}
/// Matikan semua aktuator
Future<void> 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<Map<String, dynamic>> 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 == '') {

View File

@ -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<String, DateTime> _lastWateringTime = {};
Map<String, bool> _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<void> _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<void> _executeWatering({
required String scheduleId,
required bool pompaAir,
required bool pompaPupuk,
required List<int> pots,
required int durasiDetik,
}) async {
try {
_isWateringActive[scheduleId] = true;
_lastWateringTime[scheduleId] = DateTime.now();
// Nyalakan pompa dan valve
final updates = <String, bool>{};
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 = <String, bool>{};
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<void> _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<String, dynamic> sensorData) {
// Could be used for real-time alerts or logging
// For now, the periodic check is sufficient
}
/// Siram pot berdasarkan sensor
Future<void> _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<String, bool> 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();
}
}