import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_browser_client.dart'; import 'package:typed_data/typed_data.dart'; class MQTTProvider extends ChangeNotifier { static const String MQTT_BROKER = "smartac.local"; // atau IP Raspberry Pi static const int MQTT_PORT = 9001; static const String PRESENCE_TOPIC = "classroom/presence"; static const String AC_STATUS_TOPIC = "classroom/ac/status"; static const String AC_CONTROL_TOPIC = "classroom/ac/control"; static const String WEBCAM_TOPIC = "classroom/webcam"; late MqttBrowserClient client; // Status String _connectionStatus = "Disconnected"; String _presenceStatus = "tidak ada"; String _acStatus = "off"; DateTime _lastUpdate = DateTime.now(); List> _history = []; bool _isAutoMode = true; // Detail AC int _currentTemp = 24; String _currentMode = "cool"; String _activeTime = "00:00:00"; DateTime? _acStartTime; Timer? _activeTimeTimer; // Timer Delay int _delayTimer = 5; bool _isDelayActive = false; int _delayRemaining = 0; // Webcam String? _webcamImage; int _detectionCount = 0; // Getter String get connectionStatus => _connectionStatus; String get presenceStatus => _presenceStatus; String get acStatus => _acStatus; DateTime get lastUpdate => _lastUpdate; List> get history => _history; bool get isAutoMode => _isAutoMode; int get currentTemp => _currentTemp; String get currentMode => _currentMode; String get activeTime => _activeTime; String? get webcamImage => _webcamImage; int get detectionCount => _detectionCount; int get delayTimer => _delayTimer; bool get isDelayActive => _isDelayActive; int get delayRemaining => _delayRemaining; Color get presenceColor => _presenceStatus == "ada" ? Colors.green : Colors.grey; Color get acColor => _acStatus == "on" ? Colors.orange : Colors.grey; IconData get acIcon => _acStatus == "on" ? Icons.ac_unit : Icons.ac_unit_outlined; MQTTProvider() { connect(); _startActiveTimeUpdater(); } void _startActiveTimeUpdater() { _activeTimeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_acStatus == "on" && _acStartTime != null) { final duration = DateTime.now().difference(_acStartTime!); final hours = duration.inHours; final minutes = duration.inMinutes.remainder(60); final seconds = duration.inSeconds.remainder(60); _activeTime = '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; notifyListeners(); } }); } Future connect() async { try { _connectionStatus = "Connecting..."; notifyListeners(); client = MqttBrowserClient( 'ws://$MQTT_BROKER:$MQTT_PORT', 'flutter_web_${DateTime.now().millisecondsSinceEpoch}', ); client.port = MQTT_PORT; client.websocketProtocols = ['mqtt', 'mqttv3.1']; client.keepAlivePeriod = 20; client.connectTimeoutPeriod = 5000; client.logging(on: false); final connMess = MqttConnectMessage() .withClientIdentifier( 'flutter_web_${DateTime.now().millisecondsSinceEpoch}') .startClean() .withWillQos(MqttQos.atLeastOnce); client.connectionMessage = connMess; await client.connect(); if (client.connectionStatus?.state == MqttConnectionState.connected) { _connectionStatus = "Connected"; _subscribeToTopics(); requestWebcamFrame(); } else { _connectionStatus = "Connection Failed"; } } catch (e) { _connectionStatus = "Error"; print('Connection Error: $e'); } notifyListeners(); } void _subscribeToTopics() { if (client.connectionStatus?.state != MqttConnectionState.connected) return; client.subscribe(PRESENCE_TOPIC, MqttQos.atLeastOnce); client.subscribe(AC_STATUS_TOPIC, MqttQos.atLeastOnce); client.subscribe(WEBCAM_TOPIC, MqttQos.atLeastOnce); client.updates!.listen((List>? messages) { if (messages == null || messages.isEmpty) return; final recMess = messages[0]; final message = recMess.payload as MqttPublishMessage; final topic = recMess.topic; final payloadBytes = message.payload.message; _lastUpdate = DateTime.now(); if (topic == PRESENCE_TOPIC) { _presenceStatus = String.fromCharCodes(payloadBytes).trim(); print('📥 Presence: $_presenceStatus'); _addToHistory('presence', _presenceStatus); notifyListeners(); } else if (topic == AC_STATUS_TOPIC) { final payload = String.fromCharCodes(payloadBytes).trim(); print('📥 AC Status: $payload'); _handleACStatus(payload); } else if (topic == WEBCAM_TOPIC) { try { _webcamImage = String.fromCharCodes(payloadBytes).trim(); _detectionCount++; print('📸 Webcam frame: ${_detectionCount}'); notifyListeners(); } catch (e) { print('Webcam error: $e'); } } }); } void _handleACStatus(String payload) { try { Map data = jsonDecode(payload); // Update status AC if (data.containsKey('ac_state')) { final newStatus = data['ac_state']; if (_acStatus != newStatus) { print('🔄 Status AC berubah: $_acStatus -> $newStatus'); _acStatus = newStatus; // Update waktu aktif if (_acStatus == "on" && _acStartTime == null) { _acStartTime = DateTime.now(); } else if (_acStatus == "off") { _acStartTime = null; _activeTime = "00:00:00"; } } } // Update timer delay if (data.containsKey('delay_active')) { _isDelayActive = data['delay_active'] ?? false; } if (data.containsKey('delay_remaining')) { _delayRemaining = data['delay_remaining'] ?? 0; } // Update suhu if (data.containsKey('temperature')) { _currentTemp = data['temperature']; } // Update mode if (data.containsKey('mode')) { _currentMode = data['mode']; } // Update auto mode if (data.containsKey('auto_mode')) { _isAutoMode = data['auto_mode']; } } catch (e) { print('Error parsing AC status: $e'); // Fallback: mungkin payload plain text if (payload == "on" || payload == "off") { if (_acStatus != payload) { print('🔄 Status AC berubah (plain): $_acStatus -> $payload'); _acStatus = payload; } } } _addToHistory('ac', _acStatus); notifyListeners(); } void setDelayTimer(int minutes) { if (minutes < 1) minutes = 1; if (minutes > 60) minutes = 60; _delayTimer = minutes; _addToHistory('setting', 'Timer delay diubah ke $minutes menit'); print('⚙️ Timer: $minutes menit'); controlAC("delay_$minutes", manual: true); notifyListeners(); } void controlAC(String command, {bool manual = true}) { if (client.connectionStatus?.state != MqttConnectionState.connected) { _connectionStatus = "Disconnected"; notifyListeners(); return; } print('📤 Perintah: $command ${manual ? "(Manual)" : "(Auto)"}'); final buffer = Uint8Buffer(); buffer.addAll(command.codeUnits); client.publishMessage(AC_CONTROL_TOPIC, MqttQos.atLeastOnce, buffer); _addToHistory('command', command, manual: manual); } void setTemperature(int temp) { if (_currentTemp != temp) { controlAC("temp_$temp", manual: true); _currentTemp = temp; notifyListeners(); } } void setMode(String mode) { if (_currentMode != mode) { controlAC("mode_$mode", manual: true); _currentMode = mode; notifyListeners(); } } void toggleAutoMode() { _isAutoMode = !_isAutoMode; if (_isAutoMode) { print('🤖 Mode AUTO diaktifkan'); controlAC("auto_on", manual: true); } else { print('👆 Mode MANUAL diaktifkan'); controlAC("auto_off", manual: true); } notifyListeners(); } void requestWebcamFrame() { if (client.connectionStatus?.state == MqttConnectionState.connected) { final buffer = Uint8Buffer(); buffer.addAll("request".codeUnits); client.publishMessage( "classroom/webcam/request", MqttQos.atLeastOnce, buffer); } } void _addToHistory(String type, String value, {bool manual = false}) { String display = value; if (type == 'presence') { display = value == "ada" ? "👥 Orang terdeteksi" : "🚪 Ruangan kosong"; } else if (type == 'command') { if (value == "on") display = "🔛 AC Dinyalakan"; else if (value == "off") display = "🔴 AC Dimatikan"; else if (value == "auto_on") display = "🤖 Mode Auto diaktifkan"; else if (value == "auto_off") display = "👆 Mode Manual diaktifkan"; else if (value.startsWith('temp_')) display = "🌡️ Suhu ${value.split('_')[1]}°C"; else if (value.startsWith('mode_')) display = "🎛️ Mode ${value.split('_')[1].toUpperCase()}"; else if (value.startsWith('delay_')) display = "⏰ Timer ${value.split('_')[1]} menit"; } else if (type == 'ac') { display = value == "on" ? "❄️ AC Menyala" : "⭕ AC Mati"; } else if (type == 'setting') { display = "⚙️ $value"; } _history.insert(0, { 'time': DateTime.now(), 'type': type, 'value': display, 'mode': manual ? 'manual' : 'auto', }); if (_history.length > 50) _history.removeLast(); } void disconnect() { _activeTimeTimer?.cancel(); if (client.connectionStatus?.state == MqttConnectionState.connected) { client.disconnect(); } } @override void dispose() { disconnect(); super.dispose(); } }