468 lines
15 KiB
Dart
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();
|
|
}
|
|
}
|