336 lines
10 KiB
Dart
336 lines
10 KiB
Dart
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<Map<String, dynamic>> _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<Map<String, dynamic>> 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<void> 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<MqttReceivedMessage<MqttMessage?>>? 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<String, dynamic> 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();
|
|
}
|
|
}
|