TKK_E32222484/lib/monitoring_page.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],
),
],
),
),
);
}
}