tersingkrong per tanggal 12 januari 2026
This commit is contained in:
parent
2586f744a9
commit
c3b68edba1
|
|
@ -96,5 +96,4 @@ class DefaultFirebaseOptions {
|
|||
storageBucket: 'project-ta-951b4.firebasestorage.app',
|
||||
measurementId: 'G-SB8J6ZZQHZ',
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 == '') {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue