From 30a2a84e44054d1a0dd7c1ba082fb83e9cdad5a2 Mon Sep 17 00:00:00 2001 From: Dwikyyy Date: Thu, 30 Jul 2026 12:51:43 +0700 Subject: [PATCH] Upload files to "Source Code Mobile" --- Source Code Mobile/ac_config_provider.dart | 41 ++++++ Source Code Mobile/ac_provider.dart | 136 ++++++++++++++++++ Source Code Mobile/ac_setup_provider.dart | 158 ++++++++++++++++++++ Source Code Mobile/auth_provider.dart | 83 +++++++++++ Source Code Mobile/history_provider.dart | 160 +++++++++++++++++++++ 5 files changed, 578 insertions(+) create mode 100644 Source Code Mobile/ac_config_provider.dart create mode 100644 Source Code Mobile/ac_provider.dart create mode 100644 Source Code Mobile/ac_setup_provider.dart create mode 100644 Source Code Mobile/auth_provider.dart create mode 100644 Source Code Mobile/history_provider.dart diff --git a/Source Code Mobile/ac_config_provider.dart b/Source Code Mobile/ac_config_provider.dart new file mode 100644 index 0000000..6a0920b --- /dev/null +++ b/Source Code Mobile/ac_config_provider.dart @@ -0,0 +1,41 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:firebase_database/firebase_database.dart'; + +import '../services/firebase_service.dart'; + +class ACConfigProvider extends ChangeNotifier { + bool _hasConfig = false; + String _protocol = 'UNKNOWN'; + int _model = -1; + + bool get hasConfig => _hasConfig; + String get protocol => _protocol; + int get model => _model; + + StreamSubscription? _subscription; + + void startListening() { + _subscription?.cancel(); + _subscription = FirebaseService.acStateRef.onValue.listen((event) { + if (event.snapshot.value == null) { + _hasConfig = false; + } else { + final data = Map.from(event.snapshot.value as Map); + _hasConfig = + data['has_config'] == true || + (data['protocol'] != null && data['protocol'] != 'UNKNOWN'); + _protocol = data['protocol']?.toString() ?? 'UNKNOWN'; + _model = int.tryParse(data['model']?.toString() ?? '-1') ?? -1; + } + notifyListeners(); + }); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } +} diff --git a/Source Code Mobile/ac_provider.dart b/Source Code Mobile/ac_provider.dart new file mode 100644 index 0000000..8247a0d --- /dev/null +++ b/Source Code Mobile/ac_provider.dart @@ -0,0 +1,136 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:firebase_database/firebase_database.dart'; + +import '../models/ac_model.dart'; +import '../services/firebase_service.dart'; + +class ACProvider extends ChangeNotifier { + ACModel _ac = ACModel(power: false, mode: 'COOL', temp: 24, fan: 'AUTO', swing: false, controlMode: 'auto'); + + ACModel get ac => _ac; + + bool _isSending = false; + bool get isSending => _isSending; + + StreamSubscription? _subscription; + + // ================= LISTENER ================= + + void startListening() { + _subscription?.cancel(); + + _subscription = FirebaseService.acStateRef.onValue.listen((event) { + if (event.snapshot.value == null) return; + + final data = Map.from(event.snapshot.value as Map); + _ac = ACModel.fromMap(data); + notifyListeners(); + }); + } + + // ================= SEND COMMAND ================= + + Future _sendCommand(String cmd, String val) async { + try { + _isSending = true; + notifyListeners(); + await FirebaseService.acCommandRef.set({'command': cmd, 'value': val}); + + // Write control history record to Firebase + String actionText = '$cmd: $val'; + if (cmd == 'power') { + actionText = val == 'on' ? 'Menyalakan AC' : 'Mematikan AC'; + } else if (cmd == 'mode') { + actionText = 'Mengubah Mode ke ${val.toUpperCase()}'; + } else if (cmd == 'temp') { + actionText = 'Mengatur Suhu ke $val°C'; + } else if (cmd == 'fan') { + actionText = 'Mengatur Kipas ke ${val.toUpperCase()}'; + } else if (cmd == 'swing') { + actionText = val == 'on' ? 'Mengaktifkan Swing' : 'Mematikan Swing'; + } else if (cmd == 'control_mode') { + actionText = 'Mengubah Mode Kontrol ke ${val.toUpperCase()}'; + } + + final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; + await FirebaseService.controlHistoryRef.push().set({ + 'action': actionText, + 'source': 'Mobile', + 'timestamp': timestamp, + }); + + _isSending = false; + notifyListeners(); + } catch (e) { + _isSending = false; + notifyListeners(); + rethrow; + } + } + + // ================= SINGLE FIELD UPDATES ================= + + Future setPower(bool value) async { + await _sendCommand('power', value ? 'on' : 'off'); + _ac = _ac.copyWith(power: value); + notifyListeners(); + } + + Future setMode(String value) async { + await _sendCommand('mode', value.toLowerCase()); + _ac = _ac.copyWith(mode: value); + notifyListeners(); + } + + Future setTemp(int value) async { + if (value < 16 || value > 30) return; + await _sendCommand('temp', value.toString()); + _ac = _ac.copyWith(temp: value); + notifyListeners(); + } + + Future setFan(String value) async { + await _sendCommand('fan', value.toLowerCase()); + _ac = _ac.copyWith(fan: value); + notifyListeners(); + } + + Future toggleSwing() async { + final nextValue = !_ac.swing; + await _sendCommand('swing', nextValue ? 'on' : 'off'); + _ac = _ac.copyWith(swing: nextValue); + notifyListeners(); + } + + Future setControlMode(String value) async { + await _sendCommand('control_mode', value.toLowerCase()); + _ac = _ac.copyWith(controlMode: value); + notifyListeners(); + } + + // ================= CYCLE HELPERS ================= + + /// Cycles through: COOL → DRY → HEAT → FAN → AUTO → COOL + Future cycleMode() async { + const modes = ['COOL', 'DRY', 'HEAT', 'FAN', 'AUTO']; + final idx = modes.indexOf(_ac.mode.toUpperCase()); + final next = modes[(idx == -1 ? 0 : idx + 1) % modes.length]; + await setMode(next); + } + + /// Cycles through: AUTO → LOW → MED → HIGH → MAX → AUTO + Future cycleFan() async { + const fans = ['AUTO', 'LOW', 'MED', 'HIGH', 'MAX']; + final idx = fans.indexOf(_ac.fan.toUpperCase()); + final next = fans[(idx == -1 ? 0 : idx + 1) % fans.length]; + await setFan(next); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } +} diff --git a/Source Code Mobile/ac_setup_provider.dart b/Source Code Mobile/ac_setup_provider.dart new file mode 100644 index 0000000..7d6c1c6 --- /dev/null +++ b/Source Code Mobile/ac_setup_provider.dart @@ -0,0 +1,158 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:firebase_database/firebase_database.dart'; + +import '../services/firebase_service.dart'; + +class ACSetupProvider extends ChangeNotifier { + // Status from Firebase + String _mode = 'idle'; // idle, config, sweep, learn + String _step = 'not_configured'; // not_configured, select_brand, confirm, learning, ready + bool _hasSavedConfig = false; + int _brandIndex = 0; + String _brandName = ''; + String _brandDesc = ''; + String _modelName = ''; + String _message = ''; + + // Detected in learning + String _detectedProtocol = ''; + String _detectedDesc = ''; + + // Brands list from Firebase + List> _brands = []; + + // Getters + String get mode => _mode; + String get step => _step; + bool get hasSavedConfig => _hasSavedConfig; + int get brandIndex => _brandIndex; + String get brandName => _brandName; + String get brandDesc => _brandDesc; + String get modelName => _modelName; + String get message => _message; + String get detectedProtocol => _detectedProtocol; + String get detectedDesc => _detectedDesc; + List> get brands => _brands; + + StreamSubscription? _statusSubscription; + StreamSubscription? _brandsSubscription; + + bool _isSending = false; + bool get isSending => _isSending; + + void startListening() { + _statusSubscription?.cancel(); + _brandsSubscription?.cancel(); + + // 1. Listen to config status + _statusSubscription = FirebaseService.acConfigStatusRef.onValue.listen((event) { + if (event.snapshot.value == null) { + _mode = 'idle'; + _step = 'not_configured'; + _hasSavedConfig = false; + _brandIndex = 0; + _brandName = ''; + _brandDesc = ''; + _modelName = ''; + _message = ''; + _detectedProtocol = ''; + _detectedDesc = ''; + } else { + final data = Map.from(event.snapshot.value as Map); + _mode = data['mode']?.toString() ?? 'idle'; + _step = data['step']?.toString() ?? 'not_configured'; + _hasSavedConfig = data['has_config'] == true; + _brandIndex = int.tryParse(data['brand_index']?.toString() ?? '0') ?? 0; + _brandName = data['brand_name']?.toString() ?? ''; + _brandDesc = data['brand_desc']?.toString() ?? ''; + _modelName = data['model_name']?.toString() ?? ''; + _message = data['message']?.toString() ?? ''; + _detectedProtocol = data['detected_protocol']?.toString() ?? ''; + _detectedDesc = data['detected_desc']?.toString() ?? ''; + } + notifyListeners(); + }); + + // 2. Listen to config brands + _brandsSubscription = FirebaseService.acConfigBrandsRef.onValue.listen((event) { + if (event.snapshot.value == null) { + _brands = []; + } else { + final rawList = event.snapshot.value; + if (rawList is List) { + _brands = rawList + .where((item) => item != null) + .map((item) => Map.from(item as Map)) + .toList(); + } else if (rawList is Map) { + _brands = rawList.values + .where((item) => item != null) + .map((item) => Map.from(item as Map)) + .toList(); + } else { + _brands = []; + } + } + notifyListeners(); + }); + } + + Future _sendCommand(String cmd, String val) async { + try { + _isSending = true; + notifyListeners(); + await FirebaseService.acCommandRef.set({ + 'command': cmd, + 'value': val, + 'timestamp': ServerValue.timestamp, + }); + _isSending = false; + notifyListeners(); + } catch (e) { + _isSending = false; + notifyListeners(); + rethrow; + } + } + + Future startConfigMode() async { + await _sendCommand('config', ''); + } + + Future selectBrand(int index) async { + await _sendCommand('select_brand', index.toString()); + } + + Future confirm(String value) async { + await _sendCommand('confirm', value); + } + + Future cancelConfig() async { + await _sendCommand('cancel_config', ''); + } + + Future saveConfig() async { + await _sendCommand('save_config', ''); + } + + Future clearConfig() async { + await _sendCommand('clear_config', ''); + } + + Future startLearnMode() async { + await _sendCommand('learn', ''); + } + + Future sendRaw() async { + await _sendCommand('sendraw', ''); + } + + @override + void dispose() { + _statusSubscription?.cancel(); + _brandsSubscription?.cancel(); + super.dispose(); + } +} diff --git a/Source Code Mobile/auth_provider.dart b/Source Code Mobile/auth_provider.dart new file mode 100644 index 0000000..37c2195 --- /dev/null +++ b/Source Code Mobile/auth_provider.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import '../services/auth_service.dart'; + +class AuthProvider extends ChangeNotifier { + final AuthService _authService = AuthService(); + + bool _isLoading = false; + + bool get isLoading => _isLoading; + + Map? userData; + + // ================= LOGIN ================= + + Future login({ + required String email, + required String password, + }) async { + try { + _isLoading = true; + notifyListeners(); + + await _authService.login(email: email, password: password); + + userData = await _authService.getUserData(); + + _isLoading = false; + notifyListeners(); + + return null; + } catch (e) { + _isLoading = false; + notifyListeners(); + + return e.toString(); + } + } + + // ================= REGISTER ================= + + Future register({ + required String name, + required String email, + required String password, + }) async { + try { + _isLoading = true; + notifyListeners(); + + await _authService.register(name: name, email: email, password: password); + + userData = await _authService.getUserData(); + + _isLoading = false; + notifyListeners(); + + return null; + } catch (e) { + _isLoading = false; + notifyListeners(); + + return e.toString(); + } + } + + // ================= LOAD USER ================= + + Future loadUser() async { + userData = await _authService.getUserData(); + + notifyListeners(); + } + + // ================= LOGOUT ================= + + Future logout() async { + await _authService.logout(); + + userData = null; + + notifyListeners(); + } +} diff --git a/Source Code Mobile/history_provider.dart b/Source Code Mobile/history_provider.dart new file mode 100644 index 0000000..8c29e36 --- /dev/null +++ b/Source Code Mobile/history_provider.dart @@ -0,0 +1,160 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:firebase_database/firebase_database.dart'; + +import '../models/history_model.dart'; +import '../models/control_history_model.dart'; +import '../services/firebase_service.dart'; + +class HistoryProvider extends ChangeNotifier { + List monitoringHistory = []; + List controlHistory = []; + + bool isLoading = false; + + StreamSubscription? monitoringSubscription; + StreamSubscription? _manualSubscription; + StreamSubscription? _autoSubscription; + + final List _manualHistory = []; + final List _autoHistory = []; + + // ================= MONITORING ================= + + void startMonitoringListener() { + monitoringSubscription?.cancel(); + + monitoringSubscription = FirebaseService.monitoringHistoryRef.onValue + .listen((event) { + monitoringHistory.clear(); + + if (event.snapshot.value == null) { + notifyListeners(); + return; + } + + final data = Map.from(event.snapshot.value as Map); + + data.forEach((key, value) { + monitoringHistory.add( + HistoryModel.fromMap(Map.from(value)), + ); + }); + + monitoringHistory.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + + notifyListeners(); + }); + } + + // ================= CONTROL (Manual & Auto triggers combined) ================= + + void startControlListener() { + _manualSubscription?.cancel(); + _autoSubscription?.cancel(); + + // 1. Listen to manual controls + _manualSubscription = FirebaseService.controlHistoryRef.onValue.listen((event) { + _manualHistory.clear(); + if (event.snapshot.value != null) { + final data = Map.from(event.snapshot.value as Map); + data.forEach((key, value) { + _manualHistory.add( + ControlHistoryModel.fromMap(Map.from(value)), + ); + }); + } + _combineAndSortHistory(); + }); + + // 2. Listen to auto triggers + _autoSubscription = FirebaseService.acAutoTriggerHistoryRef.onValue.listen((event) { + _autoHistory.clear(); + if (event.snapshot.value != null) { + final data = Map.from(event.snapshot.value as Map); + data.forEach((key, value) { + final mapVal = Map.from(value); + final rawAction = mapVal['action']?.toString() ?? 'unknown'; + String actionText = rawAction; + if (rawAction == 'auto_off') { + actionText = 'AC Dimatikan Otomatis (Semua Parameter Baik)'; + } else if (rawAction == 'auto_dry') { + actionText = 'Mode Dry Otomatis (Kelembapan Tinggi)'; + } else if (rawAction == 'auto_cool') { + actionText = 'Mode Cool Otomatis (Suhu/Kelembapan Luar Batas)'; + } else if (rawAction == 'auto_fan') { + actionText = 'Mode Fan Otomatis (Udara Buruk)'; + } + + final ts = mapVal['timestamp'] is int ? mapVal['timestamp'] : 0; + _autoHistory.add( + ControlHistoryModel( + action: actionText, + source: 'Auto (ESP32)', + timestamp: _parseTimestampString(ts), + rawTimestamp: ts, + ), + ); + }); + } + _combineAndSortHistory(); + }); + } + + void _combineAndSortHistory() { + controlHistory = [..._manualHistory, ..._autoHistory]; + controlHistory.sort((a, b) => b.rawTimestamp.compareTo(a.rawTimestamp)); + notifyListeners(); + } + + String _parseTimestampString(int value) { + if (value == 0) return ''; + final dt = DateTime.fromMillisecondsSinceEpoch(value * 1000); + return "${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} " + "${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}"; + } + + // ================= CLEAR HISTORIES ================= + + Future clearMonitoringHistory() async { + try { + isLoading = true; + notifyListeners(); + await FirebaseService.monitoringHistoryRef.remove(); + monitoringHistory.clear(); + isLoading = false; + notifyListeners(); + } catch (e) { + isLoading = false; + notifyListeners(); + rethrow; + } + } + + Future clearControlHistory() async { + try { + isLoading = true; + notifyListeners(); + await FirebaseService.controlHistoryRef.remove(); + await FirebaseService.acAutoTriggerHistoryRef.remove(); + _manualHistory.clear(); + _autoHistory.clear(); + controlHistory.clear(); + isLoading = false; + notifyListeners(); + } catch (e) { + isLoading = false; + notifyListeners(); + rethrow; + } + } + + @override + void dispose() { + monitoringSubscription?.cancel(); + _manualSubscription?.cancel(); + _autoSubscription?.cancel(); + super.dispose(); + } +}