440 lines
14 KiB
Dart
440 lines
14 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
class MonitoringPage extends StatefulWidget {
|
|
const MonitoringPage({super.key});
|
|
|
|
@override
|
|
_MonitoringPageState createState() => _MonitoringPageState();
|
|
}
|
|
|
|
class _MonitoringPageState extends State<MonitoringPage> {
|
|
// Referensi ke node 'monitoring' untuk data sensor dan status aktual perangkat
|
|
final dbMonitoringRef = FirebaseDatabase.instance.ref("monitoring");
|
|
// Referensi ke node 'kontrol' untuk perintah manual
|
|
final dbControlRef = FirebaseDatabase.instance.ref("kontrol");
|
|
// Referensi ke node 'histori' untuk log aktivitas
|
|
final historiRef = FirebaseDatabase.instance.ref("histori");
|
|
|
|
// Variabel untuk data sensor
|
|
double ph = 0, tds = 0, suhu = 0;
|
|
|
|
// Variabel status aktual perangkat (dari /monitoring)
|
|
bool fanStatus = false;
|
|
bool pompaUtamaStatus = false;
|
|
bool abmixHabis = false; // Status sensor level air AB Mix
|
|
bool pompaAbMixStatus =
|
|
false; // Status aktual ON/OFF pompa AB Mix (baru dari Arduino)
|
|
|
|
// Variabel kontrol mode manual (dari /kontrol)
|
|
bool manualMode = false;
|
|
|
|
String waktu = DateFormat('HH:mm:ss').format(DateTime.now());
|
|
late Timer _timer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Timer untuk update waktu lokal setiap detik
|
|
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
setState(() {
|
|
waktu = DateFormat('HH:mm:ss').format(DateTime.now());
|
|
});
|
|
});
|
|
|
|
// --- LOGIKA INISIALISASI NODE /KONTROL ---
|
|
dbControlRef
|
|
.once()
|
|
.then((DatabaseEvent event) {
|
|
if (!event.snapshot.exists) {
|
|
dbControlRef
|
|
.set({
|
|
"mode_manual": false,
|
|
"pompa": false,
|
|
"fan": false,
|
|
"abmix": false,
|
|
})
|
|
.then((_) {
|
|
_showSnackBar(
|
|
"Node /kontrol berhasil diinisialisasi di Firebase.",
|
|
);
|
|
})
|
|
.catchError((e) {
|
|
_showSnackBar("Gagal menginisialisasi node /kontrol: $e");
|
|
});
|
|
}
|
|
})
|
|
.catchError((e) {
|
|
_showSnackBar("Gagal memeriksa keberadaan node /kontrol: $e");
|
|
});
|
|
// --- AKHIR LOGIKA INISIALISASI ---
|
|
|
|
// Listener untuk data monitoring (sensor dan status aktual perangkat dari Arduino)
|
|
dbMonitoringRef.onValue.listen((event) {
|
|
if (event.snapshot.value != null) {
|
|
final data = Map<String, dynamic>.from(event.snapshot.value as Map);
|
|
|
|
final bool oldAbmixHabis = abmixHabis;
|
|
|
|
setState(() {
|
|
ph = (data['ph'] ?? 0).toDouble();
|
|
tds = (data['tds'] ?? 0).toDouble();
|
|
suhu = (data['suhu_udara'] ?? 0).toDouble();
|
|
fanStatus = data['fan'] ?? false;
|
|
pompaUtamaStatus = data['pompa'] ?? false;
|
|
abmixHabis = data['abmix_habis'] ?? false;
|
|
pompaAbMixStatus = data['pompa_abmix'] ?? false;
|
|
});
|
|
|
|
if (oldAbmixHabis != abmixHabis && abmixHabis) {
|
|
_showSnackBar("⚠️ AB MIX HABIS!");
|
|
}
|
|
}
|
|
});
|
|
|
|
// Listener untuk data kontrol (mode manual dari Firebase)
|
|
dbControlRef.onValue.listen((event) {
|
|
if (event.snapshot.value != null) {
|
|
final dataControl = Map<String, dynamic>.from(
|
|
event.snapshot.value as Map,
|
|
);
|
|
setState(() {
|
|
manualMode = dataControl['mode_manual'] ?? false;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer.cancel(); // Hentikan timer di sini
|
|
super.dispose();
|
|
}
|
|
|
|
// Fungsi untuk menampilkan SnackBar di bagian bawah layar
|
|
void _showSnackBar(String pesan) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(pesan)));
|
|
}
|
|
|
|
// Fungsi untuk mencatat histori ke Firebase dari Flutter (untuk perintah manual)
|
|
void _logHistoriManual(String aksi) {
|
|
historiRef
|
|
.push()
|
|
.set({
|
|
"timestamp": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
|
|
"aksi": aksi,
|
|
})
|
|
.catchError((e) {
|
|
_showSnackBar("❌ Gagal mencatat histori manual: $e");
|
|
});
|
|
}
|
|
|
|
// Fungsi untuk mengontrol perangkat (hanya dalam mode manual)
|
|
void toggleDevice(String keyFirebase, bool currentStatus) {
|
|
final newValue = !currentStatus;
|
|
String label = "";
|
|
String statusText = newValue ? "ON" : "OFF";
|
|
|
|
if (keyFirebase == "fan") {
|
|
label = "Kipas";
|
|
// Optimistic UI update: langsung ubah status di UI
|
|
setState(() {
|
|
fanStatus = newValue;
|
|
});
|
|
} else if (keyFirebase == "pompa") {
|
|
label = "Pompa Utama";
|
|
// Optimistic UI update: langsung ubah status di UI
|
|
setState(() {
|
|
pompaUtamaStatus = newValue;
|
|
});
|
|
} else if (keyFirebase == "abmix") {
|
|
label = "Pompa AB Mix";
|
|
// Optimistic UI update: langsung ubah status di UI
|
|
setState(() {
|
|
pompaAbMixStatus = newValue;
|
|
});
|
|
}
|
|
|
|
dbControlRef
|
|
.update({keyFirebase: newValue})
|
|
.then((_) {
|
|
_logHistoriManual("$label $statusText (Perintah Manual)");
|
|
_showSnackBar("✅ Perintah $label berhasil dikirim: $statusText");
|
|
})
|
|
.catchError((e) {
|
|
_showSnackBar("❌ Gagal mengirim perintah $label: $e");
|
|
// Revert UI jika ada error
|
|
setState(() {
|
|
if (keyFirebase == "fan")
|
|
fanStatus = !newValue;
|
|
else if (keyFirebase == "pompa")
|
|
pompaUtamaStatus = !newValue;
|
|
else if (keyFirebase == "abmix")
|
|
pompaAbMixStatus = !newValue;
|
|
});
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Monitoring Selada',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
'Jam: $waktu',
|
|
style: TextStyle(fontSize: 14, color: Colors.white70),
|
|
),
|
|
],
|
|
),
|
|
centerTitle: false,
|
|
backgroundColor: Colors.green[700],
|
|
elevation: 0,
|
|
),
|
|
body: monitoringContent(),
|
|
);
|
|
}
|
|
|
|
// Widget utama untuk konten halaman monitoring
|
|
Widget monitoringContent() {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.green.shade50, Colors.green.shade100],
|
|
),
|
|
),
|
|
child: SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Bagian Judul dan Waktu sebelumnya sudah di AppBar, jadi ini dihapus
|
|
// const SizedBox(height: 25), // Jarak ini juga bisa dikurangi atau dihapus
|
|
|
|
// Bagian Informasi Sensor
|
|
Text(
|
|
"Data Sensor",
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green[800],
|
|
),
|
|
),
|
|
const SizedBox(height: 15),
|
|
infoCard("pH", ph.toStringAsFixed(2), Icons.science),
|
|
infoCard(
|
|
"TDS",
|
|
"${tds.toStringAsFixed(0)} ppm",
|
|
Icons.water_drop,
|
|
),
|
|
infoCard(
|
|
"Suhu Udara",
|
|
"${suhu.toStringAsFixed(1)} °C",
|
|
Icons.thermostat,
|
|
),
|
|
const SizedBox(height: 25),
|
|
|
|
// Bagian Kontrol Sistem
|
|
Text(
|
|
"Kontrol Sistem",
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green[800],
|
|
),
|
|
),
|
|
const SizedBox(height: 15),
|
|
|
|
// Switch untuk Mode Manual
|
|
switchCard("Mode Manual", manualMode, (newValue) {
|
|
// Optimistic UI update untuk mode manual
|
|
setState(() {
|
|
manualMode = newValue;
|
|
});
|
|
dbControlRef
|
|
.update({"mode_manual": newValue})
|
|
.then((_) {
|
|
_logHistoriManual(
|
|
"Mode Manual ${newValue ? 'ON' : 'OFF'}",
|
|
);
|
|
})
|
|
.catchError((e) {
|
|
_showSnackBar("Gagal mengubah mode manual: $e");
|
|
// Revert UI jika ada error
|
|
setState(() {
|
|
manualMode = !newValue;
|
|
});
|
|
});
|
|
}),
|
|
|
|
// Switch untuk Kipas (hanya bisa diubah jika manualMode aktif)
|
|
switchCard(
|
|
"Kipas",
|
|
fanStatus,
|
|
manualMode
|
|
? (newValue) => toggleDevice("fan", fanStatus)
|
|
: null,
|
|
),
|
|
// Switch untuk Pompa Utama (hanya bisa diubah jika manualMode aktif)
|
|
switchCard(
|
|
"Pompa Utama",
|
|
pompaUtamaStatus,
|
|
manualMode
|
|
? (newValue) => toggleDevice("pompa", pompaUtamaStatus)
|
|
: null,
|
|
),
|
|
// Switch untuk Pompa AB Mix (hanya bisa diubah jika manualMode aktif)
|
|
switchCard(
|
|
"Pompa AB Mix",
|
|
pompaAbMixStatus,
|
|
manualMode
|
|
? (newValue) => toggleDevice("abmix", pompaAbMixStatus)
|
|
: null,
|
|
),
|
|
|
|
const SizedBox(height: 15),
|
|
|
|
// Pesan jika mode manual tidak aktif
|
|
if (!manualMode)
|
|
Container(
|
|
padding: const EdgeInsets.all(12.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.shade100,
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: Colors.orange.shade300),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.lock, color: Colors.orange[700]),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
"Aktifkan Mode Manual untuk kontrol perangkat",
|
|
style: TextStyle(
|
|
color: Colors.orange[700],
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
// Pesan peringatan AB MIX HABIS
|
|
if (abmixHabis)
|
|
Container(
|
|
padding: const EdgeInsets.all(12.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.shade100,
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: Colors.red.shade300),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.warning, color: Colors.red[700]),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
"⚠️ AB MIX HABIS! Segera isi ulang.",
|
|
style: TextStyle(
|
|
color: Colors.red[700],
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Widget kustom untuk menampilkan informasi sensor dalam bentuk kartu yang lebih menarik
|
|
Widget infoCard(String label, String value, IconData icon) {
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, color: Colors.green[600], size: 35),
|
|
const SizedBox(width: 15),
|
|
Expanded(
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey[800],
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20,
|
|
color: Colors.green[800],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Widget kustom untuk menampilkan switch kontrol perangkat dalam bentuk kartu yang lebih menarik
|
|
Widget switchCard(String label, bool status, ValueChanged<bool>? onChanged) {
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
|
elevation: 4,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey[800],
|
|
),
|
|
),
|
|
),
|
|
Switch(
|
|
value: status,
|
|
onChanged: onChanged,
|
|
activeColor: Colors.green[600],
|
|
inactiveThumbColor: Colors.grey,
|
|
inactiveTrackColor: Colors.grey[300],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|