TKK_E32230814/lib/services/kontrol_automation_service....

468 lines
15 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'firebase_database_service.dart';
import 'automation_constants.dart';
import 'connection_monitor_service.dart';
import 'notification_service.dart';
/// Service untuk menghandle logika otomatis kontrol waktu dan sensor
/// Berjalan di background untuk monitoring dan eksekusi otomatis
class KontrolAutomationService {
static final KontrolAutomationService _instance =
KontrolAutomationService._internal();
factory KontrolAutomationService() => _instance;
KontrolAutomationService._internal();
final FirebaseDatabaseService _dbService = FirebaseDatabaseService();
final ConnectionMonitorService _connectionMonitor =
ConnectionMonitorService();
Timer? _waktuCheckTimer;
Timer? _sensorCheckTimer;
StreamSubscription? _sensorSubscription;
StreamSubscription? _kontrolSubscription;
bool _isWaktuModeActive = false;
bool _isSensorModeActive = false;
// State untuk mencegah trigger berulang
final Map<String, DateTime> _lastWateringTime = {};
final Map<String, bool> _isWateringActive = {};
// ==================== KONTROL WAKTU ====================
/// Start monitoring waktu mode
/// Uses NotificationService exact alarm scheduling instead of polling
/// This eliminates dependency on worker and timer-based checking
void startWaktuMode() {
// Disabled on mobile app to let Railway worker handle all scheduling/watering
debugPrint('🕐 Waktu Mode: Disabled on mobile app (handled by Railway worker instead)');
if (DateTime.now().year > 2000) return; // Early exit to prevent background scheduling/watering in app
if (_isWaktuModeActive) return;
// Start connection monitor for sensor fallback
_connectionMonitor.start();
_isWaktuModeActive = true;
debugPrint('🕐 Waktu Mode: Started (using exact alarm scheduling)');
// Setup callback to handle watering execution when alarm fires
NotificationService.setOnWateringScheduleCallback(
_handleWateringScheduleExecution,
);
// Schedule all watering executions using exact alarm
// This will fire even when app is minimized/killed
_scheduleWateringExecutions();
}
/// Stop monitoring waktu mode
void stopWaktuMode() {
_waktuCheckTimer?.cancel();
_waktuCheckTimer = null;
_isWaktuModeActive = false;
// Cancel all watering schedule executions
_cancelWateringSchedules();
// Clear callback
NotificationService.setOnWateringScheduleCallback(null);
debugPrint('🕐 Waktu Mode: Stopped');
}
/// Schedule all watering executions from Firebase
/// Called when waktu mode starts or when jadwal changes
Future<void> _scheduleWateringExecutions() async {
try {
await NotificationService.instance.scheduleAllWateringExecutions();
debugPrint(
'✅ All watering executions scheduled via notification service',
);
} catch (e) {
debugPrint('❌ Error scheduling watering executions: $e');
}
}
/// Cancel all watering schedule executions
Future<void> _cancelWateringSchedules() async {
try {
await NotificationService.instance.cancelAllWateringSchedules();
debugPrint('✅ All watering schedule executions cancelled');
} catch (e) {
debugPrint('❌ Error cancelling watering schedules: $e');
}
}
/// Handle watering execution when scheduled alarm fires
/// This is called from NotificationService when notification triggers
Future<void> _handleWateringScheduleExecution(dynamic scheduleData) async {
try {
if (scheduleData is! Map) {
debugPrint('⚠️ Invalid schedule data: $scheduleData');
return;
}
final data = Map<String, dynamic>.from(scheduleData);
final scheduleId = (data['scheduleId'] ?? 'unknown') as String;
final pompaAir = data['pompa_air'] ?? true;
final pompaPupuk = data['pompa_pupuk'] ?? false;
final durasiDetik = data['durasi'] ?? 60;
final potAktif = List<int>.from(data['pot_aktif'] ?? []);
if (potAktif.isNotEmpty) {
debugPrint(
'💧 Executing scheduled watering: $scheduleId (${durasiDetik}s)',
);
await _executeWatering(
scheduleId: scheduleId,
pompaAir: pompaAir,
pompaPupuk: pompaPupuk,
pots: potAktif,
durasiDetik: durasiDetik,
);
} else {
debugPrint('⚠️ No pots selected for $scheduleId');
}
} catch (e) {
debugPrint('❌ Error handling watering execution: $e');
}
}
/// Execute penyiraman dengan durasi tertentu
Future<void> _executeWatering({
required String scheduleId,
required bool pompaAir,
required bool pompaPupuk,
required List<int> pots,
required int durasiDetik,
}) async {
try {
_isWateringActive[scheduleId] = true;
_lastWateringTime[scheduleId] = DateTime.now();
// Nyalakan pompa dan valve
final updates = <String, bool>{};
if (pompaAir) updates['mosvet_1'] = true;
if (pompaPupuk) updates['mosvet_2'] = true;
for (var pot in pots) {
if (pot >= 1 && pot <= 5) {
updates['mosvet_${pot + 2}'] = true;
}
}
await _dbService.setMultipleAktuator(updates);
debugPrint('✅ Watering started: $updates');
// Tunggu sesuai durasi
await Future.delayed(Duration(seconds: durasiDetik));
// Matikan semua
final offUpdates = <String, bool>{};
updates.forEach((key, _) => offUpdates[key] = false);
await _dbService.setMultipleAktuator(offUpdates);
debugPrint('✅ Watering completed for $scheduleId');
// Send notification after watering is done
try {
final potText =
pots.length == 5 ? 'semua pot' : 'pot ${pots.join(', ')}';
await NotificationService.instance.showWateringNotification(
title: 'IrriGo - Penyiraman Selesai',
body:
'Penyiraman selesai untuk $potText selama ${durasiDetik} detik.',
payload: {
'type': 'watering_completed',
'scheduleId': scheduleId,
'pots': pots,
'duration': durasiDetik,
},
);
} catch (notifError) {
debugPrint('⚠️ Error sending notification: $notifError');
}
_isWateringActive[scheduleId] = false;
} catch (e) {
debugPrint('❌ Error executing watering: $e');
_isWateringActive[scheduleId] = false;
}
}
// ==================== KONTROL SENSOR ====================
/// Start monitoring sensor mode
void startSensorMode() {
// Disabled on mobile app to let Railway worker handle all automation
debugPrint('🌡️ Sensor Mode: Disabled on mobile app (handled by Railway worker instead)');
if (DateTime.now().year > 2000) return; // Early exit to prevent background sensor check in app
if (_isSensorModeActive) return;
// Start connection monitor
_connectionMonitor.start();
_isSensorModeActive = true;
debugPrint('🌡️ Sensor Mode: Started');
// Monitor perubahan sensor lebih responsif (gunakan constant)
_sensorCheckTimer = Timer.periodic(
Duration(seconds: AutomationConstants.sensorCheckInterval),
(timer) => _checkSensorThreshold(),
);
// Listen to sensor data real-time
_sensorSubscription = _dbService.getSensorDataStream().listen((sensorData) {
if (_isSensorModeActive) {
_processSensorData(sensorData);
}
});
// Jalankan check pertama kali
_checkSensorThreshold();
}
/// Stop monitoring sensor mode
void stopSensorMode() {
_sensorCheckTimer?.cancel();
_sensorCheckTimer = null;
_sensorSubscription?.cancel();
_sensorSubscription = null;
_isSensorModeActive = false;
debugPrint('🌡️ Sensor Mode: Stopped');
}
/// Check sensor threshold untuk semua pot
Future<void> _checkSensorThreshold() async {
try {
// Check connection first
if (!_connectionMonitor.isConnected) {
debugPrint('⚠️ No Firebase connection, skipping sensor check');
return;
}
final kontrolConfig = await _dbService.getKontrolConfig();
final otomatisEnabled = kontrolConfig['otomatis'] ?? false;
if (!otomatisEnabled || !_isSensorModeActive) return;
final batasAtas =
kontrolConfig['batas_atas'] ?? AutomationConstants.defaultBatasAtas;
final batasBawah =
kontrolConfig['batas_bawah'] ?? AutomationConstants.defaultBatasBawah;
final durasiSensor =
kontrolConfig['durasi_sensor'] ??
AutomationConstants.defaultDurasiDetik;
final modeSensor =
kontrolConfig['mode_sensor'] ?? AutomationConstants.modeSensorFixed;
final sensorData = await _dbService.getSensorData();
// Check setiap pot (soil_1 sampai soil_5)
debugPrint(
'🌡️ Checking thresholds: batas_bawah=$batasBawah, batas_atas=$batasAtas, mode=$modeSensor, durasi=${durasiSensor}s',
);
for (int i = 1; i <= AutomationConstants.totalPots; i++) {
final soilKey = 'soil_$i';
final soilValue = int.tryParse(sensorData[soilKey] ?? '0') ?? 0;
debugPrint('🌡️ $soilKey = $soilValue (threshold: $batasBawah)');
// Jika kelembapan di bawah batas bawah, siram pot tersebut
if (soilValue < batasBawah) {
debugPrint(
'⚠️ $soilKey ($soilValue) < batasBawah ($batasBawah) → Triggering watering for POT $i',
);
await _waterPotBySensor(
potNumber: i,
soilValue: soilValue,
batasBawah: batasBawah,
batasAtas: batasAtas,
durasiSeconds: durasiSensor,
mode: modeSensor,
);
}
}
} catch (e) {
debugPrint('❌ Error checking sensor threshold: $e');
}
}
/// Process sensor data real-time
void _processSensorData(Map<String, dynamic> sensorData) {
// Could be used for real-time alerts or logging
// For now, the periodic check is sufficient
}
/// Siram pot berdasarkan sensor
Future<void> _waterPotBySensor({
required int potNumber,
required int soilValue,
required int batasBawah,
required int batasAtas,
required int durasiSeconds,
required String mode,
}) async {
final potKey = 'pot_$potNumber';
// Prevent multiple watering in short time
if (_isWateringActive[potKey] == true) return;
final lastTime = _lastWateringTime[potKey];
if (lastTime != null) {
final diff = DateTime.now().difference(lastTime);
final cooldownSeconds = AutomationConstants.wateringCooldownSeconds;
if (diff.inSeconds < cooldownSeconds) {
debugPrint(
'⏳ POT $potNumber: Cooldown active (${cooldownSeconds - diff.inSeconds}s remaining)',
);
return; // Minimum cooldown antar penyiraman
}
}
try {
_isWateringActive[potKey] = true;
_lastWateringTime[potKey] = DateTime.now();
debugPrint(
'🌡️ Sensor Mode: Watering POT $potNumber (soil: $soilValue < $batasBawah)',
);
debugPrint('🔧 Mode: $mode, Durasi: ${durasiSeconds}s');
// Nyalakan pompa air dan valve pot tersebut
// pot 1 → mosvet_3, pot 2 → mosvet_4, ... pot 5 → mosvet_7
debugPrint(
'💧 Starting watering: POT $potNumber (mosvet_${potNumber + 2})',
);
await _dbService.setPompaAir(true);
await _dbService.setPot(potNumber, true);
int elapsedSeconds = 0;
if (mode == 'smart') {
// SMART MODE: Siram sampai batas atas atau timeout
debugPrint(
'🧠 Smart Mode: Target soil_$potNumber >= $batasAtas (currently: $soilValue), max ${durasiSeconds}s',
);
while (elapsedSeconds < durasiSeconds && _isSensorModeActive) {
await Future.delayed(const Duration(seconds: 5));
elapsedSeconds += 5;
// Check sensor lagi
final currentData = await _dbService.getSensorData();
final currentSoil =
int.tryParse(currentData['soil_$potNumber'] ?? '0') ?? 0;
debugPrint(
'💧 POT $potNumber watering: ${elapsedSeconds}s, soil_$potNumber: $currentSoil',
);
if (currentSoil >= batasAtas) {
debugPrint(
'✅ POT $potNumber reached target: $currentSoil >= $batasAtas (Smart Mode)',
);
break;
}
}
if (elapsedSeconds >= durasiSeconds) {
debugPrint(
'⏰ POT $potNumber timeout: ${elapsedSeconds}s (Smart Mode safety)',
);
}
} else {
// FIXED DURATION MODE: Siram selama durasi tetap
debugPrint(
'⏱️ Fixed Duration Mode: Watering for exactly ${durasiSeconds}s',
);
while (elapsedSeconds < durasiSeconds && _isSensorModeActive) {
await Future.delayed(const Duration(seconds: 5));
elapsedSeconds += 5;
// Check sensor untuk logging saja (tidak break)
final currentData = await _dbService.getSensorData();
final currentSoil =
int.tryParse(currentData['soil_$potNumber'] ?? '0') ?? 0;
debugPrint(
'💧 POT $potNumber watering: ${elapsedSeconds}s/${durasiSeconds}s, soil_$potNumber: $currentSoil',
);
}
debugPrint(
'✅ POT $potNumber fixed duration completed: ${durasiSeconds}s',
);
}
// Matikan pompa dan valve
await _dbService.setPompaAir(false);
await _dbService.setPot(potNumber, false);
debugPrint(
'✅ Sensor Mode: Watering completed for POT $potNumber (${elapsedSeconds}s, mode: $mode)',
);
// Send notification after watering is done
try {
await NotificationService.instance.showWateringNotification(
title: 'IrriGo - Penyiraman Sensor Selesai',
body:
'Penyiraman POT $potNumber selesai (kelembapan: $soilValue%, durasi: ${elapsedSeconds}s, mode: $mode).',
payload: {
'type': 'sensor_watering_completed',
'pot': potNumber,
'soil': soilValue,
'duration': elapsedSeconds,
'mode': mode,
},
);
} catch (notifError) {
debugPrint('⚠️ Error sending notification: $notifError');
}
// Pastikan flag ter-reset dengan benar
await Future.delayed(const Duration(milliseconds: 500));
_isWateringActive[potKey] = false;
} catch (e) {
debugPrint('❌ Error watering pot by sensor: $e');
// Matikan semua untuk safety
try {
await _dbService.setPompaAir(false);
await _dbService.setPot(potNumber, false);
} catch (cleanupError) {
debugPrint('❌ Error during cleanup: $cleanupError');
}
// Reset flag
_isWateringActive[potKey] = false;
}
}
// ==================== UTILITY ====================
/// Get status semua automation
Map<String, bool> getAutomationStatus() {
return {'waktuMode': _isWaktuModeActive, 'sensorMode': _isSensorModeActive};
}
/// Stop semua automation
void stopAll() {
stopWaktuMode();
stopSensorMode();
debugPrint('⏹️ All automation stopped');
}
/// Cleanup saat app dispose
void dispose() {
stopAll();
_kontrolSubscription?.cancel();
}
}