From 2586f744a9065d9b0d7b2269274dfafdac9a76a2 Mon Sep 17 00:00:00 2001 From: Wizznu Date: Mon, 12 Jan 2026 17:53:26 +0700 Subject: [PATCH] dashboard terhubung firebase --- lib/firebase_options.dart | 5 + lib/screens/dashboard_page.dart | 198 +++++++++++++---- lib/services/firebase_database_service.dart | 199 ++++++++++++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 24 +++ pubspec.yaml | 1 + 6 files changed, 393 insertions(+), 36 deletions(-) create mode 100644 lib/services/firebase_database_service.dart diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 50c6acc..10cdc7e 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -50,6 +50,7 @@ class DefaultFirebaseOptions { messagingSenderId: '217854138058', projectId: 'project-ta-951b4', storageBucket: 'project-ta-951b4.firebasestorage.app', + databaseURL: 'https://project-ta-951b4-default-rtdb.firebaseio.com', ); // Pilih project Anda > Project Settings > Your apps > Android app @@ -60,6 +61,7 @@ class DefaultFirebaseOptions { messagingSenderId: '217854138058', projectId: 'project-ta-951b4', storageBucket: 'project-ta-951b4.firebasestorage.app', + databaseURL: 'https://project-ta-951b4-default-rtdb.firebaseio.com', iosBundleId: 'com.kampus.apsgo', ); @@ -69,6 +71,7 @@ class DefaultFirebaseOptions { messagingSenderId: '217854138058', projectId: 'project-ta-951b4', authDomain: 'project-ta-951b4.firebaseapp.com', + databaseURL: 'https://project-ta-951b4-default-rtdb.firebaseio.com', storageBucket: 'project-ta-951b4.firebasestorage.app', measurementId: 'G-6ML8QQEGNZ', ); @@ -79,6 +82,7 @@ class DefaultFirebaseOptions { messagingSenderId: '217854138058', projectId: 'project-ta-951b4', storageBucket: 'project-ta-951b4.firebasestorage.app', + databaseURL: 'https://project-ta-951b4-default-rtdb.firebaseio.com', iosBundleId: 'com.example.projectTa', ); @@ -87,6 +91,7 @@ class DefaultFirebaseOptions { appId: '1:217854138058:web:233d93e01272a0870c4633', messagingSenderId: '217854138058', projectId: 'project-ta-951b4', + databaseURL: 'https://project-ta-951b4-default-rtdb.firebaseio.com', authDomain: 'project-ta-951b4.firebaseapp.com', storageBucket: 'project-ta-951b4.firebasestorage.app', measurementId: 'G-SB8J6ZZQHZ', diff --git a/lib/screens/dashboard_page.dart b/lib/screens/dashboard_page.dart index 4ca98c6..bbd664c 100644 --- a/lib/screens/dashboard_page.dart +++ b/lib/screens/dashboard_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../theme/app_color.dart'; import '../services/auth_service.dart'; +import '../services/firebase_database_service.dart'; import 'kontrol_page.dart'; import 'histori_page.dart'; @@ -13,6 +14,7 @@ class DashboardPage extends StatefulWidget { class _DashboardPageState extends State { int _selectedIndex = 0; + final _dbService = FirebaseDatabaseService(); final List> _pages = [ {'title': 'Dashboard', 'icon': Icons.dashboard}, @@ -248,47 +250,152 @@ class _DashboardPageState extends State { } Widget _buildDashboard() { - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Sensor Cards - Temperature, Humidity, Light - _buildSensorCard( - title: "Temperature", - value: "30", - icon: Icons.thermostat_outlined, - color: Colors.orange, - ), - _buildSensorCard( - title: "Humidity", - value: "70", - icon: Icons.water_drop_outlined, - color: Colors.blue, - ), - _buildSensorCard( - title: "Light", - value: "85", - icon: Icons.wb_sunny_outlined, - color: Colors.amber, - ), + return StreamBuilder>( + stream: _dbService.getSensorDataStream(), + builder: (context, snapshot) { + // Loading state + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } - const SizedBox(height: 12), + // Error state + if (snapshot.hasError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 48, color: Colors.red), + const SizedBox(height: 16), + Text('Error: ${snapshot.error}'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => setState(() {}), + child: const Text('Retry'), + ), + ], + ), + ); + } - // Pot Cards - Pot 1 to Pot 5 - _buildPotCard(potNumber: 1, soilMoisture: "45"), - _buildPotCard(potNumber: 2, soilMoisture: "52"), - _buildPotCard(potNumber: 3, soilMoisture: "38"), - _buildPotCard(potNumber: 4, soilMoisture: "61"), - _buildPotCard(potNumber: 5, soilMoisture: "48"), - ], - ), + // No data state + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.cloud_off, size: 48, color: Colors.grey), + SizedBox(height: 16), + Text('No data available'), + ], + ), + ); + } + + final data = snapshot.data!; + final suhu = data['suhu'] ?? '0'; + final kelembapan = data['kelembapan'] ?? '0'; + final ldr = data['ldr'] ?? '0'; + final soil1 = data['soil_1'] ?? '0'; + final soil2 = data['soil_2'] ?? '0'; + final soil3 = data['soil_3'] ?? '0'; + final soil4 = data['soil_4'] ?? '0'; + final soil5 = data['soil_5'] ?? '0'; + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Connection Status Indicator + _buildConnectionStatus(), + const SizedBox(height: 8), + + // Sensor Cards - Temperature, Humidity, Light + _buildSensorCard( + title: "Temperature", + value: suhu, + unit: "°C", + icon: Icons.thermostat_outlined, + color: Colors.orange, + ), + _buildSensorCard( + title: "Humidity", + value: kelembapan, + unit: "%", + icon: Icons.water_drop_outlined, + color: Colors.blue, + ), + _buildSensorCard( + title: "Light", + value: ldr, + unit: "%", + icon: Icons.wb_sunny_outlined, + color: Colors.amber, + ), + + const SizedBox(height: 12), + + // Pot Cards - Pot 1 to Pot 5 + _buildPotCard(potNumber: 1, soilMoisture: soil1), + _buildPotCard(potNumber: 2, soilMoisture: soil2), + _buildPotCard(potNumber: 3, soilMoisture: soil3), + _buildPotCard(potNumber: 4, soilMoisture: soil4), + _buildPotCard(potNumber: 5, soilMoisture: soil5), + ], + ), + ); + }, + ); + } + + Widget _buildConnectionStatus() { + return StreamBuilder( + stream: _dbService.getConnectionStatus(), + builder: (context, snapshot) { + final isConnected = snapshot.data ?? false; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + 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, + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: isConnected ? Colors.green : Colors.red, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Text( + isConnected ? 'Connected' : 'Disconnected', + style: TextStyle( + color: isConnected ? Colors.green : Colors.red, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + }, ); } Widget _buildSensorCard({ required String title, required String value, + String unit = '', required IconData icon, required Color color, }) { @@ -325,9 +432,28 @@ class _DashboardPageState extends State { ), ), ), - Text( - value, - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + value, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + if (unit.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 2), + child: Text( + unit, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ), + ], ), ], ), diff --git a/lib/services/firebase_database_service.dart b/lib/services/firebase_database_service.dart new file mode 100644 index 0000000..572eaad --- /dev/null +++ b/lib/services/firebase_database_service.dart @@ -0,0 +1,199 @@ +import 'package:firebase_database/firebase_database.dart'; + +class FirebaseDatabaseService { + static final FirebaseDatabaseService _instance = + FirebaseDatabaseService._internal(); + factory FirebaseDatabaseService() => _instance; + FirebaseDatabaseService._internal(); + + 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, + ); + return { + 'suhu': _parseValue(data['suhu']), + 'kelembapan': _parseValue(data['kelembapan']), + 'ldr': _parseValue(data['ldr']), + 'soil_1': _parseValue(data['soil_1']), + 'soil_2': _parseValue(data['soil_2']), + 'soil_3': _parseValue(data['soil_3']), + 'soil_4': _parseValue(data['soil_4']), + 'soil_5': _parseValue(data['soil_5']), + }; + } + return {}; + }); + } + + /// Get sensor data once + Future> getSensorData() async { + try { + final snapshot = await _database.child('data').get(); + if (snapshot.exists) { + final data = Map.from(snapshot.value as Map); + return { + 'suhu': _parseValue(data['suhu']), + 'kelembapan': _parseValue(data['kelembapan']), + 'ldr': _parseValue(data['ldr']), + 'soil_1': _parseValue(data['soil_1']), + 'soil_2': _parseValue(data['soil_2']), + 'soil_3': _parseValue(data['soil_3']), + 'soil_4': _parseValue(data['soil_4']), + 'soil_5': _parseValue(data['soil_5']), + }; + } + } catch (e) { + print('Error getting sensor data: $e'); + } + return {}; + } + + // ==================== 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, + ); + return { + 'mosvet_1': data['mosvet_1'] ?? false, // Pompa Air + 'mosvet_2': data['mosvet_2'] ?? false, // Pompa Pupuk + 'mosvet_3': data['mosvet_3'] ?? false, // Valve 1 + 'mosvet_4': data['mosvet_4'] ?? false, // Valve 2 + 'mosvet_5': data['mosvet_5'] ?? false, // Valve 3 + 'mosvet_6': data['mosvet_6'] ?? false, // Valve 4 + 'mosvet_7': data['mosvet_7'] ?? false, // Valve 5 + }; + } + return {}; + }); + } + + /// Set aktuator status (untuk kontrol manual) + Future setAktuator(String mosfetName, bool value) async { + try { + await _database.child('aktuator/$mosfetName').set(value); + } catch (e) { + print('Error setting aktuator: $e'); + rethrow; + } + } + + // ==================== KONTROL CONFIG ==================== + + /// Get real-time stream of kontrol configuration + Stream> getKontrolStream() { + return _database.child('kontrol').onValue.map((event) { + if (event.snapshot.value != null) { + return Map.from(event.snapshot.value as Map); + } + return {}; + }); + } + + /// Get kontrol configuration once + Future> getKontrolConfig() async { + try { + final snapshot = await _database.child('kontrol').get(); + if (snapshot.exists) { + return Map.from(snapshot.value as Map); + } + } catch (e) { + print('Error getting kontrol config: $e'); + } + return {}; + } + + /// Update kontrol configuration + Future updateKontrolConfig(Map config) async { + try { + await _database.child('kontrol').update(config); + } catch (e) { + print('Error updating kontrol config: $e'); + rethrow; + } + } + + /// Set batas threshold (batas_atas, batas_bawah) + Future setThreshold({int? batasAtas, int? batasBawah}) async { + try { + final updates = {}; + if (batasAtas != null) updates['batas_atas'] = batasAtas; + if (batasBawah != null) updates['batas_bawah'] = batasBawah; + await _database.child('kontrol').update(updates); + } catch (e) { + print('Error setting threshold: $e'); + rethrow; + } + } + + /// Set waktu penyiraman + Future setWaktuPenyiraman({ + String? waktu1, + String? waktu2, + bool? waktuEnabled, + }) async { + try { + final updates = {}; + if (waktu1 != null) updates['waktu_1'] = waktu1; + if (waktu2 != null) updates['waktu_2'] = waktu2; + if (waktuEnabled != null) updates['waktu'] = waktuEnabled; + await _database.child('kontrol').update(updates); + } catch (e) { + print('Error setting waktu: $e'); + rethrow; + } + } + + /// Set durasi penyiraman + Future setDurasi({int? durasi1, int? durasi2}) async { + try { + final updates = {}; + if (durasi1 != null) updates['durasi_1'] = durasi1; + if (durasi2 != null) updates['durasi_2'] = durasi2; + await _database.child('kontrol').update(updates); + } catch (e) { + print('Error setting durasi: $e'); + rethrow; + } + } + + /// Toggle mode otomatis + Future setOtomatis(bool value) async { + try { + await _database.child('kontrol/otomatis').set(value); + } catch (e) { + print('Error setting otomatis: $e'); + rethrow; + } + } + + // ==================== HELPER FUNCTIONS ==================== + + /// Parse value dari Firebase (handle empty string) + String _parseValue(dynamic value) { + if (value == null || value == '') { + return '0'; + } + return value.toString(); + } + + /// Get database reference (untuk advanced usage) + DatabaseReference get databaseRef => _database; + + /// Check connection status + Stream getConnectionStatus() { + return _database.child('.info/connected').onValue.map((event) { + return event.snapshot.value as bool? ?? false; + }); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f866ff4..f5405fc 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,14 @@ import Foundation import firebase_auth import firebase_core +import firebase_database import google_sign_in_ios import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index df110a8..d67ba43 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -137,6 +137,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.24.1" + firebase_database: + dependency: "direct main" + description: + name: firebase_database + sha256: "35b37c04307b99c5f746387ce03292531c3aa1de91facffbd9cff5e069a8b5fd" + url: "https://pub.dev" + source: hosted + version: "11.3.10" + firebase_database_platform_interface: + dependency: transitive + description: + name: firebase_database_platform_interface + sha256: "095342e96d94b486b8273afc6327f777d53b63a169bd4201e5153ee3b8210c11" + url: "https://pub.dev" + source: hosted + version: "0.2.6+10" + firebase_database_web: + dependency: transitive + description: + name: firebase_database_web + sha256: "05f9b871d97b3ca879937947d0728ea95294395e7ddd5685583e8662be99eb16" + url: "https://pub.dev" + source: hosted + version: "0.2.6+16" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index c513b17..f917843 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: rename: ^3.1.0 firebase_core: ^3.8.1 firebase_auth: ^5.3.4 + firebase_database: ^11.1.8 google_sign_in: ^6.2.1 shared_preferences: ^2.2.2